@hachej/boring-workspace 0.1.31 → 0.1.32

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.
@@ -1,10 +1,10 @@
1
1
  import { PiPackageSource, PiExtensionFactory, CreateAgentAppOptions, ProvisionWorkspaceRuntimeOptions } from '@hachej/boring-agent/server';
2
2
  export { PiPackageSource as WorkspacePiPackageSource } from '@hachej/boring-agent/server';
3
3
  import { FastifyInstance } from 'fastify';
4
- import { W as WorkspaceServerPlugin, S as ServerBootstrapOptions, B as BoringPluginFrontTargetResolver, a as WorkspaceProvisioningContribution, b as WorkspaceRouteContribution, c as createInMemoryBridge } from './createInMemoryBridge--ZFPAgXy.js';
5
- export { d as ServerWorkspaceRuntimeProvisioningInput } from './createInMemoryBridge--ZFPAgXy.js';
4
+ import { W as WorkspaceServerPlugin, S as ServerBootstrapOptions, B as BoringPluginFrontTargetResolver, a as WorkspaceProvisioningContribution, b as WorkspaceRouteContribution, c as createInMemoryBridge } from './createInMemoryBridge-HJopAIbo.js';
5
+ export { d as ServerWorkspaceRuntimeProvisioningInput } from './createInMemoryBridge-HJopAIbo.js';
6
6
  import './manifest-C2vVgH_e.js';
7
- import './ui-bridge-Bdgl2hR8.js';
7
+ import './ui-bridge-DFNem0df.js';
8
8
 
9
9
  /**
10
10
  * Directory-source entry: `{ dir, options?, hotReload? }`. Resolved via
@@ -133,11 +133,26 @@ interface CreateWorkspaceAgentServerOptions extends WorkspaceAgentCreateOptions,
133
133
  appPackageJsonPath?: string;
134
134
  /** Additional plugin collection roots to scan alongside workspace .pi/extensions and package/plugin-derived roots. */
135
135
  additionalBoringPluginDirs?: string[];
136
+ /**
137
+ * Install and advertise the boring plugin-authoring runtime.
138
+ *
139
+ * Keep this off for production/hosted workspaces unless a plugin-editing
140
+ * experience is explicitly enabled. Remote sandboxes can support authoring,
141
+ * but the CLI should be provisioned only for that activated editing mode,
142
+ * not for every normal workspace boot.
143
+ *
144
+ * Defaults to true for local/standalone strong-filesystem runtimes and false
145
+ * for remote/best-effort runtimes. Core/full-app may choose a stricter
146
+ * default at its composition boundary.
147
+ */
148
+ installPluginAuthoring?: boolean;
136
149
  /** Optional host-owned front-target override for boring plugin list/event payloads. */
137
150
  boringPluginFrontTargetResolver?: BoringPluginFrontTargetResolver;
138
151
  /** Preserve legacy `/@fs/...` frontUrl payloads alongside frontTarget. Defaults to true. */
139
152
  boringPluginIncludeLegacyFrontUrl?: boolean;
140
153
  }
154
+ declare const PLUGIN_AUTHORING_PROVISIONING_IDS: Set<string>;
155
+ declare function omitPluginAuthoringProvisioning(plugins: WorkspaceRuntimeProvisioningInput[]): WorkspaceRuntimeProvisioningInput[];
141
156
  interface WorkspaceAgentServerPluginCollection {
142
157
  provisioningContributions: WorkspaceProvisioningContribution[];
143
158
  runtimePlugins: WorkspaceRuntimeProvisioningInput[];
@@ -149,6 +164,8 @@ interface CollectWorkspaceAgentServerPluginsOptions extends Pick<ServerBootstrap
149
164
  workspaceRoot?: string;
150
165
  systemPromptAppend?: string;
151
166
  pi?: WorkspaceAgentPiOptions;
167
+ /** Whether to include built-in boring plugin-authoring provisioning/prompt resources. */
168
+ installPluginAuthoring?: boolean;
152
169
  }
153
170
  declare function buildWorkspaceContextPrompt(): string;
154
171
  declare function collectWorkspaceAgentServerPlugins(opts?: CollectWorkspaceAgentServerPluginsOptions): WorkspaceAgentServerPluginCollection;
@@ -181,4 +198,4 @@ interface ResolveDefaultWorkspacePluginPackagePathsOptions {
181
198
  */
182
199
  declare function resolveDefaultWorkspacePluginPackagePaths({ workspaceRoot, defaultPluginPackages, appPackageJsonPath, }?: ResolveDefaultWorkspacePluginPackagePathsOptions): string[];
183
200
 
184
- export { type CollectWorkspaceAgentServerPluginsOptions, type CreateWorkspaceAgentServerOptions, type DirPluginEntry, type PluginResolveContext, type ResolveDefaultWorkspacePluginPackagePathsOptions, type WorkspaceAgentPiOptions, type WorkspaceAgentServerPluginCollection, type WorkspaceAgentServerPluginContext, type WorkspacePluginEntry, type WorkspacePluginPackagePiSnapshot, WorkspaceProvisioningContribution, WorkspaceRouteContribution, type WorkspaceRuntimeProvisioningInput, buildWorkspaceContextPrompt, collectWorkspaceAgentServerPlugins, createWorkspaceAgentServer, hasDirServerPlugin, provisionWorkspaceAgentServer, readWorkspacePluginPackagePiSnapshot, readWorkspacePluginPackageRuntimePlugins, resolveDefaultWorkspacePluginPackagePaths, resolveOnePluginEntry };
201
+ export { type CollectWorkspaceAgentServerPluginsOptions, type CreateWorkspaceAgentServerOptions, type DirPluginEntry, PLUGIN_AUTHORING_PROVISIONING_IDS, type PluginResolveContext, type ResolveDefaultWorkspacePluginPackagePathsOptions, type WorkspaceAgentPiOptions, type WorkspaceAgentServerPluginCollection, type WorkspaceAgentServerPluginContext, type WorkspacePluginEntry, type WorkspacePluginPackagePiSnapshot, WorkspaceProvisioningContribution, WorkspaceRouteContribution, type WorkspaceRuntimeProvisioningInput, buildWorkspaceContextPrompt, collectWorkspaceAgentServerPlugins, createWorkspaceAgentServer, hasDirServerPlugin, omitPluginAuthoringProvisioning, provisionWorkspaceAgentServer, readWorkspacePluginPackagePiSnapshot, readWorkspacePluginPackageRuntimePlugins, resolveDefaultWorkspacePluginPackagePaths, resolveOnePluginEntry };
@@ -54,7 +54,7 @@ function buildBoringSystemPrompt(opts) {
54
54
  if (opts.scaffoldCommand) {
55
55
  n += 1;
56
56
  steps.push(
57
- `**${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.`
57
+ `**${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.`
58
58
  );
59
59
  } else {
60
60
  n += 1;
@@ -64,7 +64,11 @@ function buildBoringSystemPrompt(opts) {
64
64
  }
65
65
  n += 1;
66
66
  steps.push(
67
- 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.`
67
+ 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.`
68
+ );
69
+ n += 1;
70
+ steps.push(
71
+ `**${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.`
68
72
  );
69
73
  n += 1;
70
74
  if (verify) {
@@ -88,7 +92,7 @@ function buildBoringSystemPrompt(opts) {
88
92
  "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."
89
93
  ].join("\n");
90
94
  return [
91
- "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.",
95
+ "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.",
92
96
  [
93
97
  "## Plugin authoring \u2014 required workflow",
94
98
  "",
@@ -103,6 +107,7 @@ function buildBoringSystemPrompt(opts) {
103
107
  '- Server/Pi tool method: `handler` \u2014 use `execute`. Return shape: `{ content: [{ type: "text", text }] }` (NEVER a bare string).',
104
108
  "- 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.",
105
109
  "- 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`.",
110
+ "- 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.",
106
111
  "- 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."
107
112
  ].join("\n"),
108
113
  docsBlock
@@ -662,6 +667,23 @@ function validateSkills(pluginId, skills) {
662
667
  }
663
668
  }
664
669
  }
670
+ function validatePluginAssets(pluginId, assets) {
671
+ for (let i = 0; i < assets.length; i++) {
672
+ const asset = assets[i];
673
+ if (!asset || typeof asset !== "object") {
674
+ fail(pluginId, `assets[${i}] must be an object`);
675
+ }
676
+ if (!asset.name || typeof asset.name !== "string") {
677
+ fail(pluginId, `assets[${i}].name must be a non-empty string`);
678
+ }
679
+ if (!isPathLike(asset.source)) {
680
+ fail(pluginId, `assets[${i}].source must be a string or URL`);
681
+ }
682
+ if (asset.target !== void 0 && (!asset.target || typeof asset.target !== "string")) {
683
+ fail(pluginId, `assets[${i}].target must be a non-empty string when provided`);
684
+ }
685
+ }
686
+ }
665
687
  function validateProvisioning(pluginId, provisioning) {
666
688
  if (!provisioning || typeof provisioning !== "object") {
667
689
  fail(pluginId, "provisioning must be an object");
@@ -775,6 +797,12 @@ function validateServerPlugin(plugin) {
775
797
  }
776
798
  plugin.agentTools.forEach((tool, index) => validateAgentTool(plugin.id, tool, index));
777
799
  }
800
+ if (plugin.assets !== void 0) {
801
+ if (!Array.isArray(plugin.assets)) {
802
+ fail(plugin.id, "assets must be an array when provided");
803
+ }
804
+ validatePluginAssets(plugin.id, plugin.assets);
805
+ }
778
806
  if (plugin.routes !== void 0 && typeof plugin.routes !== "function") {
779
807
  fail(plugin.id, "routes must be a Fastify plugin function when provided");
780
808
  }
@@ -1465,10 +1493,11 @@ function createInMemoryBridge() {
1465
1493
  async postCommand(cmd) {
1466
1494
  const seq = nextSeq++;
1467
1495
  const annotated = { ...cmd, seq };
1468
- if (subscribers.size === 0) enqueuePending(annotated);
1496
+ let delivered = false;
1469
1497
  for (const handler of subscribers) {
1470
- handler(annotated);
1498
+ if (handler(annotated) !== false) delivered = true;
1471
1499
  }
1500
+ if (!delivered) enqueuePending(annotated);
1472
1501
  return { seq, status: "ok" };
1473
1502
  },
1474
1503
  subscribeCommands(handler) {
@@ -1602,9 +1631,9 @@ function isVerified(kind, params, state) {
1602
1631
  }
1603
1632
  function createExecUiTool(uiBridge, opts = {}) {
1604
1633
  const { workspaceRoot, resolvePathKind } = opts;
1605
- const verifyDelayMs = opts.verifyDelayMs ?? 200;
1606
- const verifyRetries = opts.verifyRetries ?? 2;
1607
- const verifyIntervalMs = opts.verifyIntervalMs ?? 200;
1634
+ const verifyDelayMs = opts.verifyDelayMs ?? 250;
1635
+ const verifyRetries = opts.verifyRetries ?? 20;
1636
+ const verifyIntervalMs = opts.verifyIntervalMs ?? 250;
1608
1637
  return {
1609
1638
  name: "exec_ui",
1610
1639
  readinessRequirements: ["ui-bridge"],
@@ -1676,11 +1705,12 @@ function createExecUiTool(uiBridge, opts = {}) {
1676
1705
  " showNotification params: { msg: string, level?: 'info'|'warn'|'error' }",
1677
1706
  "",
1678
1707
  "Returns { seq, status, uiState? }. For openFile / openPanel / openSurface /",
1679
- "closePanel the response includes a `uiState` snapshot taken ~400ms after",
1680
- "dispatch \u2014 check uiState.openTabs to confirm the action took effect.",
1708
+ "closePanel the response includes a `uiState` snapshot after waiting up",
1709
+ "to a few seconds for the browser to dispatch the command \u2014 check",
1710
+ "uiState.openTabs to confirm the action took effect.",
1681
1711
  "If the expected tab is missing from openTabs the frontend silently",
1682
1712
  "rejected the command (unknown panel component, unregistered surface",
1683
- "resolver, or surface not yet ready). For other kinds only { seq, status }",
1713
+ "resolver, or disconnected browser). For other kinds only { seq, status }",
1684
1714
  "is returned. To open a FILE prefer openFile (path-aware) over openPanel",
1685
1715
  "(which is for non-file panes like charts)."
1686
1716
  ].join("\n"),
@@ -1768,7 +1798,7 @@ function createExecUiTool(uiBridge, opts = {}) {
1768
1798
  await new Promise((r) => setTimeout(r, verifyDelayMs));
1769
1799
  let uiState = await uiBridge.getState();
1770
1800
  for (let i = 0; i < verifyRetries; i++) {
1771
- if (isVerified(effectiveKind, cmdParams, uiState)) break;
1801
+ if (uiState === null || isVerified(effectiveKind, cmdParams, uiState)) break;
1772
1802
  await new Promise((r) => setTimeout(r, verifyIntervalMs));
1773
1803
  uiState = await uiBridge.getState();
1774
1804
  }
@@ -1794,18 +1824,170 @@ function createWorkspaceUiTools(uiBridge, opts = {}) {
1794
1824
  }
1795
1825
 
1796
1826
  // src/server/ui-control/http/uiRoutes.ts
1827
+ import { z as z2 } from "zod";
1828
+
1829
+ // src/server/ui-control/panelStatus/paneRenderStatusStore.ts
1830
+ var DEFAULT_STATUS_TTL_MS = 5 * 6e4;
1831
+ var DEFAULT_UI_CONTACT_TTL_MS = 3e4;
1832
+ var DEFAULT_WORKSPACE_ID = "default";
1833
+ var MAX_ERROR_MESSAGE_LENGTH = 500;
1834
+ function normalizeWorkspaceId(workspaceId) {
1835
+ const trimmed = workspaceId?.trim();
1836
+ return trimmed || DEFAULT_WORKSPACE_ID;
1837
+ }
1838
+ function statusKey(input) {
1839
+ return `${input.workspaceId}\0${input.pluginId}\0${input.panelId}\0${input.panelInstanceId}`;
1840
+ }
1841
+ function redactMessage(message) {
1842
+ return message.replace(/\s+/g, " ").trim().slice(0, MAX_ERROR_MESSAGE_LENGTH);
1843
+ }
1844
+ function createPaneRenderStatusStore(options = {}) {
1845
+ const ttlMs = options.ttlMs ?? DEFAULT_STATUS_TTL_MS;
1846
+ const uiContactTtlMs = options.uiContactTtlMs ?? DEFAULT_UI_CONTACT_TTL_MS;
1847
+ const now = options.now ?? (() => Date.now());
1848
+ const statuses = /* @__PURE__ */ new Map();
1849
+ const lastUiContactByWorkspace = /* @__PURE__ */ new Map();
1850
+ function pruneExpired(current = now()) {
1851
+ for (const [key, status] of statuses) {
1852
+ const reportedAtMs = Date.parse(status.reportedAt);
1853
+ if (!Number.isFinite(reportedAtMs) || current - reportedAtMs > ttlMs) {
1854
+ statuses.delete(key);
1855
+ }
1856
+ }
1857
+ }
1858
+ function touchUi(workspaceId) {
1859
+ lastUiContactByWorkspace.set(normalizeWorkspaceId(workspaceId), now());
1860
+ }
1861
+ function hasRecentUiContact(workspaceId) {
1862
+ const last = lastUiContactByWorkspace.get(normalizeWorkspaceId(workspaceId));
1863
+ return last !== void 0 && now() - last <= uiContactTtlMs;
1864
+ }
1865
+ return {
1866
+ touchUi,
1867
+ hasRecentUiContact,
1868
+ report(input) {
1869
+ pruneExpired();
1870
+ const workspaceId = normalizeWorkspaceId(input.workspaceId);
1871
+ touchUi(workspaceId);
1872
+ const snapshot = {
1873
+ workspaceId,
1874
+ pluginId: input.pluginId,
1875
+ panelId: input.panelId,
1876
+ panelInstanceId: input.panelInstanceId,
1877
+ state: input.state,
1878
+ reportedAt: new Date(now()).toISOString(),
1879
+ ...input.revision !== void 0 ? { revision: input.revision } : {},
1880
+ ...input.error ? { error: { code: input.error.code, message: redactMessage(input.error.message) } } : {}
1881
+ };
1882
+ statuses.set(statusKey(snapshot), snapshot);
1883
+ return snapshot;
1884
+ },
1885
+ get(input) {
1886
+ pruneExpired();
1887
+ const workspaceId = normalizeWorkspaceId(input.workspaceId);
1888
+ if (input.pluginId && input.panelId) {
1889
+ return statuses.get(statusKey({ workspaceId, pluginId: input.pluginId, panelId: input.panelId, panelInstanceId: input.panelInstanceId }));
1890
+ }
1891
+ for (const status of statuses.values()) {
1892
+ if (status.workspaceId !== workspaceId) continue;
1893
+ if (status.panelInstanceId !== input.panelInstanceId) continue;
1894
+ if (input.pluginId && status.pluginId !== input.pluginId) continue;
1895
+ if (input.panelId && status.panelId !== input.panelId) continue;
1896
+ return status;
1897
+ }
1898
+ return void 0;
1899
+ }
1900
+ };
1901
+ }
1902
+
1903
+ // src/server/ui-control/http/paneRenderStatusRoutes.ts
1797
1904
  import { z } from "zod";
1905
+ var reportBodySchema = z.object({
1906
+ workspaceId: z.string().optional(),
1907
+ pluginId: z.string().min(1),
1908
+ panelId: z.string().min(1),
1909
+ panelInstanceId: z.string().min(1),
1910
+ revision: z.number().optional(),
1911
+ state: z.enum(["loading", "ready", "error", "missing"]),
1912
+ error: z.object({
1913
+ code: z.string().min(1),
1914
+ message: z.string().min(1)
1915
+ }).optional()
1916
+ });
1917
+ function createBodyValidator(schema) {
1918
+ return async function validateBody(request, reply) {
1919
+ const parsed = schema.safeParse(request.body);
1920
+ if (!parsed.success) {
1921
+ const firstIssue = parsed.error.issues[0];
1922
+ const fieldName = firstIssue?.path?.map((segment) => String(segment)).join(".");
1923
+ reply.code(400).send({
1924
+ error: "validation_error",
1925
+ message: firstIssue?.message ?? "Invalid request body",
1926
+ field: fieldName || void 0
1927
+ });
1928
+ return;
1929
+ }
1930
+ request.body = parsed.data;
1931
+ };
1932
+ }
1933
+ function resolvePaneStatusWorkspaceId(request) {
1934
+ const headers = request.headers;
1935
+ const header = headers["x-boring-workspace-id"] ?? headers["X-Boring-Workspace-Id"];
1936
+ if (Array.isArray(header)) return header[0];
1937
+ if (typeof header === "string" && header.trim()) return header;
1938
+ const query = request.query;
1939
+ const workspaceId = query?.workspaceId;
1940
+ return typeof workspaceId === "string" && workspaceId.trim() ? workspaceId : void 0;
1941
+ }
1942
+ function paneRenderStatusRoutes(app, opts = {}, done) {
1943
+ const store = opts.store ?? createPaneRenderStatusStore();
1944
+ const validateReport = createBodyValidator(reportBodySchema);
1945
+ const getWorkspaceId = async (request) => {
1946
+ return await opts.getWorkspaceId?.(request) ?? resolvePaneStatusWorkspaceId(request);
1947
+ };
1948
+ app.put(
1949
+ "/api/v1/ui/panels/status",
1950
+ { preHandler: validateReport },
1951
+ async (request, reply) => {
1952
+ const body = request.body;
1953
+ const workspaceId = await getWorkspaceId(request) ?? body.workspaceId;
1954
+ const status = store.report({ ...body, workspaceId });
1955
+ return reply.code(200).send({ ok: true, status });
1956
+ }
1957
+ );
1958
+ app.get("/api/v1/ui/panels/status", async (request, reply) => {
1959
+ const query = request.query;
1960
+ const panelInstanceId = query.panelInstanceId;
1961
+ if (typeof panelInstanceId !== "string" || !panelInstanceId.trim()) {
1962
+ return reply.code(400).send({ error: "validation_error", message: "panelInstanceId is required", field: "panelInstanceId" });
1963
+ }
1964
+ const workspaceId = await getWorkspaceId(request);
1965
+ const pluginId = typeof query.pluginId === "string" ? query.pluginId : void 0;
1966
+ const panelId = typeof query.panelId === "string" ? query.panelId : void 0;
1967
+ const status = store.get({ workspaceId, panelInstanceId, pluginId, panelId });
1968
+ const connected = store.hasRecentUiContact(workspaceId);
1969
+ return {
1970
+ ok: true,
1971
+ connected,
1972
+ state: status?.state ?? (connected ? "missing" : "no-browser-connected"),
1973
+ ...status ? { status } : {}
1974
+ };
1975
+ });
1976
+ done();
1977
+ }
1978
+
1979
+ // src/server/ui-control/http/uiRoutes.ts
1798
1980
  var UI_BRIDGE_PROTOCOL_VERSION = 1;
1799
1981
  var HEARTBEAT_MS = 15e3;
1800
- var setStateBodySchema = z.object({
1801
- state: z.record(z.unknown()),
1802
- causedBy: z.enum(["user", "agent", "restore"]).optional()
1982
+ var setStateBodySchema = z2.object({
1983
+ state: z2.record(z2.unknown()),
1984
+ causedBy: z2.enum(["user", "agent", "restore"]).optional()
1803
1985
  });
1804
- var postCommandBodySchema = z.object({
1805
- kind: z.string().min(1),
1806
- params: z.record(z.unknown()).default({})
1986
+ var postCommandBodySchema = z2.object({
1987
+ kind: z2.string().min(1),
1988
+ params: z2.record(z2.unknown()).default({})
1807
1989
  });
1808
- function createBodyValidator(schema) {
1990
+ function createBodyValidator2(schema) {
1809
1991
  return async function validateBody(request, reply) {
1810
1992
  const parsed = schema.safeParse(request.body);
1811
1993
  if (!parsed.success) {
@@ -1823,8 +2005,13 @@ function createBodyValidator(schema) {
1823
2005
  }
1824
2006
  function uiRoutes(app, opts, done) {
1825
2007
  const fallbackBridge = opts.bridge;
1826
- const validateSetState = createBodyValidator(setStateBodySchema);
1827
- const validatePostCommand = createBodyValidator(postCommandBodySchema);
2008
+ const paneStatusStore = opts.paneStatusStore ?? createPaneRenderStatusStore();
2009
+ const getPaneWorkspaceId = async (request) => await opts.getWorkspaceId?.(request) ?? resolvePaneStatusWorkspaceId(request);
2010
+ const touchUi = async (request) => {
2011
+ paneStatusStore.touchUi(await getPaneWorkspaceId(request));
2012
+ };
2013
+ const validateSetState = createBodyValidator2(setStateBodySchema);
2014
+ const validatePostCommand = createBodyValidator2(postCommandBodySchema);
1828
2015
  const resolveBridge = async (request) => {
1829
2016
  if (opts.getBridge) return await opts.getBridge(request);
1830
2017
  if (fallbackBridge) return fallbackBridge;
@@ -1836,7 +2023,10 @@ function uiRoutes(app, opts, done) {
1836
2023
  kind: cmd.kind,
1837
2024
  params: cmd.params
1838
2025
  });
2026
+ paneRenderStatusRoutes(app, { store: paneStatusStore, getWorkspaceId: getPaneWorkspaceId }, () => {
2027
+ });
1839
2028
  app.get("/api/v1/ui/state", async (request) => {
2029
+ await touchUi(request);
1840
2030
  const bridge = await resolveBridge(request);
1841
2031
  return await bridge.getState() ?? {};
1842
2032
  });
@@ -1844,6 +2034,7 @@ function uiRoutes(app, opts, done) {
1844
2034
  "/api/v1/ui/state",
1845
2035
  { preHandler: validateSetState },
1846
2036
  async (request, reply) => {
2037
+ await touchUi(request);
1847
2038
  const body = request.body;
1848
2039
  const bridge = await resolveBridge(request);
1849
2040
  const current = await bridge.getState() ?? {};
@@ -1865,6 +2056,7 @@ function uiRoutes(app, opts, done) {
1865
2056
  }
1866
2057
  );
1867
2058
  app.get("/api/v1/ui/commands/next", async (request, reply) => {
2059
+ await touchUi(request);
1868
2060
  const bridge = await resolveBridge(request);
1869
2061
  const query = request.query;
1870
2062
  if (query.poll === "true") {
@@ -1894,13 +2086,20 @@ data: ${JSON.stringify(encodeCommand(cmd))}
1894
2086
  }
1895
2087
  }
1896
2088
  const unsub = bridge.subscribeCommands((cmd) => {
1897
- reply.raw.write(`event: command
2089
+ if (reply.raw.destroyed || reply.raw.writableEnded) return false;
2090
+ try {
2091
+ reply.raw.write(`event: command
1898
2092
  data: ${JSON.stringify(encodeCommand(cmd))}
1899
2093
 
1900
2094
  `);
2095
+ return true;
2096
+ } catch {
2097
+ return false;
2098
+ }
1901
2099
  });
1902
2100
  const heartbeat = setInterval(() => {
1903
2101
  if (reply.raw.writableEnded) return;
2102
+ void touchUi(request);
1904
2103
  reply.raw.write(
1905
2104
  `event: heartbeat
1906
2105
  data: ${JSON.stringify({ v: UI_BRIDGE_PROTOCOL_VERSION })}
@@ -1949,41 +2148,9 @@ function readPackageVersion(packageRoot) {
1949
2148
  return void 0;
1950
2149
  }
1951
2150
  }
1952
- function nodePackageContribution(contributionId, nodePackageId, packageName, packageRoot) {
1953
- if (!packageRoot || !existsSync7(join7(packageRoot, "package.json"))) return null;
1954
- return {
1955
- id: contributionId,
1956
- provisioning: {
1957
- nodePackages: [{ id: nodePackageId, packageName, packageRoot }]
1958
- }
1959
- };
1960
- }
1961
- function publishedNodePackageContribution(contributionId, nodePackageId, packageName, version, expectedBins) {
1962
- return {
1963
- id: contributionId,
1964
- provisioning: {
1965
- nodePackages: [
1966
- {
1967
- id: nodePackageId,
1968
- packageName,
1969
- ...version ? { version } : {},
1970
- ...expectedBins ? { expectedBins } : {}
1971
- }
1972
- ]
1973
- }
1974
- };
1975
- }
1976
2151
  function useLocalPackageProvisioning() {
1977
2152
  return process.env.BORING_USE_LOCAL_PACKAGES === "1";
1978
2153
  }
1979
- function createWorkspacePackageProvisioningContribution() {
1980
- return nodePackageContribution(
1981
- "boring-workspace-package",
1982
- "boring-workspace",
1983
- "@hachej/boring-workspace",
1984
- resolveWorkspacePackageRoot()
1985
- );
1986
- }
1987
2154
  function resolveBoringPiPackageRoot() {
1988
2155
  const workspacePackageRoot = resolveWorkspacePackageRoot();
1989
2156
  const candidates = [
@@ -2003,45 +2170,48 @@ function resolveBoringPiPackageRoot() {
2003
2170
  return null;
2004
2171
  }
2005
2172
  }
2006
- function createBoringPiPackageProvisioningContribution() {
2007
- return nodePackageContribution("boring-pi-package", "boring-pi", "@hachej/boring-pi", resolveBoringPiPackageRoot());
2173
+ function isUsableBoringUiPluginCliPackageRoot(candidate) {
2174
+ try {
2175
+ const pkg = JSON.parse(readFileSync6(join7(candidate, "package.json"), "utf8"));
2176
+ return pkg.name === "@hachej/boring-ui-plugin-cli" && existsSync7(join7(candidate, "dist", "bin.js"));
2177
+ } catch {
2178
+ return false;
2179
+ }
2008
2180
  }
2009
- function resolveBoringUiCliPackageRoot() {
2181
+ function resolveBoringUiPluginCliPackageRoot() {
2010
2182
  const workspacePackageRoot = resolveWorkspacePackageRoot();
2011
2183
  const candidates = [
2012
- join7(workspacePackageRoot, "..", "cli"),
2013
- join7(workspacePackageRoot, "node_modules", "@hachej", "boring-ui-cli")
2184
+ join7(workspacePackageRoot, "..", "plugin-cli"),
2185
+ join7(workspacePackageRoot, "node_modules", "@hachej", "boring-ui-plugin-cli")
2014
2186
  ];
2015
2187
  for (const candidate of candidates) {
2016
- try {
2017
- const pkg = JSON.parse(readFileSync6(join7(candidate, "package.json"), "utf8"));
2018
- if (pkg.name === "@hachej/boring-ui-cli") return candidate;
2019
- } catch {
2020
- }
2188
+ if (isUsableBoringUiPluginCliPackageRoot(candidate)) return candidate;
2021
2189
  }
2022
2190
  try {
2023
- return dirname7(require4.resolve("@hachej/boring-ui-cli/package.json"));
2191
+ const resolved = dirname7(require4.resolve("@hachej/boring-ui-plugin-cli/package.json"));
2192
+ return isUsableBoringUiPluginCliPackageRoot(resolved) ? resolved : null;
2024
2193
  } catch {
2025
2194
  return null;
2026
2195
  }
2027
2196
  }
2028
- function createBoringUiCliPackageProvisioningContribution() {
2029
- const packageRoot = resolveBoringUiCliPackageRoot();
2030
- if (useLocalPackageProvisioning()) {
2031
- return nodePackageContribution(
2032
- "boring-ui-cli-package",
2033
- "boring-ui-cli",
2034
- "@hachej/boring-ui-cli",
2035
- packageRoot
2036
- );
2037
- }
2038
- return publishedNodePackageContribution(
2039
- "boring-ui-cli-package",
2040
- "boring-ui-cli",
2041
- "@hachej/boring-ui-cli",
2042
- packageRoot === join7(resolveWorkspacePackageRoot(), "..", "cli") ? void 0 : readPackageVersion(packageRoot) ?? readPackageVersion(resolveWorkspacePackageRoot()),
2043
- ["boring-ui"]
2044
- );
2197
+ var PLUGIN_AUTHORING_PROVISIONING_IDS = /* @__PURE__ */ new Set(["boring-ui-plugin-cli-package"]);
2198
+ function omitPluginAuthoringProvisioning(plugins) {
2199
+ return plugins.filter((plugin) => !PLUGIN_AUTHORING_PROVISIONING_IDS.has(plugin.id));
2200
+ }
2201
+ function createBoringUiPluginCliPackageProvisioningContribution() {
2202
+ const packageRoot = useLocalPackageProvisioning() ? resolveBoringUiPluginCliPackageRoot() : null;
2203
+ const version = readPackageVersion(resolveWorkspacePackageRoot());
2204
+ return {
2205
+ id: "boring-ui-plugin-cli-package",
2206
+ provisioning: {
2207
+ nodePackages: [{
2208
+ id: "boring-ui-plugin-cli",
2209
+ packageName: "@hachej/boring-ui-plugin-cli",
2210
+ ...packageRoot ? { packageRoot } : { version },
2211
+ expectedBins: ["boring-ui-plugin"]
2212
+ }]
2213
+ }
2214
+ };
2045
2215
  }
2046
2216
  function createBoringPiPackageSource(workspaceRoot) {
2047
2217
  const workspacePackageRoot = join7(workspaceRoot, "node_modules", "@hachej", "boring-pi");
@@ -2062,7 +2232,7 @@ function buildWorkspaceContextPrompt() {
2062
2232
  "- Root: `$BORING_AGENT_WORKSPACE_ROOT` (exported into every bash invocation)",
2063
2233
  "- Generated plugin skills: `$BORING_AGENT_WORKSPACE_ROOT/.boring-agent/skills/` \u2014 readable with normal file tools",
2064
2234
  "- User workspace skills: `$BORING_AGENT_WORKSPACE_ROOT/.agents/skills/`",
2065
- "- Runtime CLIs (`boring-ui`, `bm`, `python`, `pip`, `uv`) come from `.boring-agent/node`, `.boring-agent/venv`, and `.boring-agent/sdk/uv` and are already on PATH"
2235
+ "- Runtime CLIs (`boring-ui-plugin`, `bm`, `python`, `pip`, `uv`) come from `.boring-agent/node`, `.boring-agent/venv`, and `.boring-agent/sdk/uv` and are already on PATH"
2066
2236
  ].join("\n");
2067
2237
  }
2068
2238
  function collectWorkspaceAgentServerPlugins(opts = {}) {
@@ -2076,11 +2246,8 @@ function collectWorkspaceAgentServerPlugins(opts = {}) {
2076
2246
  const callerAdditional = opts.pi?.additionalSkillPaths ?? [];
2077
2247
  const callerPiPackages = opts.pi?.packages ?? [];
2078
2248
  const callerExtensionPaths = opts.pi?.extensionPaths ?? [];
2079
- const builtinProvisioningContributions = [
2080
- createWorkspacePackageProvisioningContribution(),
2081
- createBoringPiPackageProvisioningContribution(),
2082
- createBoringUiCliPackageProvisioningContribution()
2083
- ].filter((entry) => Boolean(entry));
2249
+ const excludedDefaults = new Set(opts.excludeDefaults ?? []);
2250
+ const builtinProvisioningContributions = (opts.installPluginAuthoring === false ? [] : [createBoringUiPluginCliPackageProvisioningContribution()]).filter((entry) => Boolean(entry)).filter((entry) => !excludedDefaults.has(entry.id));
2084
2251
  return {
2085
2252
  provisioningContributions: [
2086
2253
  ...builtinProvisioningContributions,
@@ -2226,13 +2393,15 @@ async function createWorkspaceAgentServer(opts = {}) {
2226
2393
  const resolvedPlugins = await Promise.all(
2227
2394
  allPluginEntries.map((entry) => resolveOnePluginEntry(entry, ctx))
2228
2395
  );
2396
+ const pluginAuthoringEnabled = (opts.installPluginAuthoring ?? workspaceFsCapability === "strong") && !(opts.excludeDefaults ?? []).includes("boring-ui-plugin-cli-package");
2229
2397
  const pluginCollection = collectWorkspaceAgentServerPlugins({
2230
2398
  ...opts,
2231
- plugins: resolvedPlugins
2399
+ plugins: resolvedPlugins,
2400
+ installPluginAuthoring: pluginAuthoringEnabled
2232
2401
  });
2233
- const workspacePackagePiPackage = createBoringPiPackageSource(workspaceRoot);
2402
+ const workspacePackagePiPackage = pluginAuthoringEnabled ? createBoringPiPackageSource(workspaceRoot) : void 0;
2234
2403
  const baseStaticPiSkillPaths = [
2235
- ...resolveBoringPiSkillPaths(workspaceRoot),
2404
+ ...pluginAuthoringEnabled ? resolveBoringPiSkillPaths(workspaceRoot) : [],
2236
2405
  ...pluginCollection.agentOptions.pi?.additionalSkillPaths ?? []
2237
2406
  ];
2238
2407
  const baseStaticPiPackages = [
@@ -2264,10 +2433,14 @@ async function createWorkspaceAgentServer(opts = {}) {
2264
2433
  frontTargetResolver: opts.boringPluginFrontTargetResolver,
2265
2434
  includeLegacyFrontUrl: opts.boringPluginIncludeLegacyFrontUrl
2266
2435
  });
2267
- const buildRuntimeProvisioningInputs = () => mergeRuntimeProvisioningInputs([
2268
- ...pluginCollection.runtimePlugins,
2269
- ...readWorkspacePluginPackageRuntimePlugins(boringPluginDirs)
2270
- ]);
2436
+ const buildRuntimeProvisioningInputs = () => {
2437
+ const inputs = mergeRuntimeProvisioningInputs([
2438
+ ...pluginCollection.runtimePlugins,
2439
+ ...readWorkspacePluginPackageRuntimePlugins(boringPluginDirs)
2440
+ ]);
2441
+ if (resolvedMode === "direct") return omitPluginAuthoringProvisioning(inputs);
2442
+ return inputs;
2443
+ };
2271
2444
  let currentRuntimeProvisioning = opts.runtimeProvisioning;
2272
2445
  const runtimeWorkspaceRoot = resolvedMode === "vercel-sandbox" ? VERCEL_SANDBOX_WORKSPACE_ROOT : workspaceRoot;
2273
2446
  const runtimeLayout = getBoringAgentRuntimePaths(runtimeWorkspaceRoot);
@@ -2307,20 +2480,18 @@ async function createWorkspaceAgentServer(opts = {}) {
2307
2480
  ],
2308
2481
  systemPromptAppend: [
2309
2482
  workspaceFsCapability === "strong" ? buildWorkspaceContextPrompt() : void 0,
2310
- // `boring-ui` resolves via PATH (pnpm's workspace bin or the
2311
- // user's npx/global install). We deliberately do NOT prefix with
2312
- // `npx @hachej/boring-ui-cli` here that would pull the
2313
- // published version, which lags the locally-installed CLI when
2314
- // the agent is iterating in a monorepo. Keep the bin name short.
2315
- buildBoringSystemPrompt({
2316
- scaffoldCommand: "boring-ui scaffold-plugin",
2317
- verifyCommand: "boring-ui verify-plugin",
2483
+ // `boring-ui-plugin` resolves via PATH from the provisioned workspace
2484
+ // runtime. It is the slim setup component for agent-authored plugins;
2485
+ // do not route plugin authoring through the full human-facing CLI.
2486
+ pluginAuthoringEnabled ? buildBoringSystemPrompt({
2487
+ scaffoldCommand: "boring-ui-plugin scaffold",
2488
+ verifyCommand: "boring-ui-plugin verify",
2318
2489
  boringPiRootOverride: boringPiRootVisibleToAgentTools(
2319
2490
  workspaceRoot,
2320
2491
  resolvedMode,
2321
2492
  opts.provisionWorkspace !== false
2322
2493
  )
2323
- }),
2494
+ }) : void 0,
2324
2495
  pluginCollection.agentOptions.systemPromptAppend,
2325
2496
  staticPluginPackagePiSnapshot.systemPromptAppend
2326
2497
  ].filter(Boolean).join("\n\n") || void 0,
@@ -2378,10 +2549,12 @@ async function createWorkspaceAgentServer(opts = {}) {
2378
2549
  return app;
2379
2550
  }
2380
2551
  export {
2552
+ PLUGIN_AUTHORING_PROVISIONING_IDS,
2381
2553
  buildWorkspaceContextPrompt,
2382
2554
  collectWorkspaceAgentServerPlugins,
2383
2555
  createWorkspaceAgentServer,
2384
2556
  hasDirServerPlugin,
2557
+ omitPluginAuthoringProvisioning,
2385
2558
  provisionWorkspaceAgentServer,
2386
2559
  readWorkspacePluginPackagePiSnapshot,
2387
2560
  readWorkspacePluginPackageRuntimePlugins,