create-zenbu-app 0.0.34 → 0.0.35
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/package.json +1 -1
- package/templates/plugin/AGENTS.md +300 -167
- package/templates/tailwind/AGENTS.md +300 -167
- package/templates/vanilla/AGENTS.md +300 -167
|
@@ -110,7 +110,7 @@ export class CounterService extends Service.create({
|
|
|
110
110
|
private count = 0
|
|
111
111
|
|
|
112
112
|
evaluate() {
|
|
113
|
-
// Called when the service starts
|
|
113
|
+
// Called when the service starts.
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
increment() {
|
|
@@ -158,7 +158,7 @@ function App() {
|
|
|
158
158
|
const rpc = useRpc()
|
|
159
159
|
|
|
160
160
|
const handleClick = async () => {
|
|
161
|
-
const newCount = await rpc.counter.increment()
|
|
161
|
+
const newCount = await rpc.app.counter.increment()
|
|
162
162
|
console.log(newCount)
|
|
163
163
|
}
|
|
164
164
|
|
|
@@ -203,15 +203,21 @@ function Notifications() {
|
|
|
203
203
|
}
|
|
204
204
|
```
|
|
205
205
|
|
|
206
|
+
## Injections
|
|
207
|
+
|
|
208
|
+
When a plugin wants to add UI to the app, it registers an [**injection**](/core/injections). The host already looks for injections in known places (sidebars, the bottom panel, the title bar, the footer) and renders them. A service registers one with `this.inject({ name, modulePath, meta })`, and any other plugin discovers it with `useInjections({ kind })` or renders it with `<View name="..." />`. The same primitive also covers [advice](/core/advice) and code that just needs to run at boot.
|
|
209
|
+
|
|
206
210
|
|
|
207
211
|
# Advice
|
|
208
212
|
Source: https://zenbulabs.mintlify.app/core/advice
|
|
209
213
|
|
|
210
214
|
|
|
211
215
|
|
|
212
|
-
Advice lets a plugin wrap or replace a function or React component owned by another plugin, without modifying the original source.
|
|
216
|
+
Advice lets a plugin wrap or replace a function or React component owned by another plugin, without modifying the original source. This API is inspired by Emacs, where [`defadvice`](https://www.gnu.org/software/emacs/manual/html_node/elisp/Advising-Functions.html) is used to modify existing functions without editing their source.
|
|
213
217
|
|
|
214
|
-
|
|
218
|
+
<Info>
|
|
219
|
+
Advice is a kind of [injection](/core/injections). `this.advise(...)` is sugar over `this.inject(...)` with `meta.kind: "advice"`.
|
|
220
|
+
</Info>
|
|
215
221
|
|
|
216
222
|
## Registering advice
|
|
217
223
|
|
|
@@ -224,7 +230,6 @@ export class ChromeService extends Service.create({ key: "chrome" }) {
|
|
|
224
230
|
evaluate() {
|
|
225
231
|
this.setup("wrap-counter", () =>
|
|
226
232
|
this.advise({
|
|
227
|
-
view: "entrypoint",
|
|
228
233
|
moduleId: "App.tsx",
|
|
229
234
|
name: "Counter",
|
|
230
235
|
type: "around",
|
|
@@ -236,24 +241,28 @@ export class ChromeService extends Service.create({ key: "chrome" }) {
|
|
|
236
241
|
}
|
|
237
242
|
```
|
|
238
243
|
|
|
239
|
-
| Field
|
|
240
|
-
|
|
|
241
|
-
| `
|
|
242
|
-
| `
|
|
243
|
-
| `
|
|
244
|
-
| `
|
|
245
|
-
| `
|
|
246
|
-
| `
|
|
244
|
+
| Field | Meaning |
|
|
245
|
+
| --------------- | ----------------------------------------------------------------------- |
|
|
246
|
+
| `moduleId` | Suffix of the source file that exports the target. |
|
|
247
|
+
| `name` | Name of the export to advise. |
|
|
248
|
+
| `type` | One of `"replace"`, `"before"`, `"after"`, `"around"`. |
|
|
249
|
+
| `modulePath` | Path to your wrapper module, relative to the plugin root (or absolute). |
|
|
250
|
+
| `exportName` | Named export inside the wrapper module. Defaults to `default`. |
|
|
251
|
+
| `injectionName` | Optional override for the synthetic injection name. |
|
|
247
252
|
|
|
248
|
-
Returns an unregister function.
|
|
253
|
+
Returns an unregister function. Wrap in `this.setup()` for hot-reload cleanup.
|
|
249
254
|
|
|
250
255
|
## Around advice
|
|
251
256
|
|
|
252
|
-
|
|
257
|
+
Around-advice receives the next function in the chain (which is the original target, or the next around-advice if several are stacked) as the **first positional argument**. For React components, since React calls components as `Component(props)`, the signature is `(Original, props)`.
|
|
253
258
|
|
|
254
259
|
```tsx src/content/wrap-counter.tsx theme={null}
|
|
255
|
-
|
|
256
|
-
|
|
260
|
+
import type { ComponentType } from "react"
|
|
261
|
+
|
|
262
|
+
export function WrapCounter<P>(
|
|
263
|
+
Original: ComponentType<P>,
|
|
264
|
+
props: P,
|
|
265
|
+
) {
|
|
257
266
|
return (
|
|
258
267
|
<div className="bordered">
|
|
259
268
|
<Original {...props} />
|
|
@@ -262,7 +271,21 @@ export function WrapCounter(props) {
|
|
|
262
271
|
}
|
|
263
272
|
```
|
|
264
273
|
|
|
265
|
-
|
|
274
|
+
For a plain function `save({ path }: { path: string })`, around-advice would be:
|
|
275
|
+
|
|
276
|
+
```typescript theme={null}
|
|
277
|
+
export function aroundSave(
|
|
278
|
+
next: (args: { path: string }) => void,
|
|
279
|
+
args: { path: string },
|
|
280
|
+
) {
|
|
281
|
+
console.log("before", args.path)
|
|
282
|
+
const result = next(args)
|
|
283
|
+
console.log("after", args.path)
|
|
284
|
+
return result
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Use around-advice when you want to render the original but add structure around it, change its props, or short-circuit it conditionally (skip the `next(...)` call).
|
|
266
289
|
|
|
267
290
|
## Replace advice
|
|
268
291
|
|
|
@@ -276,15 +299,18 @@ export function WrapCounter() {
|
|
|
276
299
|
|
|
277
300
|
## Before / After
|
|
278
301
|
|
|
279
|
-
`before` and `after` advice
|
|
302
|
+
`before` and `after` advice wrap a function without taking over the call.
|
|
303
|
+
|
|
304
|
+
* `before` runs first with the original args (`...originalArgs`); its return value is ignored.
|
|
305
|
+
* `after` runs last with the result followed by the original args (`result, ...originalArgs`). If it returns a value other than `undefined`, that value overrides the result.
|
|
280
306
|
|
|
281
307
|
```typescript theme={null}
|
|
282
|
-
// before: runs
|
|
308
|
+
// before: runs first, then the original
|
|
283
309
|
export function beforeSave(args: { path: string }) {
|
|
284
310
|
console.log("saving", args.path)
|
|
285
311
|
}
|
|
286
312
|
|
|
287
|
-
// after:
|
|
313
|
+
// after: runs last; return a value to override the result
|
|
288
314
|
export function afterSave(result: void, args: { path: string }) {
|
|
289
315
|
console.log("saved", args.path)
|
|
290
316
|
}
|
|
@@ -292,83 +318,7 @@ export function afterSave(result: void, args: { path: string }) {
|
|
|
292
318
|
|
|
293
319
|
## Hot reloading
|
|
294
320
|
|
|
295
|
-
Adding, removing, or editing a `this.advise(...)` call reloads the
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
# Content Scripts
|
|
299
|
-
Source: https://zenbulabs.mintlify.app/core/content-scripts
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
A content script is JavaScript that gets injected into a page at runtime. It runs in the same environment as the page, so it can use React hooks, read from the database, and call RPC methods. Content scripts target views by type, so you can inject into specific views or use `"*"` to inject into all of them.
|
|
304
|
-
|
|
305
|
-
## Use cases
|
|
306
|
-
|
|
307
|
-
* Add UI that appears on every page, like a devtools panel or a chat overlay.
|
|
308
|
-
* Add UI to a specific page without modifying that page's source code.
|
|
309
|
-
* Let a plugin contribute visible elements to the host app.
|
|
310
|
-
|
|
311
|
-
## Registering a content script
|
|
312
|
-
|
|
313
|
-
Call `this.injectContentScript(...)` from a service:
|
|
314
|
-
|
|
315
|
-
```typescript theme={null}
|
|
316
|
-
import { Service } from "@zenbujs/core/runtime"
|
|
317
|
-
|
|
318
|
-
export class DevtoolsService extends Service.create({ key: "devtools" }) {
|
|
319
|
-
evaluate() {
|
|
320
|
-
this.setup("inject", () =>
|
|
321
|
-
this.injectContentScript({
|
|
322
|
-
view: "*",
|
|
323
|
-
modulePath: "src/content/toolbar.tsx",
|
|
324
|
-
}),
|
|
325
|
-
)
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
| Field | Meaning |
|
|
331
|
-
| ------------ | -------------------------------------------------------------------------------------------------------------------------- |
|
|
332
|
-
| `view` | The view type to inject into. Pass `"*"` to inject into every view. |
|
|
333
|
-
| `modulePath` | Path to the script. Relative paths resolve against the plugin's directory; absolute paths are accepted as an escape hatch. |
|
|
334
|
-
|
|
335
|
-
The call returns an unsubscribe function, so wrapping it in `this.setup()` ensures cleanup on hot reload.
|
|
336
|
-
|
|
337
|
-
## Authoring a content script
|
|
338
|
-
|
|
339
|
-
A content script is a module that runs when the page loads. It can mount its own React tree into the DOM. This example adds a toolbar using a shadow root to keep its styles separate from the host page:
|
|
340
|
-
|
|
341
|
-
```tsx src/content/toolbar.tsx theme={null}
|
|
342
|
-
import { StrictMode } from "react"
|
|
343
|
-
import { createRoot } from "react-dom/client"
|
|
344
|
-
import { ZenbuProvider } from "@zenbujs/core/react"
|
|
345
|
-
import { Toolbar } from "./Toolbar"
|
|
346
|
-
|
|
347
|
-
function mount() {
|
|
348
|
-
if (document.body?.dataset.myToolbarMounted === "1") return
|
|
349
|
-
document.body.dataset.myToolbarMounted = "1"
|
|
350
|
-
|
|
351
|
-
const host = document.createElement("div")
|
|
352
|
-
document.body.appendChild(host)
|
|
353
|
-
const shadow = host.attachShadow({ mode: "closed" })
|
|
354
|
-
const reactRoot = document.createElement("div")
|
|
355
|
-
shadow.appendChild(reactRoot)
|
|
356
|
-
|
|
357
|
-
createRoot(reactRoot).render(
|
|
358
|
-
<StrictMode>
|
|
359
|
-
<ZenbuProvider>
|
|
360
|
-
<Toolbar />
|
|
361
|
-
</ZenbuProvider>
|
|
362
|
-
</StrictMode>
|
|
363
|
-
)
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
mount()
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
## Content scripts vs advice
|
|
370
|
-
|
|
371
|
-
Content scripts add new UI alongside the host view's component tree. They cannot replace or wrap existing components. For that, use [advice](/guides/advice-functions) instead.
|
|
321
|
+
Adding, removing, or editing a `this.advise(...)` call reloads the renderer so the new advice takes effect. Edits inside the advice module itself hot-replace through Vite's normal HMR.
|
|
372
322
|
|
|
373
323
|
|
|
374
324
|
# Events
|
|
@@ -441,6 +391,138 @@ The returned function unsubscribes the listener, so return it from your effect's
|
|
|
441
391
|
* **Database** is for state that should persist and drive your UI.
|
|
442
392
|
|
|
443
393
|
|
|
394
|
+
# Injections
|
|
395
|
+
Source: https://zenbulabs.mintlify.app/core/injections
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
An injection is a named value a plugin registers for other code to find. The value can be a React component, a function, or anything else. Other code looks up injections by name, or filters them by optional metadata.
|
|
400
|
+
|
|
401
|
+
## Registering an injection
|
|
402
|
+
|
|
403
|
+
From a service, call `this.inject(...)`:
|
|
404
|
+
|
|
405
|
+
```typescript src/main/services/my-plugin.ts theme={null}
|
|
406
|
+
import { Service } from "@zenbujs/core/runtime"
|
|
407
|
+
|
|
408
|
+
export class MyPluginService extends Service.create({ key: "myPlugin" }) {
|
|
409
|
+
evaluate() {
|
|
410
|
+
this.setup("inject", () =>
|
|
411
|
+
this.inject({
|
|
412
|
+
name: "my-plugin",
|
|
413
|
+
modulePath: "./src/views/my-view.tsx",
|
|
414
|
+
meta: { kind: "left-sidebar", label: "My plugin" },
|
|
415
|
+
}),
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
| Field | Required | Description |
|
|
422
|
+
| ------------ | -------- | ------------------------------------------------------------------------ |
|
|
423
|
+
| `name` | yes | Unique key in the registry. Last writer wins. |
|
|
424
|
+
| `modulePath` | yes | Path to the module. Relative paths resolve against the plugin directory. |
|
|
425
|
+
| `exportName` | no | Named export. Defaults to `default`. |
|
|
426
|
+
| `meta` | no | JSON-serializable object. Consumers filter on it. |
|
|
427
|
+
|
|
428
|
+
The call returns an unregister function. Wrap it in `this.setup(...)` so it cleans up on hot reload.
|
|
429
|
+
|
|
430
|
+
## Reading injections
|
|
431
|
+
|
|
432
|
+
`useInjections({ kind })` returns the live list of injections whose `meta.kind` matches.
|
|
433
|
+
|
|
434
|
+
```tsx theme={null}
|
|
435
|
+
import { useInjections } from "@zenbujs/core/react"
|
|
436
|
+
|
|
437
|
+
function LeftSidebar() {
|
|
438
|
+
const tabs = useInjections({ kind: "left-sidebar" })
|
|
439
|
+
return tabs.map((tab) => <Tab key={tab.name} label={tab.meta?.label} />)
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## Rendering an injection
|
|
444
|
+
|
|
445
|
+
`<View name="...">` looks up the injection and renders its value as a React component. `args` is passed to the component as a prop and is also available via `useViewArgs()` in any child.
|
|
446
|
+
|
|
447
|
+
```tsx theme={null}
|
|
448
|
+
import { View } from "@zenbujs/core/react"
|
|
449
|
+
|
|
450
|
+
<View name="my-plugin" args={{ workspaceId }} fallback={<Loading />} />
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
| Prop | Description |
|
|
454
|
+
| ---------- | ----------------------------------------------------------------------- |
|
|
455
|
+
| `name` | Injection name. Required. |
|
|
456
|
+
| `args` | Object passed to the component as `args`. |
|
|
457
|
+
| `visible` | When `false`, hides the wrapper via `display: none` without unmounting. |
|
|
458
|
+
| `fallback` | Rendered when nothing is registered under `name` yet. |
|
|
459
|
+
|
|
460
|
+
## Meta conventions
|
|
461
|
+
|
|
462
|
+
The framework does not read `meta`. Consumers do. The conventional keys are:
|
|
463
|
+
|
|
464
|
+
| Key | Used for |
|
|
465
|
+
| ------- | ------------------------------------------------------------------------------------------------- |
|
|
466
|
+
| `kind` | Slot discriminator. |
|
|
467
|
+
| `label` | Display text on tabs, palette entries, buttons. |
|
|
468
|
+
| `icon` | Inline SVG. Auto-filled from the plugin's `icons:` map when the key matches the injection `name`. |
|
|
469
|
+
| `order` | Sort hint within a slot. Lower comes first. |
|
|
470
|
+
|
|
471
|
+
## Registering from React
|
|
472
|
+
|
|
473
|
+
`useRegisterInjection` registers an injection while the calling component is mounted, and unregisters on unmount. Use it when the value can only be constructed inside React, for example a CodeMirror extension built from a hook's state.
|
|
474
|
+
|
|
475
|
+
```tsx theme={null}
|
|
476
|
+
import { useRegisterInjection } from "@zenbujs/core/react"
|
|
477
|
+
|
|
478
|
+
function VimSentinel() {
|
|
479
|
+
useRegisterInjection("cm-vim", vim(), {
|
|
480
|
+
kind: "cm.composer-extension-editable",
|
|
481
|
+
})
|
|
482
|
+
return null
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
## Injecting without a value
|
|
487
|
+
|
|
488
|
+
With no `meta`, the framework just imports the module. The module's top-level code runs, but nothing is added to the registry.
|
|
489
|
+
|
|
490
|
+
```typescript theme={null}
|
|
491
|
+
this.inject({
|
|
492
|
+
name: "my-plugin/devtools",
|
|
493
|
+
modulePath: "./src/content/devtools.tsx",
|
|
494
|
+
})
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
## Advice
|
|
498
|
+
|
|
499
|
+
Advice is a kind of injection that replaces or wraps a specific exported function or component anywhere in the app. It is registered with `this.advise(...)`, which targets the export by its source file and export name.
|
|
500
|
+
|
|
501
|
+
```typescript theme={null}
|
|
502
|
+
this.setup("wrap-counter", () =>
|
|
503
|
+
this.advise({
|
|
504
|
+
moduleId: "Counter.tsx",
|
|
505
|
+
name: "Counter",
|
|
506
|
+
type: "around",
|
|
507
|
+
modulePath: "./src/wrap-counter.tsx",
|
|
508
|
+
}),
|
|
509
|
+
)
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
The `type` field controls how the wrapper relates to the original:
|
|
513
|
+
|
|
514
|
+
* `replace` substitutes the original entirely.
|
|
515
|
+
* `around` runs in place of the original. It receives the next function in the chain as its first argument; call it (or don't) to decide whether the original runs.
|
|
516
|
+
* `before` runs first with the original arguments.
|
|
517
|
+
* `after` runs last and receives the result followed by the original arguments. Returning a value other than `undefined` overrides the result.
|
|
518
|
+
|
|
519
|
+
`this.advise(...)` is sugar over `this.inject(...)` with `meta.kind: "advice"`. For the full reference, including how the wrapper module should be written for each `type`, see [Advice](/core/advice).
|
|
520
|
+
|
|
521
|
+
## Hot reloading
|
|
522
|
+
|
|
523
|
+
Adding, removing, or editing a `this.inject(...)` call invalidates the renderer prelude and reloads the window. Edits inside the injection module itself hot-replace through Vite.
|
|
524
|
+
|
|
525
|
+
|
|
444
526
|
# Plugins
|
|
445
527
|
Source: https://zenbulabs.mintlify.app/core/plugins
|
|
446
528
|
|
|
@@ -465,19 +547,24 @@ export default defineConfig({
|
|
|
465
547
|
schema: "./src/main/schema.ts",
|
|
466
548
|
events: "./src/main/events.ts",
|
|
467
549
|
migrations: "./migrations",
|
|
550
|
+
icons: {
|
|
551
|
+
"my-view": '<svg ...>...</svg>',
|
|
552
|
+
},
|
|
468
553
|
}),
|
|
469
554
|
],
|
|
470
555
|
})
|
|
471
556
|
```
|
|
472
557
|
|
|
473
|
-
| Field | Required | Description
|
|
474
|
-
| ------------ | -------- |
|
|
475
|
-
| `name` | yes | Unique plugin identifier. Used as the top-level namespace for the plugin's database section, RPC services, and events (e.g. `db.<name>`, `rpc.<name>.<service>`, `events.<name>.<event>`). Reserved value: `core` belongs to the framework.
|
|
476
|
-
| `services` | yes | Glob patterns for main process service files.
|
|
477
|
-
| `schema` | no | Path to the database schema definition.
|
|
478
|
-
| `events` | no | Path to event type definitions.
|
|
479
|
-
| `migrations` | no | Directory of generated migration files.
|
|
480
|
-
| `
|
|
558
|
+
| Field | Required | Description |
|
|
559
|
+
| ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
560
|
+
| `name` | yes | Unique plugin identifier. Used as the top-level namespace for the plugin's database section, RPC services, and events (e.g. `db.<name>`, `rpc.<name>.<service>`, `events.<name>.<event>`). Reserved value: `core` belongs to the framework. |
|
|
561
|
+
| `services` | yes | Glob patterns for main process service files. |
|
|
562
|
+
| `schema` | no | Path to the database schema definition. |
|
|
563
|
+
| `events` | no | Path to event type definitions. |
|
|
564
|
+
| `migrations` | no | Directory of generated migration files. |
|
|
565
|
+
| `preload` | no | Path to a renderer preload module. |
|
|
566
|
+
| `icons` | no | Map of inline SVG strings keyed by [injection](/core/injections) `name`. When you call `this.inject({ name })` and a matching key exists, the framework copies the SVG into `meta.icon` for you. Lookup is per-plugin (no cross-plugin fallback). |
|
|
567
|
+
| `dependsOn` | no | Declares which other plugins this plugin needs type definitions from. See [Type dependencies](#type-dependencies). |
|
|
481
568
|
|
|
482
569
|
## Scaffolding a plugin
|
|
483
570
|
|
|
@@ -509,6 +596,39 @@ plugins: [
|
|
|
509
596
|
|
|
510
597
|
Adding or removing a plugin in `zenbu.config.ts` is a hot-reloadable change that takes effect without restarting the app.
|
|
511
598
|
|
|
599
|
+
## Local-only plugins (`localPlugins`)
|
|
600
|
+
|
|
601
|
+
A `zenbu.config.ts` can opt into a per-developer overlay file that's gitignored and loaded only if it exists. Use this when you want to wire a local plugin (e.g. one cloned into `~/.zenbu/plugins/...`) into your app without committing the path:
|
|
602
|
+
|
|
603
|
+
```typescript theme={null}
|
|
604
|
+
export default defineConfig({
|
|
605
|
+
uiEntrypoint: "./src/renderer",
|
|
606
|
+
plugins: [
|
|
607
|
+
"./plugins/app/zenbu.plugin.ts",
|
|
608
|
+
],
|
|
609
|
+
localPlugins: "./zenbu.local.ts", // gitignored; optional
|
|
610
|
+
})
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
```typescript zenbu.local.ts theme={null}
|
|
614
|
+
import type { LocalPluginsDefault } from "@zenbujs/core/config"
|
|
615
|
+
|
|
616
|
+
const plugins: LocalPluginsDefault = [
|
|
617
|
+
"/Users/me/.zenbu/plugins/the-browser-plugin/zenbu.plugin.ts",
|
|
618
|
+
// or an inline definePlugin({...})
|
|
619
|
+
]
|
|
620
|
+
export default plugins
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
Rules:
|
|
624
|
+
|
|
625
|
+
* The default export is a plugin entry, or an array of entries. Same shape as `plugins`.
|
|
626
|
+
* Relative paths inside the overlay anchor to the **overlay file's** directory, not the project root.
|
|
627
|
+
* `localPlugins` accepts a single string or an array of strings (multiple overlays).
|
|
628
|
+
* If the overlay file doesn't exist, the field is silently ignored.
|
|
629
|
+
* Editing the overlay file hot-reloads exactly like editing `zenbu.config.ts`.
|
|
630
|
+
* `zen build:source` / `zen build:electron` / `zen publish:source` **skip** `localPlugins` entirely, so a developer's overlay can never ship in a build artefact.
|
|
631
|
+
|
|
512
632
|
## Plugin capabilities
|
|
513
633
|
|
|
514
634
|
A plugin has the same capabilities as the host application:
|
|
@@ -516,9 +636,7 @@ A plugin has the same capabilities as the host application:
|
|
|
516
636
|
* **Services** run in the main process and expose methods via RPC.
|
|
517
637
|
* **Database schemas** define sections of the shared database.
|
|
518
638
|
* **Events** can be emitted and listened to across plugins.
|
|
519
|
-
* **
|
|
520
|
-
* **Content scripts** inject JavaScript into any view.
|
|
521
|
-
* **Advice** lets a plugin wrap or replace any React component in the renderer.
|
|
639
|
+
* **[Injections](/core/injections)** are the unified renderer-side surface. One primitive (`this.inject(...)` / `useRegisterInjection(...)`) covers React components other plugins can render via `<View>`, plain side-effect modules (the old "content script" pattern), plain functions/values, and [advice](/core/advice) (sugar for wrapping or replacing an existing export).
|
|
522
640
|
|
|
523
641
|
## Type dependencies
|
|
524
642
|
|
|
@@ -571,7 +689,7 @@ Under the hood, communication happens over a WebSocket, which means the same RPC
|
|
|
571
689
|
Services are scoped under their owning plugin's name. For a plugin named `app`, the call site is `rpc.app.<service>.<method>(...)`. Core's own services live under `rpc.core.<service>`.
|
|
572
690
|
|
|
573
691
|
```typescript theme={null}
|
|
574
|
-
// Main process
|
|
692
|
+
// Main process. Declared inside the `app` plugin.
|
|
575
693
|
export class MathService extends Service.create({
|
|
576
694
|
key: "math",
|
|
577
695
|
}) {
|
|
@@ -689,16 +807,18 @@ Services declare dependencies on other services through the `deps` field. The fr
|
|
|
689
807
|
import { Service } from "@zenbujs/core/runtime"
|
|
690
808
|
import { WindowService } from "@zenbujs/core/services"
|
|
691
809
|
|
|
692
|
-
export class
|
|
693
|
-
key: "
|
|
810
|
+
export class InitService extends Service.create({
|
|
811
|
+
key: "init",
|
|
694
812
|
deps: { window: WindowService },
|
|
695
813
|
}) {
|
|
696
814
|
async evaluate() {
|
|
697
|
-
await this.ctx.window.
|
|
815
|
+
await this.ctx.window.openWindow({})
|
|
698
816
|
}
|
|
699
817
|
}
|
|
700
818
|
```
|
|
701
819
|
|
|
820
|
+
`openWindow({})` boots a main window pointed at your `uiEntrypoint`. Pass `injection: "<name>"` to open a window whose entry route renders a specific [injection](/core/injections) instead.
|
|
821
|
+
|
|
702
822
|
By the time `evaluate()` runs, all dependencies in `this.ctx` are fully initialized.
|
|
703
823
|
|
|
704
824
|
## Setups and cleanups
|
|
@@ -841,13 +961,13 @@ Inside `update()`, you mutate the root object directly, the same way you would w
|
|
|
841
961
|
|
|
842
962
|
Regular data fields are always held in memory across every process. Collections are for data that can grow large (like agent messages or logs) and should only be loaded into memory when needed.
|
|
843
963
|
|
|
844
|
-
Define a collection in your schema with `
|
|
964
|
+
Define a collection in your schema with `collection(...)`:
|
|
845
965
|
|
|
846
966
|
```typescript src/main/schema.ts theme={null}
|
|
847
|
-
import { createSchema,
|
|
967
|
+
import { createSchema, collection, z } from "@zenbujs/core/db"
|
|
848
968
|
|
|
849
969
|
export default createSchema({
|
|
850
|
-
messages:
|
|
970
|
+
messages: collection(
|
|
851
971
|
z.object({
|
|
852
972
|
text: z.string(),
|
|
853
973
|
author: z.string(),
|
|
@@ -896,13 +1016,13 @@ Collection data is only loaded into memory when you subscribe or read from it. T
|
|
|
896
1016
|
|
|
897
1017
|
Blobs store binary data (`Uint8Array`) like files or images. Like collections, they live on disk and are only loaded into memory when you read them.
|
|
898
1018
|
|
|
899
|
-
Define a blob in your schema with `
|
|
1019
|
+
Define a blob in your schema with `blob(...)`:
|
|
900
1020
|
|
|
901
1021
|
```typescript src/main/schema.ts theme={null}
|
|
902
|
-
import { createSchema,
|
|
1022
|
+
import { createSchema, blob, z } from "@zenbujs/core/db"
|
|
903
1023
|
|
|
904
1024
|
export default createSchema({
|
|
905
|
-
avatar:
|
|
1025
|
+
avatar: blob({ debugName: "avatar" }),
|
|
906
1026
|
})
|
|
907
1027
|
```
|
|
908
1028
|
|
|
@@ -976,69 +1096,82 @@ Source: https://zenbulabs.mintlify.app/core/views
|
|
|
976
1096
|
|
|
977
1097
|
|
|
978
1098
|
|
|
979
|
-
|
|
1099
|
+
`<View>` renders an [injection](/core/injections) as React. The injection registry is the source of truth. `<View name="...">` looks up the component registered under `name` (by a service's `this.inject(...)` or by `useRegisterInjection(...)`) and mounts it inside the host tree.
|
|
980
1100
|
|
|
981
|
-
|
|
1101
|
+
There's no separate "view registry", no iframes, no per-view Vite server. Everything is one React tree under `<ZenbuProvider>`, sharing the host's RPC, events, DB, theme, and CSS.
|
|
1102
|
+
|
|
1103
|
+
## Embedding a view
|
|
982
1104
|
|
|
983
|
-
|
|
1105
|
+
```tsx theme={null}
|
|
1106
|
+
import { View } from "@zenbujs/core/react"
|
|
984
1107
|
|
|
985
|
-
|
|
1108
|
+
<View
|
|
1109
|
+
name="terminal"
|
|
1110
|
+
args={{ tabId }}
|
|
1111
|
+
fallback={<Spinner />}
|
|
1112
|
+
/>
|
|
1113
|
+
```
|
|
986
1114
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1115
|
+
| Prop | Meaning |
|
|
1116
|
+
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1117
|
+
| `name` | Injection name to render. Required. |
|
|
1118
|
+
| `args` | Object forwarded as the `args` prop and on `useViewArgs()`. |
|
|
1119
|
+
| `visible` | When `false`, hides the wrapper via `display: none` without unmounting. Use it to preserve in-component state across visibility toggles. |
|
|
1120
|
+
| `fallback` | Rendered while no injection has registered under `name`. |
|
|
1121
|
+
| `className`, `style` | Forwarded to the wrapper. |
|
|
990
1122
|
|
|
991
|
-
|
|
1123
|
+
If no injection has registered yet, `<View>` renders the `fallback`
|
|
1124
|
+
(an empty `<span data-zenbu-view-pending={name}>` if you don't pass
|
|
1125
|
+
one). Once a registration lands, it swaps in automatically.
|
|
992
1126
|
|
|
993
|
-
|
|
1127
|
+
## Reading view args
|
|
994
1128
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
import { fileURLToPath } from "node:url"
|
|
998
|
-
import { Service } from "@zenbujs/core/runtime"
|
|
999
|
-
import { ViewRegistryService } from "@zenbujs/core/services"
|
|
1129
|
+
Inside the rendered component, both the `args` prop and the
|
|
1130
|
+
`useViewArgs()` hook are populated with the same object:
|
|
1000
1131
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
}) {
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
type: "terminal",
|
|
1010
|
-
root: viewRoot,
|
|
1011
|
-
configFile: false,
|
|
1012
|
-
meta: { kind: "view", label: "Terminal" },
|
|
1013
|
-
})
|
|
1014
|
-
}
|
|
1132
|
+
```tsx theme={null}
|
|
1133
|
+
import { useViewArgs, type ViewComponentProps } from "@zenbujs/core/react"
|
|
1134
|
+
|
|
1135
|
+
export default function TerminalApp({ args }: ViewComponentProps<{ tabId: string }>) {
|
|
1136
|
+
// Either of these works:
|
|
1137
|
+
const fromProps = args.tabId
|
|
1138
|
+
const fromHook = useViewArgs<{ tabId: string }>().tabId
|
|
1139
|
+
// …
|
|
1015
1140
|
}
|
|
1016
1141
|
```
|
|
1017
1142
|
|
|
1018
|
-
|
|
1143
|
+
`useViewArgs` is the convenient escape hatch for deeply nested children that don't want to thread `args` down manually. The hook re-renders when the parent `<View>` passes a new `args` object.
|
|
1019
1144
|
|
|
1020
|
-
##
|
|
1021
|
-
|
|
1022
|
-
```tsx theme={null}
|
|
1023
|
-
import { View } from "@zenbujs/core/react"
|
|
1145
|
+
## Registering the component a view renders
|
|
1024
1146
|
|
|
1025
|
-
|
|
1026
|
-
```
|
|
1147
|
+
A view is just an injection. Use the API that fits your situation:
|
|
1027
1148
|
|
|
1028
|
-
|
|
1149
|
+
* **Inside a service** (most plugins): `this.inject({ name, modulePath, meta })` from [Injections](/core/injections).
|
|
1150
|
+
* **From the React tree**: `useRegisterInjection(name, Component, meta)`.
|
|
1029
1151
|
|
|
1030
|
-
|
|
1152
|
+
```typescript theme={null}
|
|
1153
|
+
// From a service.
|
|
1154
|
+
this.setup("inject", () =>
|
|
1155
|
+
this.inject({
|
|
1156
|
+
name: "terminal",
|
|
1157
|
+
modulePath: "./src/views/terminal-view.tsx",
|
|
1158
|
+
meta: { kind: "bottom-panel", label: "Terminal" },
|
|
1159
|
+
}),
|
|
1160
|
+
)
|
|
1161
|
+
```
|
|
1031
1162
|
|
|
1032
1163
|
```tsx theme={null}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
}
|
|
1164
|
+
// From React.
|
|
1165
|
+
useRegisterInjection("terminal", TerminalView, {
|
|
1166
|
+
kind: "bottom-panel",
|
|
1167
|
+
label: "Terminal",
|
|
1168
|
+
})
|
|
1039
1169
|
```
|
|
1040
1170
|
|
|
1041
|
-
|
|
1171
|
+
## See also
|
|
1172
|
+
|
|
1173
|
+
* [Injections](/core/injections) for the full registration surface and `meta` conventions.
|
|
1174
|
+
* [Advice](/core/advice) for wrapping or replacing another plugin's view.
|
|
1042
1175
|
|
|
1043
1176
|
|
|
1044
1177
|
# Releasing to Production
|
|
@@ -1386,9 +1519,9 @@ my-app/
|
|
|
1386
1519
|
├── tsconfig.json
|
|
1387
1520
|
└── src/
|
|
1388
1521
|
├── main/
|
|
1389
|
-
│ ├── schema.ts # Zod schema for the database
|
|
1390
1522
|
│ └── services/
|
|
1391
|
-
│
|
|
1523
|
+
│ ├── init.ts # Opens the main window on boot
|
|
1524
|
+
│ └── cwd.ts # RPC service that returns process.cwd()
|
|
1392
1525
|
└── renderer/
|
|
1393
1526
|
├── index.html
|
|
1394
1527
|
├── main.tsx
|
|
@@ -1401,18 +1534,18 @@ The `zenbu.config.ts` file is the entry point. It tells Zenbu.js where everythin
|
|
|
1401
1534
|
import { defineConfig, definePlugin } from "@zenbujs/core/config"
|
|
1402
1535
|
|
|
1403
1536
|
export default defineConfig({
|
|
1404
|
-
db: "./.zenbu/db",
|
|
1405
1537
|
uiEntrypoint: "./src/renderer",
|
|
1406
1538
|
plugins: [
|
|
1407
1539
|
definePlugin({
|
|
1408
1540
|
name: "app",
|
|
1409
1541
|
services: ["./src/main/services/*.ts"],
|
|
1410
|
-
schema: "./src/main/schema.ts",
|
|
1411
1542
|
}),
|
|
1412
1543
|
],
|
|
1413
1544
|
})
|
|
1414
1545
|
```
|
|
1415
1546
|
|
|
1547
|
+
Add a `schema: "./src/main/schema.ts"` field to the plugin when you're ready to introduce a database. The top-level `db` field is optional and defaults to `./.zenbu/db`.
|
|
1548
|
+
|
|
1416
1549
|
## Available scripts
|
|
1417
1550
|
|
|
1418
1551
|
| Script | What it does |
|