@hachej/boring-workspace 0.1.23 → 0.1.26

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
@@ -165,7 +165,7 @@ data: ${JSON.stringify({ v: UI_BRIDGE_PROTOCOL_VERSION })}
165
165
  }
166
166
 
167
167
  // src/server/ui-control/tools/uiTools.ts
168
- import { access } from "fs/promises";
168
+ import { stat } from "fs/promises";
169
169
  import { resolve, isAbsolute, relative, win32 } from "path";
170
170
  function makeError(message) {
171
171
  return {
@@ -203,7 +203,7 @@ function validatePathSyntax(relPath, workspaceRoot) {
203
203
  }
204
204
  return { ok: true };
205
205
  }
206
- async function validatePath(workspaceRoot, relPath) {
206
+ async function validateExistingPath(workspaceRoot, relPath) {
207
207
  const syntax = validatePathSyntax(relPath, workspaceRoot);
208
208
  if (!syntax.ok) return syntax;
209
209
  const resolved = resolve(workspaceRoot, relPath);
@@ -215,8 +215,8 @@ async function validatePath(workspaceRoot, relPath) {
215
215
  };
216
216
  }
217
217
  try {
218
- await access(resolved);
219
- return { ok: true };
218
+ const fileStat = await stat(resolved);
219
+ return { ok: true, kind: fileStat.isDirectory() ? "dir" : "file" };
220
220
  } catch {
221
221
  return {
222
222
  ok: false,
@@ -227,6 +227,7 @@ async function validatePath(workspaceRoot, relPath) {
227
227
  function createGetUiStateTool(uiBridge) {
228
228
  return {
229
229
  name: "get_ui_state",
230
+ readinessRequirements: ["ui-bridge"],
230
231
  description: [
231
232
  "Read the current workspace UI state. Returns a JSON object with:",
232
233
  "- workbenchOpen (boolean): is the right-side workbench pane visible?",
@@ -280,12 +281,13 @@ function isVerified(kind, params, state) {
280
281
  return true;
281
282
  }
282
283
  function createExecUiTool(uiBridge, opts = {}) {
283
- const { workspaceRoot } = opts;
284
+ const { workspaceRoot, resolvePathKind } = opts;
284
285
  const verifyDelayMs = opts.verifyDelayMs ?? 200;
285
286
  const verifyRetries = opts.verifyRetries ?? 2;
286
287
  const verifyIntervalMs = opts.verifyIntervalMs ?? 200;
287
288
  return {
288
289
  name: "exec_ui",
290
+ readinessRequirements: ["ui-bridge"],
289
291
  description: [
290
292
  "Execute a UI command in the workspace. Use this to open files, panels,",
291
293
  "navigate to lines, or show notifications.",
@@ -319,6 +321,8 @@ function createExecUiTool(uiBridge, opts = {}) {
319
321
  " returned \u2014 don't give up and don't switch to the read",
320
322
  " tool. Repeat until openFile succeeds or no candidate",
321
323
  " is found.",
324
+ " If the path is a folder, openFile reveals/selects it in",
325
+ " the file tree instead of opening an editor tab.",
322
326
  " Example: {kind:'openFile', params:{path:'README.md'}}",
323
327
  "",
324
328
  " openPanel params: { id: string, component: string,",
@@ -402,6 +406,7 @@ function createExecUiTool(uiBridge, opts = {}) {
402
406
  return makeError("openSurface: meta must be an object when provided");
403
407
  }
404
408
  }
409
+ let effectiveKind = kind;
405
410
  if (PATH_BEARING_KINDS.has(kind)) {
406
411
  const relPath = getPathParam(kind, cmdParams);
407
412
  if (!relPath) {
@@ -412,14 +417,25 @@ function createExecUiTool(uiBridge, opts = {}) {
412
417
  const syntax = validatePathSyntax(relPath, workspaceRoot);
413
418
  if (!syntax.ok) return makeError(syntax.reason);
414
419
  if (workspaceRoot) {
415
- const check = await validatePath(workspaceRoot, relPath);
420
+ const check = await validateExistingPath(workspaceRoot, relPath);
416
421
  if (!check.ok) {
417
422
  return makeError(check.reason);
418
423
  }
424
+ if (kind === "openFile" && check.kind === "dir") {
425
+ effectiveKind = "expandToFile";
426
+ }
427
+ } else if (resolvePathKind) {
428
+ const pathKind = await resolvePathKind(relPath);
429
+ if (!pathKind) {
430
+ return makeError(`file not found at "${relPath}". Try find or grep to locate the file before retrying openFile.`);
431
+ }
432
+ if (kind === "openFile" && pathKind === "dir") {
433
+ effectiveKind = "expandToFile";
434
+ }
419
435
  }
420
436
  }
421
437
  try {
422
- const command = { kind, params: cmdParams };
438
+ const command = { kind: effectiveKind, params: cmdParams };
423
439
  const result = await uiBridge.postCommand(command);
424
440
  if (result.status === "error") {
425
441
  return {
@@ -428,11 +444,11 @@ function createExecUiTool(uiBridge, opts = {}) {
428
444
  details: result
429
445
  };
430
446
  }
431
- if (verifyDelayMs > 0 && VERIFIABLE_KINDS.has(kind)) {
447
+ if (verifyDelayMs > 0 && VERIFIABLE_KINDS.has(effectiveKind)) {
432
448
  await new Promise((r) => setTimeout(r, verifyDelayMs));
433
449
  let uiState = await uiBridge.getState();
434
450
  for (let i = 0; i < verifyRetries; i++) {
435
- if (isVerified(kind, cmdParams, uiState)) break;
451
+ if (isVerified(effectiveKind, cmdParams, uiState)) break;
436
452
  await new Promise((r) => setTimeout(r, verifyIntervalMs));
437
453
  uiState = await uiBridge.getState();
438
454
  }
@@ -746,7 +762,7 @@ function buildBoringSystemPrompt(opts) {
746
762
  if (opts.scaffoldCommand) {
747
763
  n += 1;
748
764
  steps.push(
749
- `**${n}. Scaffold.** Bash \`${opts.scaffoldCommand} <kebab-name> "$BORING_AGENT_WORKSPACE_ROOT"\` \u2014 writes canonical files under \`$BORING_AGENT_WORKSPACE_ROOT/.pi/extensions/<kebab-name>/\`. Read the generated \`package.json\` + \`front/index.tsx\`. Do NOT skip this or write from memory. Never \`cd\` to a parent repo or write plugins outside \`$BORING_AGENT_WORKSPACE_ROOT/.pi/extensions/\`.`
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.`
750
766
  );
751
767
  } else {
752
768
  n += 1;
@@ -772,7 +788,7 @@ function buildBoringSystemPrompt(opts) {
772
788
  steps.push(`**${n}. Ask the user to run \`/reload\`** to publish the change.`);
773
789
  const docsBlock = boringPiRoot ? [
774
790
  "## boring-ui plugin authoring documentation",
775
- "Read these only when the user asks to build, modify, or debug a workspace plugin. Use your `read` tool with the absolute path; the agent runtime guarantees these files exist on the host:",
791
+ "Read these only when the user asks to build, modify, or debug a workspace plugin. Use your `read` tool with these workspace-relative paths; the agent runtime guarantees they exist inside `$BORING_AGENT_WORKSPACE_ROOT`:",
776
792
  ...buildDocsRefs(boringPiRoot).map((r) => `- ${r.topic}: ${r.path}`),
777
793
  "Follow .md cross-references when present (e.g. SKILL.md may link to a reference doc \u2014 read both)."
778
794
  ].join("\n") : [
@@ -780,7 +796,7 @@ function buildBoringSystemPrompt(opts) {
780
796
  "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."
781
797
  ].join("\n");
782
798
  return [
783
- "You are operating inside boring-ui. Workspace root: `$BORING_AGENT_WORKSPACE_ROOT`; plugin files go under `$BORING_AGENT_WORKSPACE_ROOT/.pi/extensions/<name>/`.",
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.",
784
800
  [
785
801
  "## Plugin authoring \u2014 required workflow",
786
802
  "",
@@ -790,7 +806,7 @@ function buildBoringSystemPrompt(opts) {
790
806
  "- API factories: `createPlugin`, `defineFrontPlugin`, `defineComponent` \u2014 use `definePlugin({id, panels, commands, ...})` from `@hachej/boring-workspace/plugin`.",
791
807
  "- Imperative method names: `registerComponent`, `addPanel`, `registerCommand` (no `Panel`), `registerTab` \u2014 the actual names are `registerPanel`, `registerPanelCommand`, `registerLeftTab`, `registerSurfaceResolver` (and you usually express these declaratively, not as method calls).",
792
808
  "- Import paths: `@hachej/boring-pi` (it's a skills package, not for code), `@boring-ui/*`, `@hachej/pi-sdk` \u2014 use `@hachej/boring-workspace/plugin` for front and `@hachej/boring-workspace/server` for server.",
793
- '- File visualizers: for `.csv`/file-tree opens, import `WORKSPACE_OPEN_PATH_SURFACE_KIND` (and `PaneProps`) from `@hachej/boring-workspace/plugin`, read `request.target`, and fetch `/api/v1/files/raw?path=${encodeURIComponent(request.target)}`. Never import these from the root package, use `/workspace/read`, or string kind `"WORKSPACE_OPEN_PATH_SURFACE_KIND"`.',
809
+ '- File visualizers: import `WORKSPACE_OPEN_PATH_SURFACE_KIND`/`PaneProps` from `@hachej/boring-workspace/plugin`; import `useApiBaseUrl`/`useWorkspaceRequestId` from `@hachej/boring-workspace`; read `request.target`; fetch `${apiBaseUrl}/api/v1/files/raw?...` with `credentials: "include"` and `x-boring-workspace-id` when present. Never use `/workspace/read` or string kind `"WORKSPACE_OPEN_PATH_SURFACE_KIND"`.',
794
810
  "- Pi extension tools: `defineTool` and `export const tools` do NOT exist. Export `default function (pi) { pi.registerTool({ name, description, execute }) }`.",
795
811
  '- Server/Pi tool method: `handler` \u2014 use `execute`. Return shape: `{ content: [{ type: "text", text }] }` (NEVER a bare string).',
796
812
  "- 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.",
@@ -804,7 +820,7 @@ function buildBoringSystemPrompt(opts) {
804
820
  // src/server/agentPlugins/manager.ts
805
821
  import { createHash } from "crypto";
806
822
  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";
807
- import { dirname as dirname5, isAbsolute as isAbsolute3, join as join4, relative as relative3, resolve as resolve4 } from "path";
823
+ import { dirname as dirname5, isAbsolute as isAbsolute3, join as join4, relative as relative3, resolve as resolve5 } from "path";
808
824
 
809
825
  // src/shared/plugins/manifest.ts
810
826
  var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
@@ -854,8 +870,8 @@ function validateBoringField(issues, boring) {
854
870
  ));
855
871
  }
856
872
  }
857
- if (boring.id !== void 0) {
858
- issues.push(issue("INVALID_FIELD", "boring.id", "boring.id is not supported; package discovery identity comes from package.json#name"));
873
+ if (boring.id !== void 0 && (typeof boring.id !== "string" || !isValidBoringPluginId(boring.id))) {
874
+ issues.push(issue("INVALID_ID", "boring.id", "boring.id must start with a letter or number and use only letters, numbers, dot, underscore, colon, or dash"));
859
875
  }
860
876
  const front = boring.front;
861
877
  if (front !== void 0 && (typeof front !== "string" || !isSafePluginRelativePath(front))) {
@@ -869,6 +885,7 @@ function validateBoringField(issues, boring) {
869
885
  issues.push(issue("INVALID_FIELD", "boring.label", "boring.label must be a string when provided"));
870
886
  }
871
887
  return {
888
+ ...typeof boring.id === "string" ? { id: boring.id } : {},
872
889
  ...typeof boring.front === "string" ? { front: boring.front } : {},
873
890
  ...typeof boring.server === "string" || boring.server === false ? { server: boring.server } : {},
874
891
  ...typeof boring.label === "string" ? { label: boring.label } : {}
@@ -998,6 +1015,8 @@ function resolveContainedPluginPath(rootDir, value, options = {}) {
998
1015
 
999
1016
  // src/server/agentPlugins/scan.ts
1000
1017
  function pluginIdFromPackageJson(pkg, rootDir) {
1018
+ const explicitId = typeof pkg.boring?.id === "string" && pkg.boring.id.trim() ? pkg.boring.id.trim() : void 0;
1019
+ if (explicitId) return explicitId;
1001
1020
  const name = typeof pkg.name === "string" && pkg.name.trim() ? pkg.name.trim() : void 0;
1002
1021
  return (name ?? rootDir.split(/[\\/]/).at(-1) ?? "plugin").replace(/^@/, "").replaceAll("/", "-");
1003
1022
  }
@@ -1177,8 +1196,8 @@ import { dirname as dirname4, join as join3 } from "path";
1177
1196
  var PLUGIN_SIGNATURE_CACHE_FILE = ".boring-signature.json";
1178
1197
  function pluginFileSignature(path) {
1179
1198
  if (!path || !existsSync3(path)) return "missing";
1180
- const stat = statSync2(path);
1181
- return `${stat.mtimeMs}:${stat.size}`;
1199
+ const stat2 = statSync2(path);
1200
+ return `${stat2.mtimeMs}:${stat2.size}`;
1182
1201
  }
1183
1202
  function cachePath(pluginRootDir) {
1184
1203
  return join3(pluginRootDir, PLUGIN_SIGNATURE_CACHE_FILE);
@@ -1222,7 +1241,48 @@ function clearPluginSignatureCache(pluginRootDir) {
1222
1241
  if (existsSync3(path)) rmSync(path, { force: true });
1223
1242
  }
1224
1243
 
1244
+ // src/server/agentPlugins/piPackages.ts
1245
+ import { resolve as resolve4 } from "path";
1246
+ var REMOTE_PI_PACKAGE_PREFIXES2 = ["npm:", "git:", "github:", "http:", "https:", "ssh:"];
1247
+ function isRemotePiPackageSource2(source) {
1248
+ return REMOTE_PI_PACKAGE_PREFIXES2.some((prefix) => source.startsWith(prefix));
1249
+ }
1250
+ function packageLocalPathFromSource(source) {
1251
+ if (isRemotePiPackageSource2(source)) return null;
1252
+ return source.startsWith("file:") ? source.slice("file:".length) : source;
1253
+ }
1254
+ function normalizeLocalPiPackageSource(pluginRoot, source) {
1255
+ const localPath = packageLocalPathFromSource(source);
1256
+ if (localPath == null) return source;
1257
+ if (localPath === "." || localPath === "./") return resolve4(pluginRoot);
1258
+ const normalized = localPath.startsWith("./") ? localPath.slice(2) : localPath;
1259
+ if (!isSafePluginRelativePath(normalized)) {
1260
+ throw new Error(`unsafe Pi package source: ${source}`);
1261
+ }
1262
+ return resolve4(pluginRoot, normalized);
1263
+ }
1264
+ function normalizeBoringPluginPiPackageSource(pluginRoot, source) {
1265
+ if (typeof source === "string") return normalizeLocalPiPackageSource(pluginRoot, source);
1266
+ return {
1267
+ source: normalizeLocalPiPackageSource(pluginRoot, source.source),
1268
+ ...source.extensions ? { extensions: source.extensions } : {},
1269
+ ...source.skills ? { skills: source.skills } : {},
1270
+ ...source.prompts ? { prompts: source.prompts } : {},
1271
+ ...source.themes ? { themes: source.themes } : {}
1272
+ };
1273
+ }
1274
+ function normalizeBoringPluginPiPackages(plugins) {
1275
+ return plugins.flatMap(
1276
+ (plugin) => (plugin.pi?.packages ?? []).map(
1277
+ (source) => normalizeBoringPluginPiPackageSource(plugin.rootDir, source)
1278
+ )
1279
+ );
1280
+ }
1281
+
1225
1282
  // src/server/agentPlugins/manager.ts
1283
+ function skillPathForPiLoader(path) {
1284
+ return existsSync4(join4(path, "SKILL.md")) ? dirname5(path) : path;
1285
+ }
1226
1286
  function preflightErrorId(pluginDir) {
1227
1287
  return `preflight-${createHash("sha256").update(pluginDir).digest("hex").slice(0, 12)}`;
1228
1288
  }
@@ -1245,8 +1305,8 @@ function directorySignature(root) {
1245
1305
  count++;
1246
1306
  const path = join4(dir, entry.name);
1247
1307
  const rel = relative3(root, path);
1248
- const stat = lstatSync(path);
1249
- if (stat.isSymbolicLink()) {
1308
+ const stat2 = lstatSync(path);
1309
+ if (stat2.isSymbolicLink()) {
1250
1310
  let target;
1251
1311
  try {
1252
1312
  target = realpathSync2(path);
@@ -1271,9 +1331,9 @@ function directorySignature(root) {
1271
1331
  continue;
1272
1332
  }
1273
1333
  hash.update(rel);
1274
- hash.update(String(stat.mtimeMs));
1275
- hash.update(String(stat.size));
1276
- if (stat.isDirectory()) {
1334
+ hash.update(String(stat2.mtimeMs));
1335
+ hash.update(String(stat2.size));
1336
+ if (stat2.isDirectory()) {
1277
1337
  visit(path, depth + 1);
1278
1338
  }
1279
1339
  }
@@ -1281,8 +1341,17 @@ function directorySignature(root) {
1281
1341
  visit(root, 0);
1282
1342
  return hash.digest("hex");
1283
1343
  }
1344
+ function normalizePluginSubpath(rootDir, path) {
1345
+ return relative3(rootDir, path).replaceAll("\\", "/");
1346
+ }
1347
+ function frontSignatureRoot(plugin) {
1348
+ if (!plugin.frontPath) return void 0;
1349
+ const frontRoot = join4(plugin.rootDir, "front");
1350
+ const rel = relative3(frontRoot, plugin.frontPath);
1351
+ return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel) ? frontRoot : dirname5(plugin.frontPath);
1352
+ }
1284
1353
  function pluginSignature(plugin) {
1285
- 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(plugin.frontPath ? dirname5(plugin.frontPath) : void 0)).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");
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");
1286
1355
  }
1287
1356
  function computeRequiresRestart(previous, next) {
1288
1357
  if (!previous) return [];
@@ -1297,32 +1366,55 @@ function computeRequiresRestart(previous, next) {
1297
1366
  var BoringPluginAssetManager = class {
1298
1367
  pluginDirs;
1299
1368
  errorRoot;
1369
+ frontTargetResolver;
1370
+ includeLegacyFrontUrl;
1300
1371
  loaded = /* @__PURE__ */ new Map();
1301
1372
  revisions = /* @__PURE__ */ new Map();
1302
1373
  listeners = /* @__PURE__ */ new Set();
1374
+ lastErrors = /* @__PURE__ */ new Map();
1303
1375
  loading = null;
1304
1376
  reloadQueued = false;
1305
1377
  constructor(options) {
1306
1378
  this.pluginDirs = options.pluginDirs;
1307
1379
  this.errorRoot = options.errorRoot ?? join4(process.cwd(), ".pi", "extensions");
1380
+ this.frontTargetResolver = options.frontTargetResolver;
1381
+ this.includeLegacyFrontUrl = options.includeLegacyFrontUrl ?? true;
1308
1382
  }
1309
1383
  preflight() {
1310
1384
  return preflightBoringPlugins(this.pluginDirs);
1311
1385
  }
1312
1386
  list() {
1387
+ return [...this.loaded.values()].map((plugin) => this.toListEntry(plugin));
1388
+ }
1389
+ getError(pluginId) {
1390
+ const path = this.errorPath(pluginId);
1391
+ if (!path || !existsSync4(path)) return null;
1392
+ return readFileSync3(path, "utf8");
1393
+ }
1394
+ getErrors() {
1395
+ return [...this.lastErrors.values()];
1396
+ }
1397
+ inspectLoaded() {
1313
1398
  return [...this.loaded.values()].map((plugin) => ({
1314
1399
  id: plugin.id,
1315
- boring: plugin.boring,
1316
- ...plugin.pi ? { pi: plugin.pi } : {},
1317
1400
  version: plugin.version,
1318
1401
  revision: plugin.revision,
1319
- ...plugin.frontUrl ? { frontUrl: plugin.frontUrl } : {}
1402
+ rootDir: plugin.rootDir,
1403
+ ...plugin.frontPath ? { frontPath: plugin.frontPath } : {},
1404
+ ...plugin.frontTarget ? { frontTarget: plugin.frontTarget } : {}
1320
1405
  }));
1321
1406
  }
1322
- getError(pluginId) {
1323
- const path = this.errorPath(pluginId);
1324
- if (!path || !existsSync4(path)) return null;
1325
- return readFileSync3(path, "utf8");
1407
+ inspectLoadedPiSnapshot() {
1408
+ const plugins = [...this.loaded.values()];
1409
+ const prompts = plugins.map((plugin) => plugin.pi?.systemPrompt?.trim()).filter((prompt) => Boolean(prompt));
1410
+ return {
1411
+ additionalSkillPaths: [...new Set(plugins.flatMap((plugin) => plugin.skillPaths ?? []).map(skillPathForPiLoader))],
1412
+ packages: compactPiPackages(normalizeBoringPluginPiPackages(plugins)),
1413
+ extensionPaths: plugins.flatMap((plugin) => plugin.extensionPaths ?? []),
1414
+ ...prompts.length > 0 ? { systemPromptAppend: `# Loaded boring-ui plugin context
1415
+
1416
+ ${prompts.join("\n\n")}` } : {}
1417
+ };
1326
1418
  }
1327
1419
  subscribe(listener) {
1328
1420
  this.listeners.add(listener);
@@ -1347,19 +1439,21 @@ var BoringPluginAssetManager = class {
1347
1439
  return result;
1348
1440
  }
1349
1441
  async doLoadOnce() {
1442
+ this.lastErrors.clear();
1350
1443
  const scan = scanBoringPlugins(this.pluginDirs);
1351
1444
  const nextPlugins = scan.plugins;
1352
1445
  const nextIds = new Set(nextPlugins.map((plugin) => plugin.id));
1353
- const invalidPluginDirs = new Set(scan.preflight.errors.map((error) => resolve4(error.pluginDir)));
1446
+ const invalidPluginDirs = new Set(scan.preflight.errors.map((error) => resolve5(error.pluginDir)));
1354
1447
  const events = [];
1355
1448
  const errors = [];
1356
1449
  this.collectPreflightErrors(scan.preflight, events, errors);
1357
1450
  for (const id of [...this.loaded.keys()]) {
1358
1451
  if (nextIds.has(id)) continue;
1359
1452
  const previous = this.loaded.get(id);
1360
- if (previous && invalidPluginDirs.has(resolve4(previous.rootDir))) continue;
1453
+ if (previous && invalidPluginDirs.has(resolve5(previous.rootDir))) continue;
1361
1454
  const revision = this.bumpRevision(id);
1362
1455
  this.loaded.delete(id);
1456
+ this.lastErrors.delete(id);
1363
1457
  if (previous) {
1364
1458
  try {
1365
1459
  clearPluginSignatureCache(previous.rootDir);
@@ -1376,9 +1470,17 @@ var BoringPluginAssetManager = class {
1376
1470
  const previous = this.loaded.get(plugin.id);
1377
1471
  if (previous?.signature === signature) continue;
1378
1472
  const revision = this.bumpRevision(plugin.id);
1473
+ const frontTarget = this.resolveFrontTarget(plugin, revision);
1379
1474
  const serverSignature = plugin.serverPath ? pluginFileSignature(plugin.serverPath) : null;
1380
- const record = { ...plugin, revision, signature, serverSignature };
1475
+ const record = {
1476
+ ...plugin,
1477
+ revision,
1478
+ signature,
1479
+ ...frontTarget ? { frontTarget } : {},
1480
+ serverSignature
1481
+ };
1381
1482
  this.loaded.set(plugin.id, record);
1483
+ this.lastErrors.delete(plugin.id);
1382
1484
  this.clearError(plugin.id);
1383
1485
  try {
1384
1486
  writePluginSignatureCache(plugin.rootDir, { serverSignature });
@@ -1391,7 +1493,8 @@ var BoringPluginAssetManager = class {
1391
1493
  boring: plugin.boring,
1392
1494
  version: plugin.version,
1393
1495
  revision,
1394
- ...plugin.frontUrl ? { frontUrl: plugin.frontUrl } : {},
1496
+ ...this.frontUrlPayload(plugin.frontUrl),
1497
+ ...frontTarget ? { frontTarget } : {},
1395
1498
  ...requiresRestart.length > 0 ? { requiresRestart } : {}
1396
1499
  };
1397
1500
  events.push(event);
@@ -1401,7 +1504,9 @@ var BoringPluginAssetManager = class {
1401
1504
  const message = error instanceof Error ? error.stack ?? error.message : String(error);
1402
1505
  this.writeError(plugin.id, message);
1403
1506
  const event = { type: "boring.plugin.error", id: plugin.id, revision, message };
1404
- errors.push({ id: plugin.id, revision, message });
1507
+ const loadError = { id: plugin.id, revision, message };
1508
+ this.lastErrors.set(plugin.id, loadError);
1509
+ errors.push(loadError);
1405
1510
  events.push(event);
1406
1511
  this.emit(event);
1407
1512
  }
@@ -1416,6 +1521,7 @@ var BoringPluginAssetManager = class {
1416
1521
 
1417
1522
  Plugin dir: ${error.pluginDir}`;
1418
1523
  const loadError = { id, revision, message };
1524
+ this.lastErrors.set(id, loadError);
1419
1525
  errors.push(loadError);
1420
1526
  this.writeError(id, message);
1421
1527
  const event = { type: "boring.plugin.error", id, revision, message };
@@ -1428,6 +1534,31 @@ Plugin dir: ${error.pluginDir}`;
1428
1534
  this.revisions.set(id, next);
1429
1535
  return next;
1430
1536
  }
1537
+ toListEntry(plugin) {
1538
+ return {
1539
+ id: plugin.id,
1540
+ boring: plugin.boring,
1541
+ ...plugin.pi ? { pi: plugin.pi } : {},
1542
+ version: plugin.version,
1543
+ revision: plugin.revision,
1544
+ ...this.frontUrlPayload(plugin.frontUrl),
1545
+ ...plugin.frontTarget ? { frontTarget: plugin.frontTarget } : {}
1546
+ };
1547
+ }
1548
+ frontUrlPayload(frontUrl) {
1549
+ if (!this.includeLegacyFrontUrl || !frontUrl) return {};
1550
+ return { frontUrl };
1551
+ }
1552
+ resolveFrontTarget(plugin, revision) {
1553
+ if (!plugin.frontPath || !this.frontTargetResolver) return void 0;
1554
+ const frontEntrySubpath = typeof plugin.boring.front === "string" ? plugin.boring.front.replace(/^\.\//, "") : normalizePluginSubpath(plugin.rootDir, plugin.frontPath);
1555
+ const frontTarget = this.frontTargetResolver(plugin, {
1556
+ revision,
1557
+ frontEntrySubpath
1558
+ });
1559
+ if (!frontTarget) return void 0;
1560
+ return { ...frontTarget, revision };
1561
+ }
1431
1562
  emit(event) {
1432
1563
  for (const listener of [...this.listeners]) {
1433
1564
  try {
@@ -1440,8 +1571,8 @@ Plugin dir: ${error.pluginDir}`;
1440
1571
  }
1441
1572
  errorPath(pluginId) {
1442
1573
  if (!isValidBoringPluginId(pluginId)) return null;
1443
- const root = resolve4(this.errorRoot);
1444
- const path = resolve4(root, pluginId, ".error");
1574
+ const root = resolve5(this.errorRoot);
1575
+ const path = resolve5(root, pluginId, ".error");
1445
1576
  const rel = relative3(root, path);
1446
1577
  if (rel.startsWith("..") || isAbsolute3(rel)) return null;
1447
1578
  return path;
@@ -1516,27 +1647,44 @@ async function boringPluginRoutes(app, opts) {
1516
1647
  res.setHeader("Connection", "keep-alive");
1517
1648
  res.setHeader("X-Accel-Buffering", "no");
1518
1649
  res.flushHeaders?.();
1519
- const write = (event) => {
1650
+ const write = (eventName, payload) => {
1520
1651
  try {
1521
- res.write(`event: ${event.type}
1652
+ res.write(`event: ${eventName}
1522
1653
  `);
1523
- res.write(`data: ${JSON.stringify(event)}
1654
+ res.write(`data: ${JSON.stringify(payload)}
1524
1655
 
1525
1656
  `);
1526
1657
  } catch {
1527
1658
  }
1528
1659
  };
1660
+ const liveQueue = [];
1661
+ let replaying = true;
1662
+ const unsubscribe = manager.subscribe((event) => {
1663
+ const payload = { ...event, replay: false };
1664
+ if (replaying) {
1665
+ liveQueue.push({ eventName: event.type, payload });
1666
+ return;
1667
+ }
1668
+ write(event.type, payload);
1669
+ });
1529
1670
  for (const plugin of manager.list()) {
1530
- write({
1671
+ write("boring.plugin.load", {
1531
1672
  type: "boring.plugin.load",
1532
1673
  id: plugin.id,
1533
1674
  boring: plugin.boring,
1534
1675
  version: plugin.version,
1535
1676
  revision: plugin.revision,
1536
- ...plugin.frontUrl ? { frontUrl: plugin.frontUrl } : {}
1677
+ ...plugin.frontUrl ? { frontUrl: plugin.frontUrl } : {},
1678
+ ...plugin.frontTarget ? { frontTarget: plugin.frontTarget } : {},
1679
+ replay: true
1537
1680
  });
1538
1681
  }
1539
- const unsubscribe = manager.subscribe(write);
1682
+ write("boring.plugin.replay-complete", {
1683
+ type: "boring.plugin.replay-complete",
1684
+ replay: true
1685
+ });
1686
+ replaying = false;
1687
+ for (const event of liveQueue) write(event.eventName, event.payload);
1540
1688
  const heartbeat = setInterval(() => {
1541
1689
  try {
1542
1690
  res.write(": heartbeat\n\n");
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-CT18yqwN.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-COYagY2m.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-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';
3
3
  import 'react';
4
4
  import 'dockview-react';
5
5
 
@@ -46,6 +46,7 @@ interface PanelConfig<T = any> {
46
46
  requiresCapabilities?: string[];
47
47
  essential?: boolean;
48
48
  chromeless?: boolean;
49
+ supportsFullPage?: boolean;
49
50
  /** Source: "builtin" | "app" */
50
51
  source?: string;
51
52
  pluginId?: string;
package/dist/testing.d.ts CHANGED
@@ -230,6 +230,7 @@ declare interface PanelConfig<T = any> {
230
230
  requiresCapabilities?: string[];
231
231
  essential?: boolean;
232
232
  chromeless?: boolean;
233
+ supportsFullPage?: boolean;
233
234
  /** Source: "builtin" | "app" */
234
235
  source?: string;
235
236
  pluginId?: string;