create-zenbu-app 0.0.33 → 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.
@@ -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
- This API is inspired from 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.
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 | Meaning |
240
- | ------------ | ----------------------------------------------------------------------- |
241
- | `view` | View type to apply the advice in. `"*"` for every view. |
242
- | `moduleId` | Suffix of the source file that exports the target. |
243
- | `name` | Name of the export to advise. |
244
- | `type` | One of `"replace"`, `"before"`, `"after"`, `"around"`. |
245
- | `modulePath` | Path to your wrapper module, relative to the plugin root (or absolute). |
246
- | `exportName` | Named export inside the wrapper module. |
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. Use it as a `setup` cleanup.
253
+ Returns an unregister function. Wrap in `this.setup()` for hot-reload cleanup.
249
254
 
250
255
  ## Around advice
251
256
 
252
- Receives the original as `__original` in props, making it the most flexible advice type.
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
- export function WrapCounter(props) {
256
- const Original = props.__original
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
- Use this when you want to render the original but add structure around it or change its props.
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 run extra logic around the original function.
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 your function, then calls the original
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: calls the original, then runs your function with the result
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 affected views so the new advice takes effect. Edits inside the advice module itself hot-replace through Vite's normal HMR.
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
- | `dependsOn` | no | Declares which other plugins this plugin needs type definitions from. See [Type dependencies](#type-dependencies). |
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
- * **Views** are iframe-mounted renderer entry points other plugins can embed.
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 declared inside the `app` plugin
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 AppService extends Service.create({
693
- key: "app",
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.openView({ type: "entrypoint" })
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 `f.collection()`:
964
+ Define a collection in your schema with `collection(...)`:
845
965
 
846
966
  ```typescript src/main/schema.ts theme={null}
847
- import { createSchema, f, z } from "@zenbujs/core/db"
967
+ import { createSchema, collection, z } from "@zenbujs/core/db"
848
968
 
849
969
  export default createSchema({
850
- messages: f.collection(
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 `f.blob()`:
1019
+ Define a blob in your schema with `blob(...)`:
900
1020
 
901
1021
  ```typescript src/main/schema.ts theme={null}
902
- import { createSchema, f, z } from "@zenbujs/core/db"
1022
+ import { createSchema, blob, z } from "@zenbujs/core/db"
903
1023
 
904
1024
  export default createSchema({
905
- avatar: f.blob({ debugName: "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
- <Warning>The Views API is under construction</Warning>
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
- A view is the rendering primitive in Zenbu.js. Every page your app renders is a view, including the main application itself, which is a view called `entrypoint` defined by your `uiEntrypoint` config.
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
- The `entrypoint` view runs directly in the renderer process. Additional views run in [out of process iframes](https://www.chromium.org/developers/design-documents/oop-iframes/), and connect to the same [RPC](/core/rpc), [events](/core/events), and [database](/core/state).
1105
+ ```tsx theme={null}
1106
+ import { View } from "@zenbujs/core/react"
984
1107
 
985
- Additional views are useful for:
1108
+ <View
1109
+ name="terminal"
1110
+ args={{ tabId }}
1111
+ fallback={<Spinner />}
1112
+ />
1113
+ ```
986
1114
 
987
- * Letting a plugin embed content into your app.
988
- * Isolating heavy features so a slow render in one view doesn't block the rest of the app.
989
- * Letting teams or plugins develop parts of the app independently.
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
- ## Registering a view
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
- From a service, use `ViewRegistryService` to register a view type:
1127
+ ## Reading view args
994
1128
 
995
- ```typescript theme={null}
996
- import path from "node:path"
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
- export class TerminalService extends Service.create({
1002
- key: "terminal",
1003
- deps: { viewRegistry: ViewRegistryService },
1004
- }) {
1005
- async evaluate() {
1006
- const here = path.dirname(fileURLToPath(import.meta.url))
1007
- const viewRoot = path.resolve(here, "..", "..", "views", "terminal")
1008
- await this.ctx.viewRegistry.register({
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
- The view directory must contain an `index.html` and a `main.tsx` that mounts a React tree wrapped in `<ZenbuProvider>`.
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
- ## Embedding a view
1021
-
1022
- ```tsx theme={null}
1023
- import { View } from "@zenbujs/core/react"
1145
+ ## Registering the component a view renders
1024
1146
 
1025
- <View type="terminal" args={{ tabId }} />
1026
- ```
1147
+ A view is just an injection. Use the API that fits your situation:
1027
1148
 
1028
- ## Reading view args
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
- Inside the child view, use `useViewArgs` to read the args passed by the parent:
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
- import { useViewArgs } from "@zenbujs/core/react"
1034
-
1035
- function TerminalApp() {
1036
- const { tabId } = useViewArgs<{ tabId: string }>()
1037
- // ...
1038
- }
1164
+ // From React.
1165
+ useRegisterInjection("terminal", TerminalView, {
1166
+ kind: "bottom-panel",
1167
+ label: "Terminal",
1168
+ })
1039
1169
  ```
1040
1170
 
1041
- When the parent updates `args`, the hook re-renders with the new values.
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
- └── app.ts # Opens the main window on boot
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 |