@hachej/boring-generated-pane 0.1.60

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 boringdata
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @hachej/boring-generated-pane
2
+
3
+ Generated pane runtime for Boring workspace.
4
+
5
+ This plugin wraps Vercel Labs `json-render` so agents can write safe JSON pane specs instead of React code. Apps/plugins provide the component catalog; the runtime validates the spec and renders only registered components.
6
+
7
+ ## Spec shape
8
+
9
+ ```json
10
+ {
11
+ "kind": "boring.generated-pane",
12
+ "version": 1,
13
+ "profile": "base",
14
+ "title": "Project Status",
15
+ "root": "main",
16
+ "elements": {
17
+ "main": { "type": "Card", "props": { "title": "Status" }, "children": ["body"] },
18
+ "body": { "type": "Text", "props": { "text": "All systems green" } }
19
+ }
20
+ }
21
+ ```
22
+
23
+ ## Boundary
24
+
25
+ - Agents generate JSON specs.
26
+ - Plugins define allowed components and prop schemas.
27
+ - Unknown components/props are rejected by the json-render catalog.
28
+ - Actions must be mapped by the host/plugin; no generated JavaScript is executed.
29
+
30
+ ## Eval
31
+
32
+ Run the plugin-local authoring eval and JSON validation from the repo root:
33
+
34
+ ```bash
35
+ pnpm --filter @hachej/boring-generated-pane playground:eval
36
+ ```
37
+
38
+ The eval checks that the agent writes a `panes/*.pane.json` file and the runner parses it with `parseGeneratedPaneSpec`.
39
+
40
+ ## Extending
41
+
42
+ Use `defineGeneratedPaneProfile` to add app/plugin-specific components, then render with `GeneratedPaneRenderer` or a profile-specific pane.
@@ -0,0 +1,62 @@
1
+ // src/shared/index.ts
2
+ function isRecord(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4
+ }
5
+ function isStringArray(value) {
6
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
7
+ }
8
+ function parseGeneratedPaneSpec(value) {
9
+ const errors = [];
10
+ if (!isRecord(value)) return { spec: null, errors: ["generated pane spec must be an object"] };
11
+ if (value.kind !== "boring.generated-pane") errors.push('generated pane spec kind must be "boring.generated-pane"');
12
+ if (value.version !== 1) errors.push("generated pane spec version must be 1");
13
+ if (value.profile !== void 0 && typeof value.profile !== "string") errors.push("generated pane profile must be a string");
14
+ if (value.title !== void 0 && typeof value.title !== "string") errors.push("generated pane title must be a string");
15
+ if (value.description !== void 0 && typeof value.description !== "string") errors.push("generated pane description must be a string");
16
+ if (typeof value.root !== "string" || value.root.length === 0) errors.push("generated pane root must be a string");
17
+ if (!isRecord(value.elements)) {
18
+ errors.push("generated pane elements must be an object");
19
+ } else {
20
+ for (const [id, element] of Object.entries(value.elements)) {
21
+ if (!isRecord(element)) {
22
+ errors.push(`element ${id} must be an object`);
23
+ continue;
24
+ }
25
+ if (typeof element.type !== "string" || element.type.length === 0) errors.push(`element ${id} must include type`);
26
+ if (element.props !== void 0 && !isRecord(element.props)) errors.push(`element ${id}.props must be an object`);
27
+ if (element.children !== void 0 && !isStringArray(element.children)) errors.push(`element ${id}.children must be an array of ids`);
28
+ }
29
+ }
30
+ if (isRecord(value.elements) && typeof value.root === "string") validateAcyclic(value.root, value.elements, errors);
31
+ if (value.queries !== void 0 && !isRecord(value.queries)) errors.push("generated pane queries must be an object");
32
+ if (errors.length > 0) return { spec: null, errors };
33
+ return { spec: value, errors: [] };
34
+ }
35
+ function validateAcyclic(root, elements, errors) {
36
+ const visiting = /* @__PURE__ */ new Set();
37
+ const visited = /* @__PURE__ */ new Set();
38
+ const visit = (id, path) => {
39
+ if (visiting.has(id)) {
40
+ errors.push(`generated pane element cycle: ${[...path, id].join(" -> ")}`);
41
+ return;
42
+ }
43
+ if (visited.has(id)) return;
44
+ const element = elements[id];
45
+ if (!isRecord(element)) {
46
+ errors.push(`element ${id} is referenced but not defined`);
47
+ return;
48
+ }
49
+ visiting.add(id);
50
+ if (isStringArray(element.children)) {
51
+ for (const child of element.children) visit(child, [...path, id]);
52
+ }
53
+ visiting.delete(id);
54
+ visited.add(id);
55
+ };
56
+ visit(root, []);
57
+ }
58
+
59
+ export {
60
+ parseGeneratedPaneSpec
61
+ };
62
+ //# sourceMappingURL=chunk-4IMMGGBQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/index.ts"],"sourcesContent":["export interface GeneratedPaneElementSpec {\n type: string\n props?: Record<string, unknown>\n children?: string[]\n}\n\nexport interface GeneratedPaneSpec {\n kind: \"boring.generated-pane\"\n version: 1\n profile?: string\n title?: string\n description?: string\n root: string\n elements: Record<string, GeneratedPaneElementSpec>\n queries?: Record<string, unknown>\n}\n\nexport interface GeneratedPaneValidationResult {\n spec: GeneratedPaneSpec | null\n errors: string[]\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n\nfunction isStringArray(value: unknown): value is string[] {\n return Array.isArray(value) && value.every((item) => typeof item === \"string\")\n}\n\nexport function parseGeneratedPaneSpec(value: unknown): GeneratedPaneValidationResult {\n const errors: string[] = []\n if (!isRecord(value)) return { spec: null, errors: [\"generated pane spec must be an object\"] }\n if (value.kind !== \"boring.generated-pane\") errors.push('generated pane spec kind must be \"boring.generated-pane\"')\n if (value.version !== 1) errors.push(\"generated pane spec version must be 1\")\n if (value.profile !== undefined && typeof value.profile !== \"string\") errors.push(\"generated pane profile must be a string\")\n if (value.title !== undefined && typeof value.title !== \"string\") errors.push(\"generated pane title must be a string\")\n if (value.description !== undefined && typeof value.description !== \"string\") errors.push(\"generated pane description must be a string\")\n if (typeof value.root !== \"string\" || value.root.length === 0) errors.push(\"generated pane root must be a string\")\n if (!isRecord(value.elements)) {\n errors.push(\"generated pane elements must be an object\")\n } else {\n for (const [id, element] of Object.entries(value.elements)) {\n if (!isRecord(element)) {\n errors.push(`element ${id} must be an object`)\n continue\n }\n if (typeof element.type !== \"string\" || element.type.length === 0) errors.push(`element ${id} must include type`)\n if (element.props !== undefined && !isRecord(element.props)) errors.push(`element ${id}.props must be an object`)\n if (element.children !== undefined && !isStringArray(element.children)) errors.push(`element ${id}.children must be an array of ids`)\n }\n }\n if (isRecord(value.elements) && typeof value.root === \"string\") validateAcyclic(value.root, value.elements, errors)\n if (value.queries !== undefined && !isRecord(value.queries)) errors.push(\"generated pane queries must be an object\")\n if (errors.length > 0) return { spec: null, errors }\n return { spec: value as unknown as GeneratedPaneSpec, errors: [] }\n}\n\nfunction validateAcyclic(root: string, elements: Record<string, unknown>, errors: string[]): void {\n const visiting = new Set<string>()\n const visited = new Set<string>()\n const visit = (id: string, path: string[]): void => {\n if (visiting.has(id)) {\n errors.push(`generated pane element cycle: ${[...path, id].join(\" -> \")}`)\n return\n }\n if (visited.has(id)) return\n const element = elements[id]\n if (!isRecord(element)) {\n errors.push(`element ${id} is referenced but not defined`)\n return\n }\n visiting.add(id)\n if (isStringArray(element.children)) {\n for (const child of element.children) visit(child, [...path, id])\n }\n visiting.delete(id)\n visited.add(id)\n }\n visit(root, [])\n}\n"],"mappings":";AAsBA,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,cAAc,OAAmC;AACxD,SAAO,MAAM,QAAQ,KAAK,KAAK,MAAM,MAAM,CAAC,SAAS,OAAO,SAAS,QAAQ;AAC/E;AAEO,SAAS,uBAAuB,OAA+C;AACpF,QAAM,SAAmB,CAAC;AAC1B,MAAI,CAAC,SAAS,KAAK,EAAG,QAAO,EAAE,MAAM,MAAM,QAAQ,CAAC,uCAAuC,EAAE;AAC7F,MAAI,MAAM,SAAS,wBAAyB,QAAO,KAAK,0DAA0D;AAClH,MAAI,MAAM,YAAY,EAAG,QAAO,KAAK,uCAAuC;AAC5E,MAAI,MAAM,YAAY,UAAa,OAAO,MAAM,YAAY,SAAU,QAAO,KAAK,yCAAyC;AAC3H,MAAI,MAAM,UAAU,UAAa,OAAO,MAAM,UAAU,SAAU,QAAO,KAAK,uCAAuC;AACrH,MAAI,MAAM,gBAAgB,UAAa,OAAO,MAAM,gBAAgB,SAAU,QAAO,KAAK,6CAA6C;AACvI,MAAI,OAAO,MAAM,SAAS,YAAY,MAAM,KAAK,WAAW,EAAG,QAAO,KAAK,sCAAsC;AACjH,MAAI,CAAC,SAAS,MAAM,QAAQ,GAAG;AAC7B,WAAO,KAAK,2CAA2C;AAAA,EACzD,OAAO;AACL,eAAW,CAAC,IAAI,OAAO,KAAK,OAAO,QAAQ,MAAM,QAAQ,GAAG;AAC1D,UAAI,CAAC,SAAS,OAAO,GAAG;AACtB,eAAO,KAAK,WAAW,EAAE,oBAAoB;AAC7C;AAAA,MACF;AACA,UAAI,OAAO,QAAQ,SAAS,YAAY,QAAQ,KAAK,WAAW,EAAG,QAAO,KAAK,WAAW,EAAE,oBAAoB;AAChH,UAAI,QAAQ,UAAU,UAAa,CAAC,SAAS,QAAQ,KAAK,EAAG,QAAO,KAAK,WAAW,EAAE,0BAA0B;AAChH,UAAI,QAAQ,aAAa,UAAa,CAAC,cAAc,QAAQ,QAAQ,EAAG,QAAO,KAAK,WAAW,EAAE,mCAAmC;AAAA,IACtI;AAAA,EACF;AACA,MAAI,SAAS,MAAM,QAAQ,KAAK,OAAO,MAAM,SAAS,SAAU,iBAAgB,MAAM,MAAM,MAAM,UAAU,MAAM;AAClH,MAAI,MAAM,YAAY,UAAa,CAAC,SAAS,MAAM,OAAO,EAAG,QAAO,KAAK,0CAA0C;AACnH,MAAI,OAAO,SAAS,EAAG,QAAO,EAAE,MAAM,MAAM,OAAO;AACnD,SAAO,EAAE,MAAM,OAAuC,QAAQ,CAAC,EAAE;AACnE;AAEA,SAAS,gBAAgB,MAAc,UAAmC,QAAwB;AAChG,QAAM,WAAW,oBAAI,IAAY;AACjC,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,QAAQ,CAAC,IAAY,SAAyB;AAClD,QAAI,SAAS,IAAI,EAAE,GAAG;AACpB,aAAO,KAAK,iCAAiC,CAAC,GAAG,MAAM,EAAE,EAAE,KAAK,MAAM,CAAC,EAAE;AACzE;AAAA,IACF;AACA,QAAI,QAAQ,IAAI,EAAE,EAAG;AACrB,UAAM,UAAU,SAAS,EAAE;AAC3B,QAAI,CAAC,SAAS,OAAO,GAAG;AACtB,aAAO,KAAK,WAAW,EAAE,gCAAgC;AACzD;AAAA,IACF;AACA,aAAS,IAAI,EAAE;AACf,QAAI,cAAc,QAAQ,QAAQ,GAAG;AACnC,iBAAW,SAAS,QAAQ,SAAU,OAAM,OAAO,CAAC,GAAG,MAAM,EAAE,CAAC;AAAA,IAClE;AACA,aAAS,OAAO,EAAE;AAClB,YAAQ,IAAI,EAAE;AAAA,EAChB;AACA,QAAM,MAAM,CAAC,CAAC;AAChB;","names":[]}
@@ -0,0 +1,110 @@
1
+ import * as _hachej_boring_workspace_plugin from '@hachej/boring-workspace/plugin';
2
+ import { PaneProps, BoringFrontSurfaceResolverRegistration } from '@hachej/boring-workspace/plugin';
3
+ import * as react_jsx_runtime from 'react/jsx-runtime';
4
+ import { GeneratedPaneSpec } from '../shared/index.js';
5
+ export { GeneratedPaneElementSpec, GeneratedPaneValidationResult, parseGeneratedPaneSpec } from '../shared/index.js';
6
+ import * as _json_render_core from '@json-render/core';
7
+ import { ReactNode } from 'react';
8
+ import { z } from 'zod';
9
+
10
+ type GeneratedPaneActionHandler = (params: Record<string, unknown>) => void | Promise<void>;
11
+ interface GeneratedPaneComponentDefinition {
12
+ props: z.ZodObject<Record<string, z.ZodType>>;
13
+ slots?: string[];
14
+ description: string;
15
+ component: (props: {
16
+ props: Record<string, unknown>;
17
+ children?: ReactNode;
18
+ emit: (event: string) => void;
19
+ }) => ReactNode;
20
+ }
21
+ interface GeneratedPaneActionDefinition {
22
+ description: string;
23
+ params?: z.ZodObject<Record<string, z.ZodType>>;
24
+ handler?: GeneratedPaneActionHandler;
25
+ }
26
+ interface GeneratedPaneProfile {
27
+ id: string;
28
+ label: string;
29
+ components: Record<string, GeneratedPaneComponentDefinition>;
30
+ actions?: Record<string, GeneratedPaneActionDefinition>;
31
+ }
32
+ declare function defineGeneratedPaneProfile(profile: GeneratedPaneProfile): GeneratedPaneProfile;
33
+ declare const baseGeneratedPaneProfile: GeneratedPaneProfile;
34
+ declare function mergeGeneratedPaneProfiles(...profiles: GeneratedPaneProfile[]): GeneratedPaneProfile;
35
+ declare function createGeneratedPaneCatalog(profile: GeneratedPaneProfile): _json_render_core.Catalog<{
36
+ spec: _json_render_core.SchemaType<"object", {
37
+ root: _json_render_core.SchemaType<"string", unknown>;
38
+ elements: _json_render_core.SchemaType<"record", _json_render_core.SchemaType<"object", {
39
+ type: _json_render_core.SchemaType<"ref", string>;
40
+ props: _json_render_core.SchemaType<"propsOf", string>;
41
+ children: _json_render_core.SchemaType<"array", _json_render_core.SchemaType<"string", unknown>>;
42
+ visible: _json_render_core.SchemaType<"any", unknown>;
43
+ }>>;
44
+ }>;
45
+ catalog: _json_render_core.SchemaType<"object", {
46
+ components: _json_render_core.SchemaType<"map", {
47
+ props: _json_render_core.SchemaType<"zod", unknown>;
48
+ slots: _json_render_core.SchemaType<"array", _json_render_core.SchemaType<"string", unknown>>;
49
+ description: _json_render_core.SchemaType<"string", unknown>;
50
+ example: _json_render_core.SchemaType<"any", unknown>;
51
+ }>;
52
+ actions: _json_render_core.SchemaType<"map", {
53
+ params: _json_render_core.SchemaType<"zod", unknown>;
54
+ description: _json_render_core.SchemaType<"string", unknown>;
55
+ }>;
56
+ }>;
57
+ }, {
58
+ components: {
59
+ [k: string]: {
60
+ props: z.ZodObject<Record<string, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>, z.core.$strip>;
61
+ slots: string[];
62
+ description: string;
63
+ };
64
+ };
65
+ actions: {
66
+ [k: string]: {
67
+ params: z.ZodObject<Record<string, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>, z.core.$strip> | undefined;
68
+ description: string;
69
+ };
70
+ };
71
+ }>;
72
+ declare function GeneratedPaneRenderer({ spec, profile }: {
73
+ spec: GeneratedPaneSpec;
74
+ profile?: GeneratedPaneProfile;
75
+ }): react_jsx_runtime.JSX.Element;
76
+
77
+ interface GeneratedPanePaneParams {
78
+ path?: string;
79
+ spec?: GeneratedPaneSpec;
80
+ }
81
+ declare function GeneratedPanePane({ params, profile }: PaneProps<GeneratedPanePaneParams> & {
82
+ profile?: GeneratedPaneProfile;
83
+ }): react_jsx_runtime.JSX.Element;
84
+
85
+ interface GeneratedPaneExplorerConfig {
86
+ title?: string;
87
+ patterns?: string[];
88
+ panelId?: string;
89
+ itemLabel?: string;
90
+ emptyTitle?: string;
91
+ emptyDescription?: string;
92
+ }
93
+ declare function createGeneratedPaneExplorerPane(config?: GeneratedPaneExplorerConfig): (props: PaneProps<{
94
+ searchQuery?: string;
95
+ }>) => react_jsx_runtime.JSX.Element;
96
+ declare function GeneratedPaneExplorerPane({ params, containerApi, config }: PaneProps<{
97
+ searchQuery?: string;
98
+ }> & {
99
+ config?: GeneratedPaneExplorerConfig;
100
+ }): react_jsx_runtime.JSX.Element;
101
+
102
+ declare const GENERATED_PANE_PANEL_ID = "generated-pane.panel";
103
+ declare const GENERATED_PANE_EXPLORER_LEFT_TAB_ID = "generated-pane.explorer";
104
+
105
+ declare function isGeneratedPanePath(path: string): boolean;
106
+ declare const generatedPaneSurfaceResolver: BoringFrontSurfaceResolverRegistration;
107
+
108
+ declare const generatedPanePlugin: _hachej_boring_workspace_plugin.BoringFrontFactoryWithId;
109
+
110
+ export { GENERATED_PANE_EXPLORER_LEFT_TAB_ID, GENERATED_PANE_PANEL_ID, type GeneratedPaneActionDefinition, type GeneratedPaneActionHandler, type GeneratedPaneComponentDefinition, type GeneratedPaneExplorerConfig, GeneratedPaneExplorerPane, GeneratedPanePane, type GeneratedPanePaneParams, type GeneratedPaneProfile, GeneratedPaneRenderer, GeneratedPaneSpec, baseGeneratedPaneProfile, createGeneratedPaneCatalog, createGeneratedPaneExplorerPane, generatedPanePlugin as default, defineGeneratedPaneProfile, generatedPanePlugin, generatedPaneSurfaceResolver, isGeneratedPanePath, mergeGeneratedPaneProfiles };
@@ -0,0 +1,380 @@
1
+ import {
2
+ parseGeneratedPaneSpec
3
+ } from "../chunk-4IMMGGBQ.js";
4
+
5
+ // src/front/index.ts
6
+ import { PanelTop as PanelTop2 } from "lucide-react";
7
+ import { definePlugin } from "@hachej/boring-workspace/plugin";
8
+
9
+ // src/front/GeneratedPanePane.tsx
10
+ import { useEffect, useState } from "react";
11
+ import { EmptyState, Toolbar, ToolbarGroup, Badge as Badge2 } from "@hachej/boring-ui-kit";
12
+ import { useApiBaseUrl, useWorkspaceRequestId } from "@hachej/boring-workspace";
13
+
14
+ // src/front/catalog.tsx
15
+ import { defineCatalog } from "@json-render/core";
16
+ import { defineRegistry, JSONUIProvider, Renderer } from "@json-render/react";
17
+ import { schema } from "@json-render/react/schema";
18
+ import { z } from "zod";
19
+ import { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from "@hachej/boring-ui-kit";
20
+ import { jsx, jsxs } from "react/jsx-runtime";
21
+ function defineGeneratedPaneProfile(profile) {
22
+ return profile;
23
+ }
24
+ var baseGeneratedPaneProfile = defineGeneratedPaneProfile({
25
+ id: "base",
26
+ label: "Generated Pane",
27
+ components: {
28
+ Card: {
29
+ description: "A bordered content card with an optional title and description.",
30
+ slots: ["default"],
31
+ props: z.object({ title: z.string().optional(), description: z.string().optional() }),
32
+ component: ({ props, children }) => /* @__PURE__ */ jsxs(Card, { children: [
33
+ props.title || props.description ? /* @__PURE__ */ jsxs(CardHeader, { children: [
34
+ typeof props.title === "string" ? /* @__PURE__ */ jsx(CardTitle, { children: props.title }) : null,
35
+ typeof props.description === "string" ? /* @__PURE__ */ jsx(CardDescription, { children: props.description }) : null
36
+ ] }) : null,
37
+ /* @__PURE__ */ jsx(CardContent, { children })
38
+ ] })
39
+ },
40
+ Stack: {
41
+ description: "Vertical stack for grouping child elements.",
42
+ slots: ["default"],
43
+ props: z.object({ gap: z.enum(["sm", "md", "lg"]).optional() }),
44
+ component: ({ props, children }) => /* @__PURE__ */ jsx("div", { className: props.gap === "sm" ? "space-y-2" : props.gap === "lg" ? "space-y-6" : "space-y-4", children })
45
+ },
46
+ Grid: {
47
+ description: "Responsive grid layout for child elements.",
48
+ slots: ["default"],
49
+ props: z.object({ columns: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]).optional() }),
50
+ component: ({ props, children }) => {
51
+ const columns = props.columns === 3 ? "lg:grid-cols-3" : props.columns === 4 ? "lg:grid-cols-4" : props.columns === 1 ? "grid-cols-1" : "lg:grid-cols-2";
52
+ return /* @__PURE__ */ jsx("div", { className: `grid gap-4 ${columns}`, children });
53
+ }
54
+ },
55
+ Text: {
56
+ description: "Plain text block.",
57
+ props: z.object({ text: z.string(), tone: z.enum(["default", "muted"]).optional() }),
58
+ component: ({ props }) => /* @__PURE__ */ jsx("p", { className: props.tone === "muted" ? "text-sm text-muted-foreground" : "text-sm text-foreground", children: String(props.text) })
59
+ },
60
+ Badge: {
61
+ description: "Small status badge.",
62
+ props: z.object({ label: z.string(), variant: z.enum(["default", "secondary", "outline"]).optional() }),
63
+ component: ({ props }) => /* @__PURE__ */ jsx(Badge, { variant: props.variant ?? "secondary", children: String(props.label) })
64
+ },
65
+ Alert: {
66
+ description: "Notice block for warnings, status, or context.",
67
+ props: z.object({ title: z.string(), description: z.string().optional() }),
68
+ component: ({ props }) => /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-border bg-muted/40 p-3 text-sm", children: [
69
+ /* @__PURE__ */ jsx("div", { className: "font-medium text-foreground", children: String(props.title) }),
70
+ typeof props.description === "string" ? /* @__PURE__ */ jsx("div", { className: "mt-1 text-muted-foreground", children: props.description }) : null
71
+ ] })
72
+ },
73
+ Button: {
74
+ description: "Button that emits its press event to a configured action.",
75
+ props: z.object({ label: z.string() }),
76
+ component: ({ props, emit }) => /* @__PURE__ */ jsx(Button, { size: "sm", onClick: () => emit("press"), children: String(props.label) })
77
+ }
78
+ }
79
+ });
80
+ function mergeGeneratedPaneProfiles(...profiles) {
81
+ const [first, ...rest] = profiles;
82
+ return {
83
+ id: first?.id ?? "generated-pane",
84
+ label: first?.label ?? "Generated Pane",
85
+ components: Object.assign({}, ...profiles.map((profile) => profile.components)),
86
+ actions: Object.assign({}, ...profiles.map((profile) => profile.actions ?? {}))
87
+ };
88
+ }
89
+ function createGeneratedPaneCatalog(profile) {
90
+ return defineCatalog(schema, {
91
+ components: Object.fromEntries(Object.entries(profile.components).map(([name, definition]) => [name, {
92
+ props: definition.props,
93
+ slots: definition.slots ?? [],
94
+ description: definition.description
95
+ }])),
96
+ actions: Object.fromEntries(Object.entries(profile.actions ?? {}).map(([name, definition]) => [name, {
97
+ params: definition.params,
98
+ description: definition.description
99
+ }]))
100
+ });
101
+ }
102
+ function GeneratedPaneRenderer({ spec, profile }) {
103
+ const activeProfile = profile ? mergeGeneratedPaneProfiles(baseGeneratedPaneProfile, profile) : baseGeneratedPaneProfile;
104
+ const catalog = createGeneratedPaneCatalog(activeProfile);
105
+ const normalizedElements = Object.fromEntries(Object.entries(spec.elements).map(([id, element]) => [id, {
106
+ ...element,
107
+ props: element.props ?? {},
108
+ children: element.children ?? [],
109
+ visible: "visible" in element ? element.visible : true
110
+ }]));
111
+ const validation = catalog.validate({ root: spec.root, elements: normalizedElements });
112
+ if (!validation.success) {
113
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive", children: [
114
+ "Invalid generated pane spec: ",
115
+ validation.error?.issues.slice(0, 3).map((issue) => issue.message).join(" \u2022 ")
116
+ ] });
117
+ }
118
+ if (!validation.data) {
119
+ return /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive", children: "Invalid generated pane spec" });
120
+ }
121
+ const { registry } = defineRegistry(catalog, {
122
+ components: Object.fromEntries(Object.entries(activeProfile.components).map(([name, definition]) => [name, ({ props, children, emit }) => definition.component({ props, children, emit })])),
123
+ actions: {}
124
+ });
125
+ return /* @__PURE__ */ jsx(JSONUIProvider, { registry, handlers: {}, children: /* @__PURE__ */ jsx(Renderer, { spec: validation.data, registry }) });
126
+ }
127
+
128
+ // src/front/GeneratedPanePane.tsx
129
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
130
+ function GeneratedPanePane({ params, profile }) {
131
+ const apiBaseUrl = useApiBaseUrl();
132
+ const workspaceId = useWorkspaceRequestId();
133
+ const [loadedFile, setLoadedFile] = useState({ spec: null, loading: false });
134
+ useEffect(() => {
135
+ if (!params?.path || params.spec) {
136
+ setLoadedFile({ spec: null, loading: false });
137
+ return;
138
+ }
139
+ const controller = new AbortController();
140
+ setLoadedFile({ spec: null, loading: true });
141
+ void fetch(`${apiBaseUrl}/api/v1/files/raw?path=${encodeURIComponent(params.path)}`, {
142
+ signal: controller.signal,
143
+ credentials: "include",
144
+ headers: workspaceId ? { "x-boring-workspace-id": workspaceId } : {}
145
+ }).then(async (response) => {
146
+ if (!response.ok) throw new Error(`Failed to load ${params.path}: HTTP ${response.status}`);
147
+ return await response.text();
148
+ }).then((text) => setLoadedFile({ spec: JSON.parse(text), loading: false })).catch((error) => {
149
+ if (controller.signal.aborted) return;
150
+ setLoadedFile({ spec: null, loading: false, error: error instanceof Error ? error.message : String(error) });
151
+ });
152
+ return () => controller.abort();
153
+ }, [apiBaseUrl, params?.path, params?.spec, workspaceId]);
154
+ if (loadedFile.loading) return /* @__PURE__ */ jsx2(PaneState, { title: "Loading generated pane", description: params?.path });
155
+ if (loadedFile.error) return /* @__PURE__ */ jsx2(PaneState, { title: "Could not load generated pane", description: loadedFile.error });
156
+ const parsed = parseGeneratedPaneSpec(params?.spec ?? loadedFile.spec);
157
+ if (!parsed.spec) return /* @__PURE__ */ jsx2(PaneState, { title: "Invalid generated pane spec", description: parsed.errors.slice(0, 5).join(" \u2022 ") });
158
+ return /* @__PURE__ */ jsxs2("div", { className: "flex h-full min-h-0 min-w-0 flex-col bg-background text-foreground", children: [
159
+ /* @__PURE__ */ jsx2(Toolbar, { className: "border-b border-border px-3 py-2", children: /* @__PURE__ */ jsxs2(ToolbarGroup, { children: [
160
+ /* @__PURE__ */ jsx2(Badge2, { variant: "secondary", children: "Generated Pane" }),
161
+ parsed.spec.profile ? /* @__PURE__ */ jsx2(Badge2, { variant: "outline", children: parsed.spec.profile }) : null
162
+ ] }) }),
163
+ /* @__PURE__ */ jsxs2("div", { className: "min-h-0 min-w-0 flex-1 overflow-auto p-4", children: [
164
+ parsed.spec.title ? /* @__PURE__ */ jsx2("h1", { className: "mb-1 text-2xl font-semibold tracking-tight", children: parsed.spec.title }) : null,
165
+ parsed.spec.description ? /* @__PURE__ */ jsx2("p", { className: "mb-4 text-sm text-muted-foreground", children: parsed.spec.description }) : null,
166
+ /* @__PURE__ */ jsx2(GeneratedPaneRenderer, { spec: parsed.spec, profile })
167
+ ] })
168
+ ] });
169
+ }
170
+ function PaneState({ title, description }) {
171
+ return /* @__PURE__ */ jsx2("div", { className: "flex h-full min-h-0 min-w-0 items-center justify-center bg-background p-6 text-foreground", children: /* @__PURE__ */ jsx2(EmptyState, { title, description }) });
172
+ }
173
+
174
+ // src/front/GeneratedPaneExplorerPane.tsx
175
+ import { useEffect as useEffect2, useMemo, useState as useState2 } from "react";
176
+ import { FileJson2, PanelTop, RefreshCw } from "lucide-react";
177
+ import { Badge as Badge3, EmptyState as EmptyState2, IconButton } from "@hachej/boring-ui-kit";
178
+ import { useApiBaseUrl as useApiBaseUrl2, useWorkspaceRequestId as useWorkspaceRequestId2 } from "@hachej/boring-workspace";
179
+
180
+ // src/front/constants.ts
181
+ var GENERATED_PANE_PANEL_ID = "generated-pane.panel";
182
+ var GENERATED_PANE_EXPLORER_LEFT_TAB_ID = "generated-pane.explorer";
183
+
184
+ // src/front/GeneratedPaneExplorerPane.tsx
185
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
186
+ var DEFAULT_PATTERNS = ["**/*.pane.json"];
187
+ function titleFromPath(path) {
188
+ const file = path.split("/").pop() ?? path;
189
+ return file.replace(/\.(pane|dashboard)\.json$/i, "").replace(/[-_]+/g, " ");
190
+ }
191
+ function matchesQuery(path, query) {
192
+ const value = query?.trim().toLowerCase();
193
+ if (!value) return true;
194
+ return path.toLowerCase().includes(value) || titleFromPath(path).toLowerCase().includes(value);
195
+ }
196
+ function dedupeAndSort(values) {
197
+ return [...new Set(values)].sort((a, b) => a.localeCompare(b));
198
+ }
199
+ function createGeneratedPaneExplorerPane(config = {}) {
200
+ return function GeneratedPaneExplorerPaneWithConfig(props) {
201
+ return /* @__PURE__ */ jsx3(GeneratedPaneExplorerPane, { ...props, config });
202
+ };
203
+ }
204
+ function GeneratedPaneExplorerPane({ params, containerApi, config = {} }) {
205
+ const apiBaseUrl = useApiBaseUrl2();
206
+ const workspaceId = useWorkspaceRequestId2();
207
+ const patterns = config.patterns?.length ? config.patterns : DEFAULT_PATTERNS;
208
+ const panelId = config.panelId ?? GENERATED_PANE_PANEL_ID;
209
+ const title = config.title ?? "Panes";
210
+ const itemLabel = config.itemLabel ?? "Generated pane";
211
+ const [state, setState] = useState2({ loading: true, paths: [] });
212
+ const [refreshKey, setRefreshKey] = useState2(0);
213
+ useEffect2(() => {
214
+ const controller = new AbortController();
215
+ setState((prev) => ({ ...prev, loading: true, error: void 0 }));
216
+ void Promise.all(patterns.map(async (pattern) => {
217
+ const response = await fetch(`${apiBaseUrl}/api/v1/files/search?q=${encodeURIComponent(pattern)}&limit=500`, {
218
+ signal: controller.signal,
219
+ credentials: "include",
220
+ headers: workspaceId ? { "x-boring-workspace-id": workspaceId } : {}
221
+ });
222
+ if (!response.ok) throw new Error(`Pane search failed for ${pattern} with HTTP ${response.status}`);
223
+ const body = await response.json();
224
+ return body.results ?? [];
225
+ })).then((groups) => {
226
+ setState({ loading: false, paths: dedupeAndSort(groups.flat()) });
227
+ }).catch((error) => {
228
+ if (controller.signal.aborted) return;
229
+ setState({ loading: false, paths: [], error: error instanceof Error ? error.message : String(error) });
230
+ });
231
+ return () => controller.abort();
232
+ }, [apiBaseUrl, patterns.join("\n"), refreshKey, workspaceId]);
233
+ const paths = useMemo(
234
+ () => state.paths.filter((path) => matchesQuery(path, params?.searchQuery)),
235
+ [params?.searchQuery, state.paths]
236
+ );
237
+ const openPane = (path) => {
238
+ containerApi.addPanel({
239
+ id: `${panelId}:${path}`,
240
+ component: panelId,
241
+ title: titleFromPath(path),
242
+ params: { path }
243
+ });
244
+ };
245
+ return /* @__PURE__ */ jsxs3("div", { className: "flex h-full min-h-0 flex-col text-sm", children: [
246
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between border-b border-border/60 px-3 py-2", children: [
247
+ /* @__PURE__ */ jsxs3("div", { className: "flex min-w-0 items-center gap-2", children: [
248
+ /* @__PURE__ */ jsx3(PanelTop, { className: "h-4 w-4 text-muted-foreground" }),
249
+ /* @__PURE__ */ jsx3("span", { className: "truncate font-medium", children: title }),
250
+ /* @__PURE__ */ jsx3(Badge3, { variant: "secondary", children: state.paths.length })
251
+ ] }),
252
+ /* @__PURE__ */ jsx3(
253
+ IconButton,
254
+ {
255
+ type: "button",
256
+ "aria-label": `Refresh ${title.toLowerCase()}`,
257
+ variant: "ghost",
258
+ size: "icon-xs",
259
+ onClick: () => setRefreshKey((value) => value + 1),
260
+ disabled: state.loading,
261
+ children: /* @__PURE__ */ jsx3(RefreshCw, { className: `h-3.5 w-3.5 ${state.loading ? "animate-spin" : ""}` })
262
+ }
263
+ )
264
+ ] }),
265
+ /* @__PURE__ */ jsx3("div", { className: "min-h-0 flex-1 overflow-auto p-2", children: state.error ? /* @__PURE__ */ jsx3(EmptyState2, { title: `Could not list ${title.toLowerCase()}`, description: state.error }) : state.loading ? /* @__PURE__ */ jsxs3("div", { className: "px-2 py-3 text-xs text-muted-foreground", children: [
266
+ "Scanning ",
267
+ title.toLowerCase(),
268
+ "\u2026"
269
+ ] }) : paths.length === 0 ? /* @__PURE__ */ jsx3(
270
+ EmptyState2,
271
+ {
272
+ title: config.emptyTitle ?? `No ${title.toLowerCase()} found`,
273
+ description: config.emptyDescription ?? `Create ${patterns.join(" or ")} files to list them here.`
274
+ }
275
+ ) : /* @__PURE__ */ jsx3("div", { className: "space-y-1", children: paths.map((path) => /* @__PURE__ */ jsxs3(
276
+ "button",
277
+ {
278
+ type: "button",
279
+ onClick: () => openPane(path),
280
+ className: "flex w-full min-w-0 items-start gap-2 rounded-lg px-2 py-2 text-left hover:bg-background/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40",
281
+ children: [
282
+ /* @__PURE__ */ jsx3(FileJson2, { className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" }),
283
+ /* @__PURE__ */ jsxs3("span", { className: "min-w-0 flex-1", children: [
284
+ /* @__PURE__ */ jsx3("span", { className: "block truncate text-[13px] font-medium text-foreground", children: titleFromPath(path) }),
285
+ /* @__PURE__ */ jsx3("span", { className: "block truncate text-[11px] text-muted-foreground", children: path })
286
+ ] }),
287
+ /* @__PURE__ */ jsx3(Badge3, { variant: "outline", className: "mt-0.5 shrink-0 text-[10px]", children: itemLabel })
288
+ ]
289
+ },
290
+ path
291
+ )) }) })
292
+ ] });
293
+ }
294
+
295
+ // src/front/surfaceResolver.ts
296
+ import {
297
+ WORKSPACE_OPEN_PATH_SURFACE_KIND
298
+ } from "@hachej/boring-workspace/plugin";
299
+ function isGeneratedPanePath(path) {
300
+ return /(^|\/)panes\/[^/].*\.pane\.json$/i.test(path) || /\.pane\.json$/i.test(path);
301
+ }
302
+ function titleFromPath2(path) {
303
+ const file = path.split("/").pop() ?? path;
304
+ return file.replace(/\.pane\.json$/i, "").replace(/[-_]+/g, " ");
305
+ }
306
+ var generatedPaneSurfaceResolver = {
307
+ id: "generated-pane.open-path",
308
+ kind: WORKSPACE_OPEN_PATH_SURFACE_KIND,
309
+ source: "app",
310
+ resolve: (request) => {
311
+ if (request.kind !== WORKSPACE_OPEN_PATH_SURFACE_KIND) return null;
312
+ const target = String(request.target ?? "");
313
+ if (!isGeneratedPanePath(target)) return null;
314
+ return {
315
+ component: GENERATED_PANE_PANEL_ID,
316
+ title: titleFromPath2(target),
317
+ params: { path: target },
318
+ score: 110
319
+ };
320
+ }
321
+ };
322
+
323
+ // src/front/index.ts
324
+ var generatedPanePlugin = definePlugin({
325
+ id: "generated-pane",
326
+ label: "Generated Pane",
327
+ panels: [
328
+ {
329
+ id: GENERATED_PANE_PANEL_ID,
330
+ label: "Generated Pane",
331
+ icon: PanelTop2,
332
+ component: GeneratedPanePane,
333
+ supportsFullPage: true
334
+ }
335
+ ],
336
+ leftTabs: [
337
+ {
338
+ id: GENERATED_PANE_EXPLORER_LEFT_TAB_ID,
339
+ title: "Panes",
340
+ panelId: GENERATED_PANE_EXPLORER_LEFT_TAB_ID,
341
+ icon: PanelTop2,
342
+ component: createGeneratedPaneExplorerPane({
343
+ title: "Panes",
344
+ patterns: ["**/*.pane.json"],
345
+ panelId: GENERATED_PANE_PANEL_ID,
346
+ itemLabel: "Pane",
347
+ emptyDescription: "Create panes/*.pane.json files to list generated panes here."
348
+ }),
349
+ chromeless: true
350
+ }
351
+ ],
352
+ surfaceResolvers: [generatedPaneSurfaceResolver],
353
+ commands: [
354
+ {
355
+ id: "generated-pane.open",
356
+ title: "Open Generated Pane",
357
+ panelId: GENERATED_PANE_PANEL_ID,
358
+ keywords: ["json-render", "generated", "custom pane", "pane"]
359
+ }
360
+ ]
361
+ });
362
+ var front_default = generatedPanePlugin;
363
+ export {
364
+ GENERATED_PANE_EXPLORER_LEFT_TAB_ID,
365
+ GENERATED_PANE_PANEL_ID,
366
+ GeneratedPaneExplorerPane,
367
+ GeneratedPanePane,
368
+ GeneratedPaneRenderer,
369
+ baseGeneratedPaneProfile,
370
+ createGeneratedPaneCatalog,
371
+ createGeneratedPaneExplorerPane,
372
+ front_default as default,
373
+ defineGeneratedPaneProfile,
374
+ generatedPanePlugin,
375
+ generatedPaneSurfaceResolver,
376
+ isGeneratedPanePath,
377
+ mergeGeneratedPaneProfiles,
378
+ parseGeneratedPaneSpec
379
+ };
380
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/front/index.ts","../../src/front/GeneratedPanePane.tsx","../../src/front/catalog.tsx","../../src/front/GeneratedPaneExplorerPane.tsx","../../src/front/constants.ts","../../src/front/surfaceResolver.ts"],"sourcesContent":["import { PanelTop } from \"lucide-react\"\nimport { definePlugin } from \"@hachej/boring-workspace/plugin\"\nimport { GeneratedPanePane } from \"./GeneratedPanePane\"\nimport { createGeneratedPaneExplorerPane, GeneratedPaneExplorerPane } from \"./GeneratedPaneExplorerPane\"\nimport { GENERATED_PANE_EXPLORER_LEFT_TAB_ID, GENERATED_PANE_PANEL_ID } from \"./constants\"\nimport { generatedPaneSurfaceResolver } from \"./surfaceResolver\"\n\nexport { GeneratedPanePane }\nexport type { GeneratedPanePaneParams } from \"./GeneratedPanePane\"\nexport { GeneratedPaneExplorerPane, createGeneratedPaneExplorerPane }\nexport type { GeneratedPaneExplorerConfig } from \"./GeneratedPaneExplorerPane\"\nexport {\n GeneratedPaneRenderer,\n baseGeneratedPaneProfile,\n createGeneratedPaneCatalog,\n defineGeneratedPaneProfile,\n mergeGeneratedPaneProfiles,\n} from \"./catalog\"\nexport type {\n GeneratedPaneActionDefinition,\n GeneratedPaneActionHandler,\n GeneratedPaneComponentDefinition,\n GeneratedPaneProfile,\n} from \"./catalog\"\nexport type { GeneratedPaneElementSpec, GeneratedPaneSpec, GeneratedPaneValidationResult } from \"../shared\"\nexport { parseGeneratedPaneSpec } from \"../shared\"\nexport { GENERATED_PANE_EXPLORER_LEFT_TAB_ID, GENERATED_PANE_PANEL_ID } from \"./constants\"\nexport { generatedPaneSurfaceResolver, isGeneratedPanePath } from \"./surfaceResolver\"\n\nexport const generatedPanePlugin = definePlugin({\n id: \"generated-pane\",\n label: \"Generated Pane\",\n panels: [\n {\n id: GENERATED_PANE_PANEL_ID,\n label: \"Generated Pane\",\n icon: PanelTop,\n component: GeneratedPanePane,\n supportsFullPage: true,\n },\n ],\n leftTabs: [\n {\n id: GENERATED_PANE_EXPLORER_LEFT_TAB_ID,\n title: \"Panes\",\n panelId: GENERATED_PANE_EXPLORER_LEFT_TAB_ID,\n icon: PanelTop,\n component: createGeneratedPaneExplorerPane({\n title: \"Panes\",\n patterns: [\"**/*.pane.json\"],\n panelId: GENERATED_PANE_PANEL_ID,\n itemLabel: \"Pane\",\n emptyDescription: \"Create panes/*.pane.json files to list generated panes here.\",\n }),\n chromeless: true,\n },\n ],\n surfaceResolvers: [generatedPaneSurfaceResolver],\n commands: [\n {\n id: \"generated-pane.open\",\n title: \"Open Generated Pane\",\n panelId: GENERATED_PANE_PANEL_ID,\n keywords: [\"json-render\", \"generated\", \"custom pane\", \"pane\"],\n },\n ],\n})\n\nexport default generatedPanePlugin\n","import { useEffect, useState } from \"react\"\nimport { EmptyState, Toolbar, ToolbarGroup, Badge } from \"@hachej/boring-ui-kit\"\nimport { useApiBaseUrl, useWorkspaceRequestId } from \"@hachej/boring-workspace\"\nimport type { PaneProps } from \"@hachej/boring-workspace/plugin\"\nimport { parseGeneratedPaneSpec, type GeneratedPaneSpec } from \"../shared\"\nimport { GeneratedPaneRenderer, type GeneratedPaneProfile } from \"./catalog\"\n\nexport interface GeneratedPanePaneParams {\n path?: string\n spec?: GeneratedPaneSpec\n}\n\ninterface LoadedPaneFile {\n spec: unknown\n error?: string\n loading: boolean\n}\n\nexport function GeneratedPanePane({ params, profile }: PaneProps<GeneratedPanePaneParams> & { profile?: GeneratedPaneProfile }) {\n const apiBaseUrl = useApiBaseUrl()\n const workspaceId = useWorkspaceRequestId()\n const [loadedFile, setLoadedFile] = useState<LoadedPaneFile>({ spec: null, loading: false })\n\n useEffect(() => {\n if (!params?.path || params.spec) {\n setLoadedFile({ spec: null, loading: false })\n return\n }\n const controller = new AbortController()\n setLoadedFile({ spec: null, loading: true })\n void fetch(`${apiBaseUrl}/api/v1/files/raw?path=${encodeURIComponent(params.path)}`, {\n signal: controller.signal,\n credentials: \"include\",\n headers: workspaceId ? { \"x-boring-workspace-id\": workspaceId } : {},\n })\n .then(async (response) => {\n if (!response.ok) throw new Error(`Failed to load ${params.path}: HTTP ${response.status}`)\n return await response.text()\n })\n .then((text) => setLoadedFile({ spec: JSON.parse(text), loading: false }))\n .catch((error) => {\n if (controller.signal.aborted) return\n setLoadedFile({ spec: null, loading: false, error: error instanceof Error ? error.message : String(error) })\n })\n return () => controller.abort()\n }, [apiBaseUrl, params?.path, params?.spec, workspaceId])\n\n if (loadedFile.loading) return <PaneState title=\"Loading generated pane\" description={params?.path} />\n if (loadedFile.error) return <PaneState title=\"Could not load generated pane\" description={loadedFile.error} />\n\n const parsed = parseGeneratedPaneSpec(params?.spec ?? loadedFile.spec)\n if (!parsed.spec) return <PaneState title=\"Invalid generated pane spec\" description={parsed.errors.slice(0, 5).join(\" • \")} />\n\n return (\n <div className=\"flex h-full min-h-0 min-w-0 flex-col bg-background text-foreground\">\n <Toolbar className=\"border-b border-border px-3 py-2\">\n <ToolbarGroup>\n <Badge variant=\"secondary\">Generated Pane</Badge>\n {parsed.spec.profile ? <Badge variant=\"outline\">{parsed.spec.profile}</Badge> : null}\n </ToolbarGroup>\n </Toolbar>\n <div className=\"min-h-0 min-w-0 flex-1 overflow-auto p-4\">\n {parsed.spec.title ? <h1 className=\"mb-1 text-2xl font-semibold tracking-tight\">{parsed.spec.title}</h1> : null}\n {parsed.spec.description ? <p className=\"mb-4 text-sm text-muted-foreground\">{parsed.spec.description}</p> : null}\n <GeneratedPaneRenderer spec={parsed.spec} profile={profile} />\n </div>\n </div>\n )\n}\n\nfunction PaneState({ title, description }: { title: string; description?: string }) {\n return (\n <div className=\"flex h-full min-h-0 min-w-0 items-center justify-center bg-background p-6 text-foreground\">\n <EmptyState title={title} description={description} />\n </div>\n )\n}\n","import type { ReactNode } from \"react\"\nimport { defineCatalog } from \"@json-render/core\"\nimport { defineRegistry, JSONUIProvider, Renderer } from \"@json-render/react\"\nimport { schema } from \"@json-render/react/schema\"\nimport { z } from \"zod\"\nimport { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from \"@hachej/boring-ui-kit\"\nimport type { GeneratedPaneSpec } from \"../shared\"\n\nexport type GeneratedPaneActionHandler = (params: Record<string, unknown>) => void | Promise<void>\n\nexport interface GeneratedPaneComponentDefinition {\n props: z.ZodObject<Record<string, z.ZodType>>\n slots?: string[]\n description: string\n component: (props: { props: Record<string, unknown>; children?: ReactNode; emit: (event: string) => void }) => ReactNode\n}\n\nexport interface GeneratedPaneActionDefinition {\n description: string\n params?: z.ZodObject<Record<string, z.ZodType>>\n handler?: GeneratedPaneActionHandler\n}\n\nexport interface GeneratedPaneProfile {\n id: string\n label: string\n components: Record<string, GeneratedPaneComponentDefinition>\n actions?: Record<string, GeneratedPaneActionDefinition>\n}\n\nexport function defineGeneratedPaneProfile(profile: GeneratedPaneProfile): GeneratedPaneProfile {\n return profile\n}\n\nexport const baseGeneratedPaneProfile = defineGeneratedPaneProfile({\n id: \"base\",\n label: \"Generated Pane\",\n components: {\n Card: {\n description: \"A bordered content card with an optional title and description.\",\n slots: [\"default\"],\n props: z.object({ title: z.string().optional(), description: z.string().optional() }),\n component: ({ props, children }) => (\n <Card>\n {props.title || props.description ? (\n <CardHeader>\n {typeof props.title === \"string\" ? <CardTitle>{props.title}</CardTitle> : null}\n {typeof props.description === \"string\" ? <CardDescription>{props.description}</CardDescription> : null}\n </CardHeader>\n ) : null}\n <CardContent>{children}</CardContent>\n </Card>\n ),\n },\n Stack: {\n description: \"Vertical stack for grouping child elements.\",\n slots: [\"default\"],\n props: z.object({ gap: z.enum([\"sm\", \"md\", \"lg\"]).optional() }),\n component: ({ props, children }) => <div className={props.gap === \"sm\" ? \"space-y-2\" : props.gap === \"lg\" ? \"space-y-6\" : \"space-y-4\"}>{children}</div>,\n },\n Grid: {\n description: \"Responsive grid layout for child elements.\",\n slots: [\"default\"],\n props: z.object({ columns: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]).optional() }),\n component: ({ props, children }) => {\n const columns = props.columns === 3 ? \"lg:grid-cols-3\" : props.columns === 4 ? \"lg:grid-cols-4\" : props.columns === 1 ? \"grid-cols-1\" : \"lg:grid-cols-2\"\n return <div className={`grid gap-4 ${columns}`}>{children}</div>\n },\n },\n Text: {\n description: \"Plain text block.\",\n props: z.object({ text: z.string(), tone: z.enum([\"default\", \"muted\"]).optional() }),\n component: ({ props }) => <p className={props.tone === \"muted\" ? \"text-sm text-muted-foreground\" : \"text-sm text-foreground\"}>{String(props.text)}</p>,\n },\n Badge: {\n description: \"Small status badge.\",\n props: z.object({ label: z.string(), variant: z.enum([\"default\", \"secondary\", \"outline\"]).optional() }),\n component: ({ props }) => <Badge variant={(props.variant as \"default\" | \"secondary\" | \"outline\" | undefined) ?? \"secondary\"}>{String(props.label)}</Badge>,\n },\n Alert: {\n description: \"Notice block for warnings, status, or context.\",\n props: z.object({ title: z.string(), description: z.string().optional() }),\n component: ({ props }) => (\n <div className=\"rounded-lg border border-border bg-muted/40 p-3 text-sm\">\n <div className=\"font-medium text-foreground\">{String(props.title)}</div>\n {typeof props.description === \"string\" ? <div className=\"mt-1 text-muted-foreground\">{props.description}</div> : null}\n </div>\n ),\n },\n Button: {\n description: \"Button that emits its press event to a configured action.\",\n props: z.object({ label: z.string() }),\n component: ({ props, emit }) => <Button size=\"sm\" onClick={() => emit(\"press\")}>{String(props.label)}</Button>,\n },\n },\n})\n\nexport function mergeGeneratedPaneProfiles(...profiles: GeneratedPaneProfile[]): GeneratedPaneProfile {\n const [first, ...rest] = profiles\n return {\n id: first?.id ?? \"generated-pane\",\n label: first?.label ?? \"Generated Pane\",\n components: Object.assign({}, ...profiles.map((profile) => profile.components)),\n actions: Object.assign({}, ...profiles.map((profile) => profile.actions ?? {})),\n }\n}\n\nexport function createGeneratedPaneCatalog(profile: GeneratedPaneProfile) {\n return defineCatalog(schema, {\n components: Object.fromEntries(Object.entries(profile.components).map(([name, definition]) => [name, {\n props: definition.props,\n slots: definition.slots ?? [],\n description: definition.description,\n }])),\n actions: Object.fromEntries(Object.entries(profile.actions ?? {}).map(([name, definition]) => [name, {\n params: definition.params,\n description: definition.description,\n }])),\n })\n}\n\nexport function GeneratedPaneRenderer({ spec, profile }: { spec: GeneratedPaneSpec; profile?: GeneratedPaneProfile }) {\n const activeProfile = profile ? mergeGeneratedPaneProfiles(baseGeneratedPaneProfile, profile) : baseGeneratedPaneProfile\n const catalog = createGeneratedPaneCatalog(activeProfile)\n const normalizedElements = Object.fromEntries(Object.entries(spec.elements).map(([id, element]) => [id, {\n ...element,\n props: element.props ?? {},\n children: element.children ?? [],\n visible: \"visible\" in element ? element.visible : true,\n }]))\n const validation = catalog.validate({ root: spec.root, elements: normalizedElements })\n if (!validation.success) {\n return <div className=\"rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive\">Invalid generated pane spec: {validation.error?.issues.slice(0, 3).map((issue) => issue.message).join(\" • \")}</div>\n }\n if (!validation.data) {\n return <div className=\"rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive\">Invalid generated pane spec</div>\n }\n const { registry } = defineRegistry(catalog, {\n components: Object.fromEntries(Object.entries(activeProfile.components).map(([name, definition]) => [name, ({ props, children, emit }: { props: Record<string, unknown>; children?: ReactNode; emit: (event: string) => void }) => definition.component({ props, children, emit })])),\n actions: {},\n })\n return (\n <JSONUIProvider registry={registry} handlers={{}}>\n <Renderer spec={validation.data as never} registry={registry} />\n </JSONUIProvider>\n )\n}\n","import { useEffect, useMemo, useState } from \"react\"\nimport { FileJson2, PanelTop, RefreshCw } from \"lucide-react\"\nimport { Badge, EmptyState, IconButton } from \"@hachej/boring-ui-kit\"\nimport { useApiBaseUrl, useWorkspaceRequestId } from \"@hachej/boring-workspace\"\nimport type { PaneProps } from \"@hachej/boring-workspace/plugin\"\nimport { GENERATED_PANE_PANEL_ID } from \"./constants\"\n\nexport interface GeneratedPaneExplorerConfig {\n title?: string\n patterns?: string[]\n panelId?: string\n itemLabel?: string\n emptyTitle?: string\n emptyDescription?: string\n}\n\ninterface PaneSearchState {\n loading: boolean\n error?: string\n paths: string[]\n}\n\ninterface PaneSearchResponse {\n results?: string[]\n}\n\nconst DEFAULT_PATTERNS = [\"**/*.pane.json\"]\n\nfunction titleFromPath(path: string): string {\n const file = path.split(\"/\").pop() ?? path\n return file\n .replace(/\\.(pane|dashboard)\\.json$/i, \"\")\n .replace(/[-_]+/g, \" \")\n}\n\nfunction matchesQuery(path: string, query: string | undefined): boolean {\n const value = query?.trim().toLowerCase()\n if (!value) return true\n return path.toLowerCase().includes(value) || titleFromPath(path).toLowerCase().includes(value)\n}\n\nfunction dedupeAndSort(values: string[]): string[] {\n return [...new Set(values)].sort((a, b) => a.localeCompare(b))\n}\n\nexport function createGeneratedPaneExplorerPane(config: GeneratedPaneExplorerConfig = {}) {\n return function GeneratedPaneExplorerPaneWithConfig(props: PaneProps<{ searchQuery?: string }>) {\n return <GeneratedPaneExplorerPane {...props} config={config} />\n }\n}\n\nexport function GeneratedPaneExplorerPane({ params, containerApi, config = {} }: PaneProps<{ searchQuery?: string }> & { config?: GeneratedPaneExplorerConfig }) {\n const apiBaseUrl = useApiBaseUrl()\n const workspaceId = useWorkspaceRequestId()\n const patterns = config.patterns?.length ? config.patterns : DEFAULT_PATTERNS\n const panelId = config.panelId ?? GENERATED_PANE_PANEL_ID\n const title = config.title ?? \"Panes\"\n const itemLabel = config.itemLabel ?? \"Generated pane\"\n const [state, setState] = useState<PaneSearchState>({ loading: true, paths: [] })\n const [refreshKey, setRefreshKey] = useState(0)\n\n useEffect(() => {\n const controller = new AbortController()\n setState((prev) => ({ ...prev, loading: true, error: undefined }))\n void Promise.all(patterns.map(async (pattern) => {\n const response = await fetch(`${apiBaseUrl}/api/v1/files/search?q=${encodeURIComponent(pattern)}&limit=500`, {\n signal: controller.signal,\n credentials: \"include\",\n headers: workspaceId ? { \"x-boring-workspace-id\": workspaceId } : {},\n })\n if (!response.ok) throw new Error(`Pane search failed for ${pattern} with HTTP ${response.status}`)\n const body = await response.json() as PaneSearchResponse\n return body.results ?? []\n }))\n .then((groups) => {\n setState({ loading: false, paths: dedupeAndSort(groups.flat()) })\n })\n .catch((error) => {\n if (controller.signal.aborted) return\n setState({ loading: false, paths: [], error: error instanceof Error ? error.message : String(error) })\n })\n return () => controller.abort()\n }, [apiBaseUrl, patterns.join(\"\\n\"), refreshKey, workspaceId])\n\n const paths = useMemo(\n () => state.paths.filter((path) => matchesQuery(path, params?.searchQuery)),\n [params?.searchQuery, state.paths],\n )\n\n const openPane = (path: string) => {\n containerApi.addPanel({\n id: `${panelId}:${path}`,\n component: panelId,\n title: titleFromPath(path),\n params: { path },\n })\n }\n\n return (\n <div className=\"flex h-full min-h-0 flex-col text-sm\">\n <div className=\"flex items-center justify-between border-b border-border/60 px-3 py-2\">\n <div className=\"flex min-w-0 items-center gap-2\">\n <PanelTop className=\"h-4 w-4 text-muted-foreground\" />\n <span className=\"truncate font-medium\">{title}</span>\n <Badge variant=\"secondary\">{state.paths.length}</Badge>\n </div>\n <IconButton\n type=\"button\"\n aria-label={`Refresh ${title.toLowerCase()}`}\n variant=\"ghost\"\n size=\"icon-xs\"\n onClick={() => setRefreshKey((value) => value + 1)}\n disabled={state.loading}\n >\n <RefreshCw className={`h-3.5 w-3.5 ${state.loading ? \"animate-spin\" : \"\"}`} />\n </IconButton>\n </div>\n\n <div className=\"min-h-0 flex-1 overflow-auto p-2\">\n {state.error ? (\n <EmptyState title={`Could not list ${title.toLowerCase()}`} description={state.error} />\n ) : state.loading ? (\n <div className=\"px-2 py-3 text-xs text-muted-foreground\">Scanning {title.toLowerCase()}…</div>\n ) : paths.length === 0 ? (\n <EmptyState\n title={config.emptyTitle ?? `No ${title.toLowerCase()} found`}\n description={config.emptyDescription ?? `Create ${patterns.join(\" or \")} files to list them here.`}\n />\n ) : (\n <div className=\"space-y-1\">\n {paths.map((path) => (\n <button\n key={path}\n type=\"button\"\n onClick={() => openPane(path)}\n className=\"flex w-full min-w-0 items-start gap-2 rounded-lg px-2 py-2 text-left hover:bg-background/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40\"\n >\n <FileJson2 className=\"mt-0.5 h-4 w-4 shrink-0 text-muted-foreground\" />\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-[13px] font-medium text-foreground\">{titleFromPath(path)}</span>\n <span className=\"block truncate text-[11px] text-muted-foreground\">{path}</span>\n </span>\n <Badge variant=\"outline\" className=\"mt-0.5 shrink-0 text-[10px]\">{itemLabel}</Badge>\n </button>\n ))}\n </div>\n )}\n </div>\n </div>\n )\n}\n","export const GENERATED_PANE_PANEL_ID = \"generated-pane.panel\"\nexport const GENERATED_PANE_EXPLORER_LEFT_TAB_ID = \"generated-pane.explorer\"\n","import {\n WORKSPACE_OPEN_PATH_SURFACE_KIND,\n type BoringFrontSurfaceResolverRegistration,\n} from \"@hachej/boring-workspace/plugin\"\nimport { GENERATED_PANE_PANEL_ID } from \"./constants\"\n\nexport function isGeneratedPanePath(path: string): boolean {\n return /(^|\\/)panes\\/[^/].*\\.pane\\.json$/i.test(path) || /\\.pane\\.json$/i.test(path)\n}\n\nfunction titleFromPath(path: string): string {\n const file = path.split(\"/\").pop() ?? path\n return file.replace(/\\.pane\\.json$/i, \"\").replace(/[-_]+/g, \" \")\n}\n\nexport const generatedPaneSurfaceResolver: BoringFrontSurfaceResolverRegistration = {\n id: \"generated-pane.open-path\",\n kind: WORKSPACE_OPEN_PATH_SURFACE_KIND,\n source: \"app\",\n resolve: (request) => {\n if (request.kind !== WORKSPACE_OPEN_PATH_SURFACE_KIND) return null\n const target = String(request.target ?? \"\")\n if (!isGeneratedPanePath(target)) return null\n return {\n component: GENERATED_PANE_PANEL_ID,\n title: titleFromPath(target),\n params: { path: target },\n score: 110,\n }\n },\n}\n"],"mappings":"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","names":["PanelTop","Badge","jsx","jsxs","Badge","useEffect","useState","Badge","EmptyState","useApiBaseUrl","useWorkspaceRequestId","jsx","jsxs","useApiBaseUrl","useWorkspaceRequestId","useState","useEffect","Badge","EmptyState","titleFromPath","PanelTop"]}
@@ -0,0 +1,22 @@
1
+ interface GeneratedPaneElementSpec {
2
+ type: string;
3
+ props?: Record<string, unknown>;
4
+ children?: string[];
5
+ }
6
+ interface GeneratedPaneSpec {
7
+ kind: "boring.generated-pane";
8
+ version: 1;
9
+ profile?: string;
10
+ title?: string;
11
+ description?: string;
12
+ root: string;
13
+ elements: Record<string, GeneratedPaneElementSpec>;
14
+ queries?: Record<string, unknown>;
15
+ }
16
+ interface GeneratedPaneValidationResult {
17
+ spec: GeneratedPaneSpec | null;
18
+ errors: string[];
19
+ }
20
+ declare function parseGeneratedPaneSpec(value: unknown): GeneratedPaneValidationResult;
21
+
22
+ export { type GeneratedPaneElementSpec, type GeneratedPaneSpec, type GeneratedPaneValidationResult, parseGeneratedPaneSpec };
@@ -0,0 +1,7 @@
1
+ import {
2
+ parseGeneratedPaneSpec
3
+ } from "../chunk-4IMMGGBQ.js";
4
+ export {
5
+ parseGeneratedPaneSpec
6
+ };
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,98 @@
1
+ # Generated pane plugin backed by json-render
2
+
3
+ ## Decision
4
+
5
+ Create a first-party `@hachej/boring-generated-pane` plugin that gives any workspace a safe, agent-authored UI pane system. It uses Vercel Labs `json-render` as the rendering/catalog substrate and keeps boring-ui as the workspace/security boundary.
6
+
7
+ ## Goals
8
+
9
+ - Agents generate JSON pane specs, never React/JS.
10
+ - Child apps and plugins define distinct component catalogs.
11
+ - Specs validate against the active catalog before render.
12
+ - BI dashboard is a `profile: "bi-dashboard"` component pack on top of generated-pane, not a parallel `bsl.dashboard` UI DSL.
13
+ - Plugin examples/evals/playground live with the plugin, not in the generic workspace playground.
14
+
15
+ ## Non-goals
16
+
17
+ - Do not make generated-pane a default workspace-playground plugin.
18
+ - Do not make `bsl.dashboard` the primary dashboard format unless BSL later needs a portable external contract.
19
+ - Do not execute generated functions, inline JavaScript, or arbitrary action handlers from specs.
20
+
21
+ ## Spec shape
22
+
23
+ ```json
24
+ {
25
+ "kind": "boring.generated-pane",
26
+ "version": 1,
27
+ "profile": "bi-dashboard",
28
+ "title": "Revenue Overview",
29
+ "root": "main",
30
+ "elements": {
31
+ "main": { "type": "DashboardGrid", "props": { "columns": 12 }, "children": ["chart"] },
32
+ "chart": { "type": "BSLChart", "props": { "queryId": "revenue", "chartType": "line" } }
33
+ },
34
+ "queries": {}
35
+ }
36
+ ```
37
+
38
+ ## Architecture
39
+
40
+ ```txt
41
+ @hachej/boring-generated-pane
42
+ - owns boring.generated-pane envelope validation
43
+ - wraps @json-render/core + @json-render/react
44
+ - exposes defineGeneratedPaneProfile(...)
45
+ - provides safe base components
46
+ - provides generic GeneratedPanePane/GeneratedPaneRenderer
47
+
48
+ @hachej/boring-bi-dashboard
49
+ - depends on generated-pane
50
+ - contributes a bi-dashboard profile/catalog:
51
+ DashboardGrid, BSLMetric, BSLChart, BSLPerspectiveViewer, BSLFilter, BSLText
52
+ - owns dashboard query loading and data bridge binding
53
+ - keeps dashboard examples/evals/playground under plugins/bi-dashboard
54
+ ```
55
+
56
+ ## Safety boundary
57
+
58
+ - `json-render` validates the renderable `root/elements` tree against the catalog.
59
+ - Boring validates the outer envelope (`kind`, `version`, `profile`, acyclic ids, plugin-specific query invariants).
60
+ - Component implementations are real React components authored by trusted plugins/apps.
61
+ - Generated specs cannot import modules or execute code.
62
+ - Future action components must route through explicit WorkspaceBridge allowlists.
63
+
64
+ ## Child app extension model
65
+
66
+ A child app defines a profile/catalog with its own components, for example:
67
+
68
+ ```ts
69
+ defineGeneratedPaneProfile({
70
+ id: "seneca",
71
+ label: "Seneca",
72
+ components: {
73
+ StudentCard,
74
+ AssignmentList,
75
+ GradeTrendChart,
76
+ },
77
+ })
78
+ ```
79
+
80
+ The agent sees only the active catalog/profile instructions, and the renderer rejects unknown components/props.
81
+
82
+ ## Implementation steps
83
+
84
+ 1. Add `plugins/generated-pane` package with json-render dependencies, base profile, renderer, generic panel, skill, example, and playground docs.
85
+ 2. Convert BI dashboard specs from `kind: bsl.dashboard` + `components` to `kind: boring.generated-pane`, `profile: bi-dashboard`, and `elements`.
86
+ 3. Replace BI dashboard's hand-rolled recursive renderer with `GeneratedPaneRenderer` plus a dashboard profile.
87
+ 4. Keep BI dashboard query/data bridge logic in BI dashboard; generated-pane remains domain-neutral.
88
+ 5. Keep all examples/evals/playground helpers inside plugin folders.
89
+ 6. Run focused package gates for generated-pane, data-bridge, and bi-dashboard.
90
+
91
+ ## Acceptance
92
+
93
+ - PR diff remains plugin-scoped except lockfile.
94
+ - `@hachej/boring-generated-pane` typechecks/builds.
95
+ - BI dashboard renders through generated-pane/json-render.
96
+ - BI dashboard sample/tests use `boring.generated-pane` + `profile: bi-dashboard` + `elements`.
97
+ - No workspace-playground default plugin wiring is added.
98
+ - Thermo review finds no structural blocker.
@@ -0,0 +1,12 @@
1
+ *
2
+ !.gitignore
3
+ !README.md
4
+ !panes/
5
+ !panes/**
6
+ !eval/
7
+ !eval/**
8
+ !.pi/
9
+ !.pi/extensions/
10
+ !.pi/extensions/generated-pane/
11
+ !.pi/extensions/generated-pane/**
12
+ .pi/extensions/generated-pane/.boring-signature.json
@@ -0,0 +1 @@
1
+ export { default } from "@hachej/boring-generated-pane/front"
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "generated-pane-demo-extension",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "boring": {
7
+ "id": "generated-pane",
8
+ "label": "Generated Pane Demo",
9
+ "front": "front/index.ts"
10
+ }
11
+ }
@@ -0,0 +1,5 @@
1
+ # Generated pane demo workspace
2
+
3
+ This fixture contains a valid generated pane spec under `panes/` and is used by the plugin-local playground/eval runner.
4
+
5
+ It also contains `.pi/extensions/generated-pane`, a tiny workspace-local front extension that re-exports `@hachej/boring-generated-pane/front` for manual browser testing with external plugins enabled.
@@ -0,0 +1,24 @@
1
+ # Eval suite: generic generated-pane authoring.
2
+ #
3
+ # Run from the repo root:
4
+ # pnpm --filter @hachej/boring-generated-pane playground:eval
5
+
6
+ model:
7
+ provider: openai-codex
8
+ id: gpt-5.5
9
+
10
+ defaults:
11
+ retries: 1
12
+ timeoutMs: 180000
13
+
14
+ prompts:
15
+ - prompt: Can you make me a launch readiness view for this workspace? I want to open it as a pane later.
16
+ expect:
17
+ - tool: write
18
+ params:
19
+ path: !EvalRegex 'panes/.+\.pane\.json'
20
+ content: !EvalRegex '"kind"\s*:\s*"boring\.generated-pane"'
21
+ - tool: write
22
+ params:
23
+ path: !EvalRegex 'panes/.+\.pane\.json'
24
+ content: !EvalRegex '"elements"\s*:'
@@ -0,0 +1,23 @@
1
+ {
2
+ "kind": "boring.generated-pane",
3
+ "version": 1,
4
+ "profile": "base",
5
+ "title": "Project Status",
6
+ "description": "Example generated pane rendered from a safe json-render catalog.",
7
+ "root": "main",
8
+ "elements": {
9
+ "main": {
10
+ "type": "Card",
11
+ "props": { "title": "Launch readiness", "description": "Generated from JSON, not React code." },
12
+ "children": ["stack"]
13
+ },
14
+ "stack": {
15
+ "type": "Stack",
16
+ "props": { "gap": "md" },
17
+ "children": ["badge", "summary", "notice"]
18
+ },
19
+ "badge": { "type": "Badge", "props": { "label": "On track", "variant": "secondary" } },
20
+ "summary": { "type": "Text", "props": { "text": "The generated-pane plugin validates this spec against a component catalog before rendering." } },
21
+ "notice": { "type": "Alert", "props": { "title": "Safe by default", "description": "Unknown components, props, and actions are rejected." } }
22
+ }
23
+ }
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@hachej/boring-generated-pane",
3
+ "version": "0.1.60",
4
+ "type": "module",
5
+ "private": false,
6
+ "license": "MIT",
7
+ "description": "Generated pane runtime for Boring workspace using json-render catalogs and safe WorkspaceBridge-backed actions.",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/hachej/boring-ui"
11
+ },
12
+ "homepage": "https://github.com/hachej/boring-ui",
13
+ "boring": {
14
+ "id": "generated-pane",
15
+ "label": "Generated Pane",
16
+ "front": "dist/front/index.js",
17
+ "server": false
18
+ },
19
+ "pi": {
20
+ "skills": [
21
+ "skills/generated-pane-authoring"
22
+ ],
23
+ "systemPrompt": "Use the generated-pane-authoring skill when the user asks to create a custom workspace pane from available components. Pane files should live under panes/*.pane.json."
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "skills",
28
+ "example",
29
+ "playground",
30
+ "docs",
31
+ "README.md"
32
+ ],
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/front/index.d.ts",
36
+ "import": "./dist/front/index.js"
37
+ },
38
+ "./front": {
39
+ "types": "./dist/front/index.d.ts",
40
+ "import": "./dist/front/index.js"
41
+ },
42
+ "./shared": {
43
+ "types": "./dist/shared/index.d.ts",
44
+ "import": "./dist/shared/index.js"
45
+ },
46
+ "./package.json": "./package.json"
47
+ },
48
+ "sideEffects": false,
49
+ "peerDependencies": {
50
+ "react": "^18.0.0 || ^19.0.0",
51
+ "react-dom": "^18.0.0 || ^19.0.0",
52
+ "@hachej/boring-workspace": "0.1.60"
53
+ },
54
+ "dependencies": {
55
+ "@json-render/core": "^0.19.0",
56
+ "@json-render/react": "^0.19.0",
57
+ "lucide-react": "^1.8.0",
58
+ "zod": "^4.3.6",
59
+ "@hachej/boring-ui-kit": "0.1.60"
60
+ },
61
+ "devDependencies": {
62
+ "tsx": "^4.21.0",
63
+ "@types/react": "^19.0.0",
64
+ "@types/react-dom": "^19.0.0",
65
+ "react": "^19.0.0",
66
+ "react-dom": "^19.0.0",
67
+ "tsup": "^8.4.0",
68
+ "typescript": "~5.9.3",
69
+ "vitest": "^3.2.6",
70
+ "@hachej/boring-agent": "0.1.60",
71
+ "@hachej/boring-workspace": "0.1.60"
72
+ },
73
+ "scripts": {
74
+ "build": "tsup",
75
+ "typecheck": "tsc --noEmit",
76
+ "test": "vitest run --passWithNoTests",
77
+ "playground:eval": "tsx playground/run-eval.ts",
78
+ "lint": "pnpm run typecheck",
79
+ "clean": "rm -rf dist .tsbuildinfo"
80
+ }
81
+ }
@@ -0,0 +1,20 @@
1
+ # Generated pane playground
2
+
3
+ Run this plugin in the existing workspace playground without making it a default playground plugin. The demo workspace includes `.pi/extensions/generated-pane`, which loads the plugin as a workspace-local front extension:
4
+
5
+ ```bash
6
+ pnpm --filter @hachej/boring-generated-pane build
7
+ BORING_EXTERNAL_PLUGINS=1 \
8
+ BORING_AGENT_WORKSPACE_ROOT="$PWD/plugins/generated-pane/example" \
9
+ pnpm --filter workspace-playground dev
10
+ ```
11
+
12
+ Open `panes/project-status.pane.json` with the generated-pane panel.
13
+
14
+ ## Eval playground
15
+
16
+ Run the authoring eval and validate that the agent wrote parseable `boring.generated-pane` JSON:
17
+
18
+ ```bash
19
+ pnpm --filter @hachej/boring-generated-pane playground:eval
20
+ ```
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env -S tsx
2
+ import { cpSync, mkdtempSync } from "node:fs"
3
+ import { tmpdir } from "node:os"
4
+ import { dirname, join, resolve } from "node:path"
5
+ import { fileURLToPath } from "node:url"
6
+ import { runEvalSuite, type SuiteReport } from "@hachej/boring-agent/eval"
7
+ import { createWorkspaceAgentServer } from "@hachej/boring-workspace/app/server"
8
+ import { parseGeneratedPaneSpec } from "../src/shared/index"
9
+
10
+ const __filename = fileURLToPath(import.meta.url)
11
+ const __dirname = dirname(__filename)
12
+ const PLUGIN_ROOT = resolve(__dirname, "..")
13
+ const EXAMPLE_ROOT = resolve(PLUGIN_ROOT, "example")
14
+ const DEFAULT_EVAL = resolve(EXAMPLE_ROOT, "eval/generated-pane.yaml")
15
+
16
+ function seedWorkspace(): string {
17
+ const root = mkdtempSync(join(tmpdir(), "generated-pane-playground-"))
18
+ cpSync(EXAMPLE_ROOT, root, { recursive: true })
19
+ return root
20
+ }
21
+
22
+ async function main(): Promise<number> {
23
+ const fixturesPath = resolve(process.argv[2] ?? DEFAULT_EVAL)
24
+ const workspaceRoot = seedWorkspace()
25
+ console.log(`[generated-pane playground] running suite: ${fixturesPath}`)
26
+ console.log(`[generated-pane playground] seeded workspace: ${workspaceRoot}`)
27
+
28
+ const app = await createWorkspaceAgentServer({
29
+ workspaceRoot,
30
+ appRoot: PLUGIN_ROOT,
31
+ mode: "local",
32
+ logger: false,
33
+ defaultPluginPackages: ["@hachej/boring-generated-pane"],
34
+ })
35
+
36
+ try {
37
+ const report = await runEvalSuite({ app, fixturesPath, concurrency: 1 })
38
+ console.log(
39
+ `[generated-pane playground] ${report.passed}/${report.total} passed (${(report.passRate * 100).toFixed(1)}%) in ${(report.totalDurationMs / 1000).toFixed(1)}s`,
40
+ )
41
+
42
+ const validationErrors = validateWrittenPanes(report)
43
+ if (validationErrors.length > 0) {
44
+ console.error("\n[generated-pane playground] generated pane validation failed:")
45
+ for (const error of validationErrors) console.error(` - ${error}`)
46
+ return 1
47
+ }
48
+
49
+ if (!report.allPassed) {
50
+ console.error(`\n[generated-pane playground] ${report.failed} prompt(s) failed:`)
51
+ for (const r of report.results) {
52
+ if (r.ok) continue
53
+ console.error(`\n prompt: ${r.prompt}`)
54
+ console.error(` reason: ${r.reason ?? "(no reason)"}`)
55
+ if (r.actual.length > 0) {
56
+ console.error(` actual calls: ${JSON.stringify(r.actual, null, 2).replace(/\n/g, "\n ")}`)
57
+ }
58
+ if (r.text) {
59
+ console.error(` text: ${r.text.slice(0, 200)}${r.text.length > 200 ? "…" : ""}`)
60
+ }
61
+ }
62
+ return 1
63
+ }
64
+
65
+ return 0
66
+ } finally {
67
+ await app.close()
68
+ }
69
+ }
70
+
71
+ function validateWrittenPanes(report: SuiteReport): string[] {
72
+ const errors: string[] = []
73
+ for (const result of report.results) {
74
+ for (const call of result.actual) {
75
+ if (call.tool !== "write") continue
76
+ const path = typeof call.params.path === "string" ? call.params.path : ""
77
+ if (!/\.pane\.json$/i.test(path)) continue
78
+ if (typeof call.params.content !== "string") {
79
+ errors.push(`${path || "pane write"}: content is not a string`)
80
+ continue
81
+ }
82
+ try {
83
+ const parsed = parseGeneratedPaneSpec(JSON.parse(call.params.content))
84
+ if (!parsed.spec) errors.push(`${path}: ${parsed.errors.join("; ")}`)
85
+ } catch (error) {
86
+ errors.push(`${path}: invalid JSON (${error instanceof Error ? error.message : String(error)})`)
87
+ }
88
+ }
89
+ }
90
+ return errors
91
+ }
92
+
93
+ main().then(
94
+ (code) => process.exit(code),
95
+ (err) => {
96
+ console.error("[generated-pane playground] fatal:", err)
97
+ process.exit(2)
98
+ },
99
+ )
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: generated-pane-authoring
3
+ description: Create and edit boring.generated-pane JSON specs from predefined safe component catalogs.
4
+ ---
5
+
6
+ # Generated Pane Authoring
7
+
8
+ Use when the user asks for a custom pane, dashboard, report, workflow UI, status view, or other workspace UI composed from predefined components.
9
+
10
+ Write JSON specs to `panes/*.pane.json` unless a profile/plugin says otherwise.
11
+
12
+ Required root shape:
13
+
14
+ ```json
15
+ {
16
+ "kind": "boring.generated-pane",
17
+ "version": 1,
18
+ "profile": "base",
19
+ "title": "Short title",
20
+ "root": "main",
21
+ "elements": {
22
+ "main": { "type": "Card", "props": { "title": "Overview" }, "children": [] }
23
+ }
24
+ }
25
+ ```
26
+
27
+ Rules:
28
+
29
+ - Do not generate React, JavaScript, functions, or inline event handlers.
30
+ - Use only components documented by the active profile/catalog.
31
+ - Use only props documented for each component.
32
+ - Keep element ids stable and descriptive.
33
+ - Prefer a simple tree: one root layout, then cards/sections/widgets.
34
+ - Actions must reference catalog-approved actions only.