@hachej/boring-ui-plugin-cli 0.1.31 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,105 @@
1
+ interface CreatePluginOptions {
2
+ name: string;
3
+ path?: string;
4
+ cwd?: string;
5
+ }
6
+ interface CreatePluginResult {
7
+ id: string;
8
+ pluginDir: string;
9
+ packageName: string;
10
+ filesCreated: string[];
11
+ }
12
+ declare function createPlugin(options: CreatePluginOptions): CreatePluginResult;
13
+
14
+ interface ScaffoldPluginOptions {
15
+ /** Plugin id — must be kebab-case, used as folder name + npm package name. */
16
+ name: string;
17
+ /** Workspace root the .pi/extensions/<name>/ folder is created under. */
18
+ workspaceRoot: string;
19
+ /** Optional override for the canonical templates dir (test escape hatch). */
20
+ templatesDir?: string;
21
+ }
22
+ interface ScaffoldPluginResult {
23
+ pluginDir: string;
24
+ filesCreated: string[];
25
+ }
26
+ declare function scaffoldPlugin(opts: ScaffoldPluginOptions): ScaffoldPluginResult;
27
+
28
+ interface VerifyPluginOptions {
29
+ /** Workspace root containing `.pi/extensions/`. */
30
+ workspaceRoot: string;
31
+ /** Optional plugin name to verify only one; otherwise verifies all. */
32
+ name?: string;
33
+ }
34
+ interface PluginVerifyOutcome {
35
+ id: string;
36
+ dir: string;
37
+ ok: boolean;
38
+ errors: string[];
39
+ /**
40
+ * Non-fatal informational messages — e.g. "this plugin declares
41
+ * boring.server, which is boot-time/static composition only and is
42
+ * not hot-registered by /reload". Does NOT flip `ok` to false;
43
+ * boring-ui-plugin verify is about manifest validity, not activation.
44
+ */
45
+ warnings: string[];
46
+ }
47
+ interface VerifyPluginResult {
48
+ outcomes: PluginVerifyOutcome[];
49
+ ok: boolean;
50
+ /** Absolute path of the `.pi/extensions/` dir that was scanned. */
51
+ extensionsDir: string;
52
+ /** True when `.pi/extensions/` doesn't exist at all under the workspace. */
53
+ extensionsDirMissing: boolean;
54
+ }
55
+ /**
56
+ * Verify boring-ui plugins on disk WITHOUT a running workspace server.
57
+ * Runs the same manifest validator the asset manager runs, plus file-
58
+ * existence checks for `boring.front` / `boring.server` / `pi.extensions`
59
+ * paths. Designed to catch the common authoring mistakes (invalid
60
+ * `boring.server` value, missing front file, malformed manifest, etc.)
61
+ * in the same agent turn that wrote the files.
62
+ *
63
+ * Does NOT execute plugin code (no jiti, no Vite). Syntax errors in
64
+ * front/Pi modules only surface when the workspace's real /reload runs;
65
+ * declared `boring.server` files require static composition plus a
66
+ * process restart to execute.
67
+ */
68
+ declare function verifyPlugin(opts: VerifyPluginOptions): VerifyPluginResult;
69
+ declare function formatVerifyResult(result: VerifyPluginResult): string;
70
+ declare function findHintForError(message: string): string | undefined;
71
+
72
+ type PaneSelfTestState = "ready" | "loading" | "error" | "missing" | "timeout" | "no-browser-connected";
73
+ interface SelfTestEvent {
74
+ code: string;
75
+ message: string;
76
+ }
77
+ interface SelfTestResult {
78
+ ok: boolean;
79
+ workspaceId?: string;
80
+ pluginId: string;
81
+ revision?: number;
82
+ reloadErrors: SelfTestEvent[];
83
+ pane: {
84
+ found: boolean;
85
+ state: PaneSelfTestState;
86
+ panelId: string;
87
+ panelInstanceId: string;
88
+ error?: SelfTestEvent;
89
+ lastReportedAt?: string;
90
+ };
91
+ }
92
+ interface RunPluginSelfTestOptions {
93
+ pluginId: string;
94
+ url?: string;
95
+ workspaceId?: string;
96
+ panelId?: string;
97
+ timeoutMs?: number;
98
+ }
99
+ declare function runPluginSelfTest(options: RunPluginSelfTestOptions): Promise<SelfTestResult>;
100
+ declare function formatSelfTestResult(result: SelfTestResult): string;
101
+
1
102
  declare function pluginCommandUsage(): string;
2
- declare function runBoringUiPluginCli(argv?: string[]): void;
103
+ declare function runBoringUiPluginCli(argv?: string[]): Promise<void>;
3
104
 
4
- export { pluginCommandUsage, runBoringUiPluginCli };
105
+ export { type CreatePluginOptions, type CreatePluginResult, type PaneSelfTestState, type PluginVerifyOutcome, type RunPluginSelfTestOptions, type ScaffoldPluginOptions, type ScaffoldPluginResult, type SelfTestEvent, type SelfTestResult, type VerifyPluginResult, createPlugin, findHintForError, formatSelfTestResult, formatVerifyResult, pluginCommandUsage, runBoringUiPluginCli, runPluginSelfTest, scaffoldPlugin, verifyPlugin };
package/dist/index.js CHANGED
@@ -1,8 +1,22 @@
1
1
  import {
2
+ createPlugin,
3
+ findHintForError,
4
+ formatSelfTestResult,
5
+ formatVerifyResult,
2
6
  pluginCommandUsage,
3
- runBoringUiPluginCli
4
- } from "./chunk-MNNNQ7Y7.js";
7
+ runBoringUiPluginCli,
8
+ runPluginSelfTest,
9
+ scaffoldPlugin,
10
+ verifyPlugin
11
+ } from "./chunk-DH4PSVGY.js";
5
12
  export {
13
+ createPlugin,
14
+ findHintForError,
15
+ formatSelfTestResult,
16
+ formatVerifyResult,
6
17
  pluginCommandUsage,
7
- runBoringUiPluginCli
18
+ runBoringUiPluginCli,
19
+ runPluginSelfTest,
20
+ scaffoldPlugin,
21
+ verifyPlugin
8
22
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-ui-plugin-cli",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "description": "Slim boring-ui plugin authoring CLI for workspace runtimes.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,11 +18,6 @@
18
18
  },
19
19
  "./package.json": "./package.json"
20
20
  },
21
- "scripts": {
22
- "build": "tsup",
23
- "typecheck": "tsc --noEmit",
24
- "test": "pnpm run build && vitest run"
25
- },
26
21
  "devDependencies": {
27
22
  "@types/node": "^22.15.3",
28
23
  "tsup": "^8.4.0",
@@ -33,5 +28,10 @@
33
28
  "type": "git",
34
29
  "url": "git+https://github.com/hachej/boring-ui.git"
35
30
  },
36
- "homepage": "https://github.com/hachej/boring-ui"
37
- }
31
+ "homepage": "https://github.com/hachej/boring-ui",
32
+ "scripts": {
33
+ "build": "tsup",
34
+ "typecheck": "tsc --noEmit",
35
+ "test": "pnpm run build && vitest run"
36
+ }
37
+ }
@@ -1,34 +1,150 @@
1
- // CANONICAL front/index.tsx for a boring-ui plugin.
1
+ // CANONICAL front/index.tsx for a boring-ui runtime plugin.
2
2
  // Copy this shape — replace <kebab-name> and <Label>.
3
3
  //
4
- // definePlugin takes a single DECLARATIVE config object. For
5
- // conditional registration or runtime branching that the declarative
6
- // fields can't express, use the optional `setup: (api) => void`
7
- // escape hatch it runs LAST, after every declarative field has
8
- // been registered.
4
+ // Structure rule:
5
+ // - Commands open full-size center panes for detailed work.
6
+ // - Left tabs are compact, persistent sidebar/navigator panes.
7
+ // - If a plugin has both, do NOT render the same component in both places.
8
+ // Register a small LeftPane and a separate MainPane.
9
9
 
10
10
  import React from "react"
11
- import { definePlugin } from "@hachej/boring-workspace/plugin"
11
+ import { definePlugin, type PaneProps } from "@hachej/boring-workspace/plugin"
12
+ import {
13
+ Badge,
14
+ Button,
15
+ Card,
16
+ CardContent,
17
+ CardDescription,
18
+ CardHeader,
19
+ CardTitle,
20
+ EmptyState,
21
+ Toolbar,
22
+ ToolbarGroup,
23
+ } from "@hachej/boring-ui-kit"
12
24
 
13
- function MyPane() {
14
- return <div style={{ padding: 16 }}>Hello from &lt;kebab-name&gt;</div>
25
+ const MAIN_PANEL_ID = "<kebab-name>.panel"
26
+ const LEFT_PANEL_ID = "<kebab-name>.left"
27
+
28
+ function MainPane() {
29
+ return (
30
+ <div className="flex h-full min-h-0 min-w-0 flex-col bg-background text-foreground">
31
+ <Toolbar className="border-b border-border px-3 py-2">
32
+ <ToolbarGroup>
33
+ <Badge variant="secondary">Runtime plugin</Badge>
34
+ <Badge variant="outline"><Label></Badge>
35
+ </ToolbarGroup>
36
+ <ToolbarGroup className="ml-auto">
37
+ <Button size="sm" variant="secondary">Refresh</Button>
38
+ </ToolbarGroup>
39
+ </Toolbar>
40
+
41
+ <div className="min-h-0 min-w-0 flex-1 overflow-auto p-4">
42
+ <div className="grid min-w-0 gap-4 lg:grid-cols-[minmax(0,1fr)_280px]">
43
+ <Card className="min-w-0">
44
+ <CardHeader>
45
+ <CardTitle><Label></CardTitle>
46
+ <CardDescription>
47
+ This is the full center pane. Put detailed views, tables, charts,
48
+ editors, previews, and multi-step workflows here.
49
+ </CardDescription>
50
+ </CardHeader>
51
+ <CardContent>
52
+ <EmptyState
53
+ title="Nothing to show yet"
54
+ description="Connect data, register a surface resolver, or add actions for this plugin."
55
+ actions={<Button size="sm">Primary action</Button>}
56
+ />
57
+ </CardContent>
58
+ </Card>
59
+
60
+ <Card className="min-w-0">
61
+ <CardHeader>
62
+ <CardTitle className="text-sm">Details</CardTitle>
63
+ <CardDescription>Use side cards for metadata or secondary controls.</CardDescription>
64
+ </CardHeader>
65
+ <CardContent>
66
+ <div className="space-y-2 text-sm text-muted-foreground">
67
+ <div className="flex items-center justify-between gap-3">
68
+ <span>Status</span>
69
+ <Badge variant="outline">Ready</Badge>
70
+ </div>
71
+ <div className="flex items-center justify-between gap-3">
72
+ <span>Items</span>
73
+ <span>0</span>
74
+ </div>
75
+ </div>
76
+ </CardContent>
77
+ </Card>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ )
82
+ }
83
+
84
+ function LeftPane({ containerApi }: PaneProps) {
85
+ const openMainPane = () => {
86
+ containerApi.addPanel({
87
+ id: `${MAIN_PANEL_ID}.from-left`,
88
+ component: MAIN_PANEL_ID,
89
+ title: "<Label>",
90
+ params: { source: "left-tab" },
91
+ })
92
+ }
93
+
94
+ return (
95
+ <div className="flex h-full min-h-0 min-w-0 flex-col bg-background text-foreground">
96
+ <div className="border-b border-border/60 px-3 py-2">
97
+ <div className="text-sm font-medium"><Label></div>
98
+ <div className="text-xs text-muted-foreground">Compact sidebar navigator</div>
99
+ </div>
100
+
101
+ <div className="min-h-0 min-w-0 flex-1 overflow-auto p-3">
102
+ <div className="space-y-3">
103
+ <Card className="min-w-0">
104
+ <CardHeader className="space-y-1 p-3">
105
+ <CardTitle className="text-sm">Overview</CardTitle>
106
+ <CardDescription className="text-xs">
107
+ Keep left tabs small: summary, filters, navigation, and actions.
108
+ </CardDescription>
109
+ </CardHeader>
110
+ <CardContent className="p-3 pt-0">
111
+ <Button size="sm" className="w-full" onClick={openMainPane}>
112
+ Open main pane
113
+ </Button>
114
+ </CardContent>
115
+ </Card>
116
+
117
+ <div className="space-y-1 text-xs text-muted-foreground">
118
+ <div className="rounded-md border border-border bg-card px-2 py-1.5">Recent item</div>
119
+ <div className="rounded-md border border-border bg-card px-2 py-1.5">Saved filter</div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ )
15
125
  }
16
126
 
17
127
  export default definePlugin({
18
128
  id: "<kebab-name>", // contribution namespace; matching package name is recommended
19
129
  label: "<Label>",
20
130
  panels: [
21
- { id: "<kebab-name>.panel", label: "<Label>", component: MyPane },
131
+ { id: MAIN_PANEL_ID, label: "<Label>", component: MainPane },
132
+ // Optional left-tab component. Register separately from the main panel so
133
+ // the sidebar does not duplicate a full workbench view.
134
+ { id: LEFT_PANEL_ID, label: "<Label>", component: LeftPane },
22
135
  ],
23
136
  commands: [
24
- { id: "<kebab-name>.open", title: "Open <Label>", panelId: "<kebab-name>.panel" },
137
+ { id: "<kebab-name>.open", title: "Open <Label>", panelId: MAIN_PANEL_ID },
25
138
  ],
26
- // Do not add leftTabs by default: left tabs are persistent sidebar
27
- // navigation. Use them only for always-on tools/catalogs that deserve a
28
- // permanent sidebar entry; file visualizers should use surfaceResolvers.
139
+ // Do not add leftTabs just because a plugin has a panel. A left tab is a
140
+ // permanent sidebar category. Keep it compact, and use its buttons/rows to
141
+ // open the full center panel via containerApi.addPanel(...).
142
+ //
143
+ // If this plugin should have persistent sidebar navigation, uncomment this:
29
144
  // leftTabs: [
30
- // { id: "<kebab-name>.tab", title: "<Label>", panelId: "<kebab-name>.panel" },
145
+ // { id: "<kebab-name>.tab", title: "<Label>", panelId: LEFT_PANEL_ID },
31
146
  // ],
147
+ //
32
148
  // File visualizer example: import WORKSPACE_OPEN_PATH_SURFACE_KIND from
33
149
  // "@hachej/boring-workspace/plugin", import useApiBaseUrl/useWorkspaceRequestId
34
150
  // from "@hachej/boring-workspace", set kind: WORKSPACE_OPEN_PATH_SURFACE_KIND,
@@ -43,6 +159,10 @@ export default definePlugin({
43
159
  // setup: (api) => { if (env.beta) api.registerPanel(betaPanel) },
44
160
  })
45
161
 
162
+ // Responsive pane rule: panels and left tabs live inside resizable dock regions.
163
+ // Avoid fixed large widths; prefer w-full/min-w-0 layouts and responsive chart
164
+ // wrappers such as Recharts ResponsiveContainer.
165
+ //
46
166
  // All available `definePlugin` config fields:
47
167
  // id (required, string)
48
168
  // label (optional, string)
@@ -0,0 +1,56 @@
1
+ # Plugin template
2
+
3
+ Reference shape for **app/internal publishable npm-package plugins**.
4
+ The plugin CLI copies this directory when you run:
5
+
6
+ ```sh
7
+ boring-ui-plugin create <your-name> --path plugins
8
+ cd plugins/<your-name>
9
+ pnpm install
10
+ pnpm typecheck && pnpm test
11
+ ```
12
+
13
+ > **Building a generated/runtime user plugin instead** (hot-reloadable,
14
+ > no build step, drops into a workspace's `.pi/extensions/<name>/`)?
15
+ > Don't copy this template and don't use `npx` from inside the agent
16
+ > runtime — run the workspace-local `boring-ui-plugin scaffold <name>`.
17
+
18
+ ## Shape
19
+
20
+ ```
21
+ plugins/<name>/
22
+ package.json private: true, workspace:* deps, nested exports map
23
+ tsconfig.json paths aliases into packages/workspace/src for fast iteration
24
+ tsup.config.ts nested entries (front/index, server/index, shared/index)
25
+ vitest.config.ts jsdom + @vitejs/plugin-react + globals: true
26
+ setupFiles: ./src/test-setup.ts
27
+ src/
28
+ front/
29
+ index.ts definePlugin({ ... }) entry and re-exports
30
+ panels.tsx
31
+ surfaceResolver.ts
32
+ __tests__/xxxPlugin.test.ts
33
+ server/
34
+ index.ts createXxxServerPlugin() and default server factory
35
+ shared/
36
+ constants.ts ids, surface kinds
37
+ types.ts param/option types
38
+ index.ts re-export
39
+ ```
40
+
41
+ ## What lives where
42
+
43
+ - **front/** — anything that runs in the browser shell: panels, command
44
+ contributions, surface resolvers, providers, bindings.
45
+ - **server/** — anything that runs in the agent backend: agent tools,
46
+ system prompt fragments, server hooks.
47
+ - **shared/** — constants and types used by both sides. Keep it tiny.
48
+ - **`src/test-setup.ts`** — jest-dom matchers, ResizeObserver + Range
49
+ polyfills, testing-library cleanup. Each plugin owns its own copy;
50
+ keep them in sync with `packages/plugin-cli/templates/plugin/src/test-setup.ts`
51
+ if the canonical setup changes.
52
+
53
+ ## Invariants
54
+
55
+ `packages/workspace/scripts/check-plugin-invariants.mjs` lints this
56
+ template (and the live plugins) for the plugin contract. Keep it valid.
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@hachej/boring-plugin-template",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "private": true,
6
+ "description": "Reference app/internal boring-ui package-plugin example using declarative definePlugin({ ... }) front contributions plus boot-time server integration. Generated .pi/extensions plugins should use boring-ui-plugin scaffold instead.",
7
+ "boring": {
8
+ "label": "Sample",
9
+ "front": "dist/front/index.js",
10
+ "server": "dist/server/index.js"
11
+ },
12
+ "pi": {
13
+ "systemPrompt": "Sample plugin template \u2014 replace this prompt with a short hint telling the agent when to use your plugin's panels/tools."
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/front/index.d.ts",
21
+ "import": "./dist/front/index.js"
22
+ },
23
+ "./front": {
24
+ "types": "./dist/front/index.d.ts",
25
+ "import": "./dist/front/index.js"
26
+ },
27
+ "./server": {
28
+ "types": "./dist/server/index.d.ts",
29
+ "import": "./dist/server/index.js"
30
+ },
31
+ "./shared": {
32
+ "types": "./dist/shared/index.d.ts",
33
+ "import": "./dist/shared/index.js"
34
+ },
35
+ "./package.json": "./package.json"
36
+ },
37
+ "sideEffects": false,
38
+ "scripts": {
39
+ "build": "tsup",
40
+ "typecheck": "pnpm --filter @hachej/boring-ui-kit build && pnpm --filter @hachej/boring-agent build && pnpm --filter @hachej/boring-workspace build && tsc --noEmit",
41
+ "test": "pnpm --filter @hachej/boring-ui-kit build && vitest run --passWithNoTests",
42
+ "lint": "pnpm run typecheck",
43
+ "clean": "rm -rf dist .tsbuildinfo"
44
+ },
45
+ "peerDependencies": {
46
+ "@hachej/boring-workspace": "workspace:*",
47
+ "react": "^18.0.0 || ^19.0.0",
48
+ "react-dom": "^18.0.0 || ^19.0.0"
49
+ },
50
+ "dependencies": {
51
+ "@hachej/boring-ui-kit": "workspace:*"
52
+ },
53
+ "devDependencies": {
54
+ "@hachej/boring-workspace": "workspace:*",
55
+ "@testing-library/jest-dom": "^6.9.1",
56
+ "@testing-library/react": "^16.3.2",
57
+ "@types/react": "^19.0.0",
58
+ "@types/react-dom": "^19.0.0",
59
+ "@vitejs/plugin-react": "^4.0.0",
60
+ "jsdom": "^29.0.2",
61
+ "react": "^19.0.0",
62
+ "react-dom": "^19.0.0",
63
+ "tsup": "^8.0.0",
64
+ "typescript": "^5.4.0",
65
+ "vitest": "^3.1.3"
66
+ }
67
+ }
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it, vi } from "vitest"
2
+ import samplePlugin, { SAMPLE_PANEL_ID, SamplePanel } from "../index"
3
+ import { SAMPLE_PLUGIN_ID, SAMPLE_SURFACE_KIND } from "../../shared/constants"
4
+
5
+ describe("samplePlugin (BoringFrontFactory)", () => {
6
+ it("registers a panel, a panel command, and a surface resolver", async () => {
7
+ const registerPanel = vi.fn()
8
+ const registerPanelCommand = vi.fn()
9
+ const registerSurfaceResolver = vi.fn()
10
+ const api = {
11
+ registerProvider: vi.fn(),
12
+ registerBinding: vi.fn(),
13
+ registerCatalog: vi.fn(),
14
+ registerPanel,
15
+ registerPanelCommand,
16
+ registerLeftTab: vi.fn(),
17
+ registerSurfaceResolver,
18
+ }
19
+
20
+ await samplePlugin(api)
21
+
22
+ expect(registerPanel).toHaveBeenCalledExactlyOnceWith(
23
+ expect.objectContaining({
24
+ id: SAMPLE_PANEL_ID,
25
+ label: "Sample",
26
+ placement: "center",
27
+ component: SamplePanel,
28
+ }),
29
+ )
30
+ expect(registerPanelCommand).toHaveBeenCalledExactlyOnceWith(
31
+ expect.objectContaining({
32
+ id: "sample.open",
33
+ title: "Open Sample",
34
+ panelId: SAMPLE_PANEL_ID,
35
+ }),
36
+ )
37
+ expect(registerSurfaceResolver).toHaveBeenCalledExactlyOnceWith(
38
+ expect.objectContaining({
39
+ id: "sample",
40
+ kind: SAMPLE_SURFACE_KIND,
41
+ }),
42
+ )
43
+ })
44
+
45
+ it("is the default export (required for hot-reload dynamic import)", () => {
46
+ expect(typeof samplePlugin).toBe("function")
47
+ })
48
+
49
+ it("carries pluginId + pluginLabel metadata (definePlugin contract)", () => {
50
+ expect(samplePlugin.pluginId).toBe(SAMPLE_PLUGIN_ID)
51
+ expect(samplePlugin.pluginLabel).toBe("Sample")
52
+ })
53
+ })
@@ -0,0 +1,45 @@
1
+ import { definePlugin, type BoringFrontFactoryWithId } from "@hachej/boring-workspace/plugin"
2
+ import { SAMPLE_PLUGIN_ID } from "../shared/constants"
3
+ import { SAMPLE_PANEL_ID, SamplePanel } from "./panels"
4
+ import { sampleSurfaceResolver } from "./surfaceResolver"
5
+
6
+ /**
7
+ * Default-exported `BoringFrontFactoryWithId`. The workspace shell
8
+ * accepts this directly in `WorkspaceProvider.plugins`; on bootstrap,
9
+ * it dispatches each declarative field to the corresponding
10
+ * `api.register*` method.
11
+ *
12
+ * `definePlugin(config)` takes a single declarative config object.
13
+ * For imperative composition (calling an external factory), use the
14
+ * `setup: (api) => void` escape hatch — see SKILL.md.
15
+ *
16
+ * Plugins in `.pi/extensions/<name>/` also get hot-reloaded — the
17
+ * workspace dynamically re-imports this module and re-runs the
18
+ * factory.
19
+ */
20
+ const samplePlugin: BoringFrontFactoryWithId = definePlugin({
21
+ id: SAMPLE_PLUGIN_ID,
22
+ label: "Sample",
23
+ panels: [
24
+ {
25
+ id: SAMPLE_PANEL_ID,
26
+ label: "Sample",
27
+ component: SamplePanel,
28
+ placement: "center",
29
+ source: "app",
30
+ },
31
+ ],
32
+ commands: [
33
+ {
34
+ id: "sample.open",
35
+ title: "Open Sample",
36
+ panelId: SAMPLE_PANEL_ID,
37
+ },
38
+ ],
39
+ surfaceResolvers: [sampleSurfaceResolver],
40
+ })
41
+
42
+ export default samplePlugin
43
+
44
+ export { SamplePanel, SAMPLE_PANEL_ID } from "./panels"
45
+ export { sampleSurfaceResolver } from "./surfaceResolver"
@@ -0,0 +1,16 @@
1
+ import type { PaneProps } from "@hachej/boring-workspace"
2
+ import type { SampleParams } from "../shared/types"
3
+
4
+ export const SAMPLE_PANEL_ID = "sample-panel"
5
+
6
+ export function SamplePanel({ params }: PaneProps<SampleParams>) {
7
+ return (
8
+ <div className="flex h-full min-h-0 min-w-0 flex-col bg-background text-foreground">
9
+ <div className="min-h-0 min-w-0 flex-1 overflow-auto p-4">
10
+ <div className="rounded-lg border border-border bg-card p-4 text-sm">
11
+ {params.id}
12
+ </div>
13
+ </div>
14
+ </div>
15
+ )
16
+ }
@@ -0,0 +1,19 @@
1
+ import type { BoringFrontSurfaceResolverRegistration } from "@hachej/boring-workspace/plugin"
2
+ import { SAMPLE_SURFACE_KIND } from "../shared/constants"
3
+ import { SAMPLE_PANEL_ID } from "./panels"
4
+
5
+ export const sampleSurfaceResolver: BoringFrontSurfaceResolverRegistration = {
6
+ id: "sample",
7
+ kind: SAMPLE_SURFACE_KIND,
8
+ source: "app",
9
+ resolve(request) {
10
+ if (request.kind !== SAMPLE_SURFACE_KIND) return undefined
11
+ return {
12
+ id: `sample:${request.target}`,
13
+ component: SAMPLE_PANEL_ID,
14
+ title: request.target,
15
+ params: { id: request.target },
16
+ score: 0,
17
+ }
18
+ },
19
+ }
@@ -0,0 +1,29 @@
1
+ import type { AgentTool } from "@hachej/boring-workspace"
2
+ import {
3
+ defineServerPlugin,
4
+ type WorkspaceServerPlugin,
5
+ } from "@hachej/boring-workspace/server"
6
+ import { SAMPLE_PLUGIN_ID } from "../shared/constants"
7
+
8
+ export function createSampleServerPlugin(): WorkspaceServerPlugin {
9
+ return defineServerPlugin({
10
+ id: SAMPLE_PLUGIN_ID,
11
+ label: "Sample",
12
+ agentTools: [] satisfies AgentTool[],
13
+ systemPrompt: "## Sample Plugin",
14
+ })
15
+ }
16
+
17
+ /**
18
+ * Default export — adapter for the standard `defaultPluginPackages`
19
+ * load process. The workspace's `pluginEntryResolver` calls a
20
+ * dir-source plugin's default-exported factory with `(options, ctx)`
21
+ * where `ctx = { workspaceRoot, bridge }`. The named
22
+ * `createSampleServerPlugin()` factory stays for direct callers.
23
+ */
24
+ export default function defaultSampleServerPlugin(
25
+ _options?: unknown,
26
+ _ctx?: { workspaceRoot: string; bridge: unknown },
27
+ ): WorkspaceServerPlugin {
28
+ return createSampleServerPlugin()
29
+ }
@@ -0,0 +1,2 @@
1
+ export const SAMPLE_PLUGIN_ID = "sample"
2
+ export const SAMPLE_SURFACE_KIND = "sample.open"
@@ -0,0 +1,2 @@
1
+ export * from "./constants"
2
+ export * from "./types"
@@ -0,0 +1,3 @@
1
+ export interface SampleParams {
2
+ id: string
3
+ }