@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.
- package/dist/FileTree-BltQETt9.js +289 -0
- package/dist/{MarkdownEditor-DPBSzTBz.js → MarkdownEditor-heUJdK4j.js} +1 -1
- package/dist/WorkspaceLoadingState-InXsc_8G.js +719 -0
- package/dist/{WorkspaceProvider-0V-2x7AH.js → WorkspaceProvider-Cg-J1wxr.js} +2750 -2570
- package/dist/app-front.d.ts +22 -2
- package/dist/app-front.js +672 -524
- package/dist/app-server.d.ts +21 -4
- package/dist/app-server.js +277 -104
- package/dist/boring-workspace.css +1 -1
- package/dist/{createInMemoryBridge--ZFPAgXy.d.ts → createInMemoryBridge-HJopAIbo.d.ts} +12 -2
- package/dist/plugin.d.ts +2 -2
- package/dist/server.d.ts +62 -9
- package/dist/server.js +261 -46
- package/dist/shared.d.ts +2 -2
- package/dist/{surface-CEEkd81D.d.ts → surface-obE7YwJk.d.ts} +2 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +1 -1
- package/dist/{ui-bridge-Bdgl2hR8.d.ts → ui-bridge-DFNem0df.d.ts} +2 -2
- package/dist/workspace.css +79 -27
- package/dist/workspace.d.ts +88 -2
- package/dist/workspace.js +472 -386
- package/docs/PLUGIN_STRUCTURE.md +5 -4
- package/docs/PLUGIN_SYSTEM.md +6 -6
- package/docs/plans/archive/UNIFIED_PLUGIN_SYSTEM_PLAN.md +2 -2
- package/package.json +3 -3
- package/dist/FileTree-DUxjUbxL.js +0 -266
- package/dist/WorkspaceLoadingState-DJF_4S4_.js +0 -613
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
|
-
|
|
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 =
|
|
48
|
-
state:
|
|
49
|
-
causedBy:
|
|
200
|
+
var setStateBodySchema = z2.object({
|
|
201
|
+
state: z2.record(z2.unknown()),
|
|
202
|
+
causedBy: z2.enum(["user", "agent", "restore"]).optional()
|
|
50
203
|
});
|
|
51
|
-
var postCommandBodySchema =
|
|
52
|
-
kind:
|
|
53
|
-
params:
|
|
204
|
+
var postCommandBodySchema = z2.object({
|
|
205
|
+
kind: z2.string().min(1),
|
|
206
|
+
params: z2.record(z2.unknown()).default({})
|
|
54
207
|
});
|
|
55
|
-
function
|
|
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
|
|
74
|
-
const
|
|
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.
|
|
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 ??
|
|
286
|
-
const verifyRetries = opts.verifyRetries ??
|
|
287
|
-
const verifyIntervalMs = opts.verifyIntervalMs ??
|
|
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
|
|
360
|
-
"
|
|
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
|
|
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
|
|
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:
|
|
949
|
+
path: join2(boringPiRoot, "skills/boring-plugin-authoring/SKILL.md")
|
|
742
950
|
},
|
|
743
951
|
{
|
|
744
952
|
topic: "Panels (registration, dockview, layout)",
|
|
745
|
-
path:
|
|
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:
|
|
957
|
+
path: join2(boringPiRoot, "references/workspace/bridge.md")
|
|
750
958
|
},
|
|
751
959
|
{
|
|
752
960
|
topic: "Server plugins (defineServerPlugin, routes, agent tools)",
|
|
753
|
-
path:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
1087
|
-
if (existsSync2(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
1562
|
+
const frontRoot = join5(plugin.rootDir, "front");
|
|
1350
1563
|
const rel = relative3(frontRoot, plugin.frontPath);
|
|
1351
|
-
return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel) ? frontRoot :
|
|
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(
|
|
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 ??
|
|
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(
|
|
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-
|
|
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-
|
|
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,
|
|
4
|
+
import { h as cs, q as fs, o as ps } from "./WorkspaceProvider-Cg-J1wxr.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
|
-
}) =>
|
|
38
|
+
}) => unknown): () => void;
|
|
39
39
|
drainCommands?(): Promise<Array<UiCommand & {
|
|
40
40
|
seq: number;
|
|
41
41
|
}>>;
|