create-zenbu-app 0.0.34 → 0.0.36

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.
@@ -1,5 +1,9 @@
1
1
  # Zenbu.js docs
2
2
 
3
+ # Plugin rules
4
+
5
+ - do not add this plugin to the host config (the plugins array in zenbu.config.ts, a zenbu.local.ts overlay, or any zenbu.plugins*.jsonc manifest). build it in its own folder and leave it there. the user enables or installs it themselves when they are ready, or will ask you to. do not edit their config files on your own.
6
+
3
7
  # Overview
4
8
  Source: https://zenbulabs.mintlify.app/api-reference/overview
5
9
 
@@ -110,7 +114,7 @@ export class CounterService extends Service.create({
110
114
  private count = 0
111
115
 
112
116
  evaluate() {
113
- // Called when the service starts
117
+ // Called when the service starts.
114
118
  }
115
119
 
116
120
  increment() {
@@ -158,7 +162,7 @@ function App() {
158
162
  const rpc = useRpc()
159
163
 
160
164
  const handleClick = async () => {
161
- const newCount = await rpc.counter.increment()
165
+ const newCount = await rpc.app.counter.increment()
162
166
  console.log(newCount)
163
167
  }
164
168
 
@@ -203,15 +207,21 @@ function Notifications() {
203
207
  }
204
208
  ```
205
209
 
210
+ ## Injections
211
+
212
+ 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.
213
+
206
214
 
207
215
  # Advice
208
216
  Source: https://zenbulabs.mintlify.app/core/advice
209
217
 
210
218
 
211
219
 
212
- Advice lets a plugin wrap or replace a function or React component owned by another plugin, without modifying the original source.
220
+ 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
221
 
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.
222
+ <Info>
223
+ Advice is a kind of [injection](/core/injections). `this.advise(...)` is sugar over `this.inject(...)` with `meta.kind: "advice"`.
224
+ </Info>
215
225
 
216
226
  ## Registering advice
217
227
 
@@ -224,7 +234,6 @@ export class ChromeService extends Service.create({ key: "chrome" }) {
224
234
  evaluate() {
225
235
  this.setup("wrap-counter", () =>
226
236
  this.advise({
227
- view: "entrypoint",
228
237
  moduleId: "App.tsx",
229
238
  name: "Counter",
230
239
  type: "around",
@@ -236,24 +245,28 @@ export class ChromeService extends Service.create({ key: "chrome" }) {
236
245
  }
237
246
  ```
238
247
 
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. |
248
+ | Field | Meaning |
249
+ | --------------- | ----------------------------------------------------------------------- |
250
+ | `moduleId` | Suffix of the source file that exports the target. |
251
+ | `name` | Name of the export to advise. |
252
+ | `type` | One of `"replace"`, `"before"`, `"after"`, `"around"`. |
253
+ | `modulePath` | Path to your wrapper module, relative to the plugin root (or absolute). |
254
+ | `exportName` | Named export inside the wrapper module. Defaults to `default`. |
255
+ | `injectionName` | Optional override for the synthetic injection name. |
247
256
 
248
- Returns an unregister function. Use it as a `setup` cleanup.
257
+ Returns an unregister function. Wrap in `this.setup()` for hot-reload cleanup.
249
258
 
250
259
  ## Around advice
251
260
 
252
- Receives the original as `__original` in props, making it the most flexible advice type.
261
+ 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
262
 
254
263
  ```tsx src/content/wrap-counter.tsx theme={null}
255
- export function WrapCounter(props) {
256
- const Original = props.__original
264
+ import type { ComponentType } from "react"
265
+
266
+ export function WrapCounter<P>(
267
+ Original: ComponentType<P>,
268
+ props: P,
269
+ ) {
257
270
  return (
258
271
  <div className="bordered">
259
272
  <Original {...props} />
@@ -262,7 +275,21 @@ export function WrapCounter(props) {
262
275
  }
263
276
  ```
264
277
 
265
- Use this when you want to render the original but add structure around it or change its props.
278
+ For a plain function `save({ path }: { path: string })`, around-advice would be:
279
+
280
+ ```typescript theme={null}
281
+ export function aroundSave(
282
+ next: (args: { path: string }) => void,
283
+ args: { path: string },
284
+ ) {
285
+ console.log("before", args.path)
286
+ const result = next(args)
287
+ console.log("after", args.path)
288
+ return result
289
+ }
290
+ ```
291
+
292
+ 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
293
 
267
294
  ## Replace advice
268
295
 
@@ -276,15 +303,18 @@ export function WrapCounter() {
276
303
 
277
304
  ## Before / After
278
305
 
279
- `before` and `after` advice run extra logic around the original function.
306
+ `before` and `after` advice wrap a function without taking over the call.
307
+
308
+ * `before` runs first with the original args (`...originalArgs`); its return value is ignored.
309
+ * `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
310
 
281
311
  ```typescript theme={null}
282
- // before: runs your function, then calls the original
312
+ // before: runs first, then the original
283
313
  export function beforeSave(args: { path: string }) {
284
314
  console.log("saving", args.path)
285
315
  }
286
316
 
287
- // after: calls the original, then runs your function with the result
317
+ // after: runs last; return a value to override the result
288
318
  export function afterSave(result: void, args: { path: string }) {
289
319
  console.log("saved", args.path)
290
320
  }
@@ -292,83 +322,7 @@ export function afterSave(result: void, args: { path: string }) {
292
322
 
293
323
  ## Hot reloading
294
324
 
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.
325
+ 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
326
 
373
327
 
374
328
  # Events
@@ -441,6 +395,138 @@ The returned function unsubscribes the listener, so return it from your effect's
441
395
  * **Database** is for state that should persist and drive your UI.
442
396
 
443
397
 
398
+ # Injections
399
+ Source: https://zenbulabs.mintlify.app/core/injections
400
+
401
+
402
+
403
+ 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.
404
+
405
+ ## Registering an injection
406
+
407
+ From a service, call `this.inject(...)`:
408
+
409
+ ```typescript src/main/services/my-plugin.ts theme={null}
410
+ import { Service } from "@zenbujs/core/runtime"
411
+
412
+ export class MyPluginService extends Service.create({ key: "myPlugin" }) {
413
+ evaluate() {
414
+ this.setup("inject", () =>
415
+ this.inject({
416
+ name: "my-plugin",
417
+ modulePath: "./src/views/my-view.tsx",
418
+ meta: { kind: "left-sidebar", label: "My plugin" },
419
+ }),
420
+ )
421
+ }
422
+ }
423
+ ```
424
+
425
+ | Field | Required | Description |
426
+ | ------------ | -------- | ------------------------------------------------------------------------ |
427
+ | `name` | yes | Unique key in the registry. Last writer wins. |
428
+ | `modulePath` | yes | Path to the module. Relative paths resolve against the plugin directory. |
429
+ | `exportName` | no | Named export. Defaults to `default`. |
430
+ | `meta` | no | JSON-serializable object. Consumers filter on it. |
431
+
432
+ The call returns an unregister function. Wrap it in `this.setup(...)` so it cleans up on hot reload.
433
+
434
+ ## Reading injections
435
+
436
+ `useInjections({ kind })` returns the live list of injections whose `meta.kind` matches.
437
+
438
+ ```tsx theme={null}
439
+ import { useInjections } from "@zenbujs/core/react"
440
+
441
+ function LeftSidebar() {
442
+ const tabs = useInjections({ kind: "left-sidebar" })
443
+ return tabs.map((tab) => <Tab key={tab.name} label={tab.meta?.label} />)
444
+ }
445
+ ```
446
+
447
+ ## Rendering an injection
448
+
449
+ `<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.
450
+
451
+ ```tsx theme={null}
452
+ import { View } from "@zenbujs/core/react"
453
+
454
+ <View name="my-plugin" args={{ workspaceId }} fallback={<Loading />} />
455
+ ```
456
+
457
+ | Prop | Description |
458
+ | ---------- | ----------------------------------------------------------------------- |
459
+ | `name` | Injection name. Required. |
460
+ | `args` | Object passed to the component as `args`. |
461
+ | `visible` | When `false`, hides the wrapper via `display: none` without unmounting. |
462
+ | `fallback` | Rendered when nothing is registered under `name` yet. |
463
+
464
+ ## Meta conventions
465
+
466
+ The framework does not read `meta`. Consumers do. The conventional keys are:
467
+
468
+ | Key | Used for |
469
+ | ------- | ------------------------------------------------------------------------------------------------- |
470
+ | `kind` | Slot discriminator. |
471
+ | `label` | Display text on tabs, palette entries, buttons. |
472
+ | `icon` | Inline SVG. Auto-filled from the plugin's `icons:` map when the key matches the injection `name`. |
473
+ | `order` | Sort hint within a slot. Lower comes first. |
474
+
475
+ ## Registering from React
476
+
477
+ `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.
478
+
479
+ ```tsx theme={null}
480
+ import { useRegisterInjection } from "@zenbujs/core/react"
481
+
482
+ function VimSentinel() {
483
+ useRegisterInjection("cm-vim", vim(), {
484
+ kind: "cm.composer-extension-editable",
485
+ })
486
+ return null
487
+ }
488
+ ```
489
+
490
+ ## Injecting without a value
491
+
492
+ With no `meta`, the framework just imports the module. The module's top-level code runs, but nothing is added to the registry.
493
+
494
+ ```typescript theme={null}
495
+ this.inject({
496
+ name: "my-plugin/devtools",
497
+ modulePath: "./src/content/devtools.tsx",
498
+ })
499
+ ```
500
+
501
+ ## Advice
502
+
503
+ 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.
504
+
505
+ ```typescript theme={null}
506
+ this.setup("wrap-counter", () =>
507
+ this.advise({
508
+ moduleId: "Counter.tsx",
509
+ name: "Counter",
510
+ type: "around",
511
+ modulePath: "./src/wrap-counter.tsx",
512
+ }),
513
+ )
514
+ ```
515
+
516
+ The `type` field controls how the wrapper relates to the original:
517
+
518
+ * `replace` substitutes the original entirely.
519
+ * `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.
520
+ * `before` runs first with the original arguments.
521
+ * `after` runs last and receives the result followed by the original arguments. Returning a value other than `undefined` overrides the result.
522
+
523
+ `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).
524
+
525
+ ## Hot reloading
526
+
527
+ 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.
528
+
529
+
444
530
  # Plugins
445
531
  Source: https://zenbulabs.mintlify.app/core/plugins
446
532
 
@@ -465,19 +551,24 @@ export default defineConfig({
465
551
  schema: "./src/main/schema.ts",
466
552
  events: "./src/main/events.ts",
467
553
  migrations: "./migrations",
554
+ icons: {
555
+ "my-view": '<svg ...>...</svg>',
556
+ },
468
557
  }),
469
558
  ],
470
559
  })
471
560
  ```
472
561
 
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). |
562
+ | Field | Required | Description |
563
+ | ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
564
+ | `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. |
565
+ | `services` | yes | Glob patterns for main process service files. |
566
+ | `schema` | no | Path to the database schema definition. |
567
+ | `events` | no | Path to event type definitions. |
568
+ | `migrations` | no | Directory of generated migration files. |
569
+ | `preload` | no | Path to a renderer preload module. |
570
+ | `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). |
571
+ | `dependsOn` | no | Declares which other plugins this plugin needs type definitions from. See [Type dependencies](#type-dependencies). |
481
572
 
482
573
  ## Scaffolding a plugin
483
574
 
@@ -509,6 +600,39 @@ plugins: [
509
600
 
510
601
  Adding or removing a plugin in `zenbu.config.ts` is a hot-reloadable change that takes effect without restarting the app.
511
602
 
603
+ ## Local-only plugins (`localPlugins`)
604
+
605
+ 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:
606
+
607
+ ```typescript theme={null}
608
+ export default defineConfig({
609
+ uiEntrypoint: "./src/renderer",
610
+ plugins: [
611
+ "./plugins/app/zenbu.plugin.ts",
612
+ ],
613
+ localPlugins: "./zenbu.local.ts", // gitignored; optional
614
+ })
615
+ ```
616
+
617
+ ```typescript zenbu.local.ts theme={null}
618
+ import type { LocalPluginsDefault } from "@zenbujs/core/config"
619
+
620
+ const plugins: LocalPluginsDefault = [
621
+ "/Users/me/.zenbu/plugins/the-browser-plugin/zenbu.plugin.ts",
622
+ // or an inline definePlugin({...})
623
+ ]
624
+ export default plugins
625
+ ```
626
+
627
+ Rules:
628
+
629
+ * The default export is a plugin entry, or an array of entries. Same shape as `plugins`.
630
+ * Relative paths inside the overlay anchor to the **overlay file's** directory, not the project root.
631
+ * `localPlugins` accepts a single string or an array of strings (multiple overlays).
632
+ * If the overlay file doesn't exist, the field is silently ignored.
633
+ * Editing the overlay file hot-reloads exactly like editing `zenbu.config.ts`.
634
+ * `zen build:source` / `zen build:electron` / `zen publish:source` **skip** `localPlugins` entirely, so a developer's overlay can never ship in a build artefact.
635
+
512
636
  ## Plugin capabilities
513
637
 
514
638
  A plugin has the same capabilities as the host application:
@@ -516,9 +640,7 @@ A plugin has the same capabilities as the host application:
516
640
  * **Services** run in the main process and expose methods via RPC.
517
641
  * **Database schemas** define sections of the shared database.
518
642
  * **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.
643
+ * **[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
644
 
523
645
  ## Type dependencies
524
646
 
@@ -571,7 +693,7 @@ Under the hood, communication happens over a WebSocket, which means the same RPC
571
693
  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
694
 
573
695
  ```typescript theme={null}
574
- // Main process declared inside the `app` plugin
696
+ // Main process. Declared inside the `app` plugin.
575
697
  export class MathService extends Service.create({
576
698
  key: "math",
577
699
  }) {
@@ -689,16 +811,18 @@ Services declare dependencies on other services through the `deps` field. The fr
689
811
  import { Service } from "@zenbujs/core/runtime"
690
812
  import { WindowService } from "@zenbujs/core/services"
691
813
 
692
- export class AppService extends Service.create({
693
- key: "app",
814
+ export class InitService extends Service.create({
815
+ key: "init",
694
816
  deps: { window: WindowService },
695
817
  }) {
696
818
  async evaluate() {
697
- await this.ctx.window.openView({ type: "entrypoint" })
819
+ await this.ctx.window.openWindow({})
698
820
  }
699
821
  }
700
822
  ```
701
823
 
824
+ `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.
825
+
702
826
  By the time `evaluate()` runs, all dependencies in `this.ctx` are fully initialized.
703
827
 
704
828
  ## Setups and cleanups
@@ -841,13 +965,13 @@ Inside `update()`, you mutate the root object directly, the same way you would w
841
965
 
842
966
  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
967
 
844
- Define a collection in your schema with `f.collection()`:
968
+ Define a collection in your schema with `collection(...)`:
845
969
 
846
970
  ```typescript src/main/schema.ts theme={null}
847
- import { createSchema, f, z } from "@zenbujs/core/db"
971
+ import { createSchema, collection, z } from "@zenbujs/core/db"
848
972
 
849
973
  export default createSchema({
850
- messages: f.collection(
974
+ messages: collection(
851
975
  z.object({
852
976
  text: z.string(),
853
977
  author: z.string(),
@@ -896,13 +1020,13 @@ Collection data is only loaded into memory when you subscribe or read from it. T
896
1020
 
897
1021
  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
1022
 
899
- Define a blob in your schema with `f.blob()`:
1023
+ Define a blob in your schema with `blob(...)`:
900
1024
 
901
1025
  ```typescript src/main/schema.ts theme={null}
902
- import { createSchema, f, z } from "@zenbujs/core/db"
1026
+ import { createSchema, blob, z } from "@zenbujs/core/db"
903
1027
 
904
1028
  export default createSchema({
905
- avatar: f.blob({ debugName: "avatar" }),
1029
+ avatar: blob({ debugName: "avatar" }),
906
1030
  })
907
1031
  ```
908
1032
 
@@ -976,69 +1100,82 @@ Source: https://zenbulabs.mintlify.app/core/views
976
1100
 
977
1101
 
978
1102
 
979
- <Warning>The Views API is under construction</Warning>
1103
+ `<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
1104
 
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.
1105
+ 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.
1106
+
1107
+ ## Embedding a view
982
1108
 
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).
1109
+ ```tsx theme={null}
1110
+ import { View } from "@zenbujs/core/react"
984
1111
 
985
- Additional views are useful for:
1112
+ <View
1113
+ name="terminal"
1114
+ args={{ tabId }}
1115
+ fallback={<Spinner />}
1116
+ />
1117
+ ```
986
1118
 
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.
1119
+ | Prop | Meaning |
1120
+ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
1121
+ | `name` | Injection name to render. Required. |
1122
+ | `args` | Object forwarded as the `args` prop and on `useViewArgs()`. |
1123
+ | `visible` | When `false`, hides the wrapper via `display: none` without unmounting. Use it to preserve in-component state across visibility toggles. |
1124
+ | `fallback` | Rendered while no injection has registered under `name`. |
1125
+ | `className`, `style` | Forwarded to the wrapper. |
990
1126
 
991
- ## Registering a view
1127
+ If no injection has registered yet, `<View>` renders the `fallback`
1128
+ (an empty `<span data-zenbu-view-pending={name}>` if you don't pass
1129
+ one). Once a registration lands, it swaps in automatically.
992
1130
 
993
- From a service, use `ViewRegistryService` to register a view type:
1131
+ ## Reading view args
994
1132
 
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"
1133
+ Inside the rendered component, both the `args` prop and the
1134
+ `useViewArgs()` hook are populated with the same object:
1000
1135
 
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
- }
1136
+ ```tsx theme={null}
1137
+ import { useViewArgs, type ViewComponentProps } from "@zenbujs/core/react"
1138
+
1139
+ export default function TerminalApp({ args }: ViewComponentProps<{ tabId: string }>) {
1140
+ // Either of these works:
1141
+ const fromProps = args.tabId
1142
+ const fromHook = useViewArgs<{ tabId: string }>().tabId
1143
+ //
1015
1144
  }
1016
1145
  ```
1017
1146
 
1018
- The view directory must contain an `index.html` and a `main.tsx` that mounts a React tree wrapped in `<ZenbuProvider>`.
1147
+ `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
1148
 
1020
- ## Embedding a view
1021
-
1022
- ```tsx theme={null}
1023
- import { View } from "@zenbujs/core/react"
1149
+ ## Registering the component a view renders
1024
1150
 
1025
- <View type="terminal" args={{ tabId }} />
1026
- ```
1151
+ A view is just an injection. Use the API that fits your situation:
1027
1152
 
1028
- ## Reading view args
1153
+ * **Inside a service** (most plugins): `this.inject({ name, modulePath, meta })` from [Injections](/core/injections).
1154
+ * **From the React tree**: `useRegisterInjection(name, Component, meta)`.
1029
1155
 
1030
- Inside the child view, use `useViewArgs` to read the args passed by the parent:
1156
+ ```typescript theme={null}
1157
+ // From a service.
1158
+ this.setup("inject", () =>
1159
+ this.inject({
1160
+ name: "terminal",
1161
+ modulePath: "./src/views/terminal-view.tsx",
1162
+ meta: { kind: "bottom-panel", label: "Terminal" },
1163
+ }),
1164
+ )
1165
+ ```
1031
1166
 
1032
1167
  ```tsx theme={null}
1033
- import { useViewArgs } from "@zenbujs/core/react"
1034
-
1035
- function TerminalApp() {
1036
- const { tabId } = useViewArgs<{ tabId: string }>()
1037
- // ...
1038
- }
1168
+ // From React.
1169
+ useRegisterInjection("terminal", TerminalView, {
1170
+ kind: "bottom-panel",
1171
+ label: "Terminal",
1172
+ })
1039
1173
  ```
1040
1174
 
1041
- When the parent updates `args`, the hook re-renders with the new values.
1175
+ ## See also
1176
+
1177
+ * [Injections](/core/injections) for the full registration surface and `meta` conventions.
1178
+ * [Advice](/core/advice) for wrapping or replacing another plugin's view.
1042
1179
 
1043
1180
 
1044
1181
  # Releasing to Production
@@ -1254,15 +1391,15 @@ Source: https://zenbulabs.mintlify.app/introduction
1254
1391
 
1255
1392
 
1256
1393
 
1257
- Zenbu.js is a framework for building hackable desktop apps.
1394
+ Zenbu.js is the framework for building extensible applications, powering [Zenbu](https://zenbu.dev).
1258
1395
 
1259
- Apps built with Zenbu.js are designed to be modified after they ship. Users can edit the source code directly, or install plugins that extend the app with new functionality.
1396
+ Apps and plugins built with Zenbu.js are designed to be modified after they are installed. Users can edit the source code directly, or install plugins that extend the app with new functionality.
1260
1397
 
1261
- The framework also handles the hard parts of desktop development for you, like syncing state between processes, RPC, and hot-reloading everything as you make changes.
1398
+ The SDK also handles the hard parts of application development, like syncing state between processes, RPC, and hot-reloading everything as you make changes.
1262
1399
 
1263
- ### Why build a hackable app
1400
+ ### Why build with Zenbu.js
1264
1401
 
1265
- * Coding agents can generate and customize software on demand for a specific use case. A hackable app gives them full access to do that.
1402
+ * Coding agents can generate and customize software on demand for a specific use case. An app built on Zenbu.js gives them full access to do that.
1266
1403
  * Letting people modify your app means more directions get explored than you could reach on your own.
1267
1404
  * Extensible code tends to be more maintainable, because it's already written to be changed.
1268
1405
 
@@ -1386,9 +1523,9 @@ my-app/
1386
1523
  ├── tsconfig.json
1387
1524
  └── src/
1388
1525
  ├── main/
1389
- │ ├── schema.ts # Zod schema for the database
1390
1526
  │ └── services/
1391
- └── app.ts # Opens the main window on boot
1527
+ ├── init.ts # Opens the main window on boot
1528
+ │ └── cwd.ts # RPC service that returns process.cwd()
1392
1529
  └── renderer/
1393
1530
  ├── index.html
1394
1531
  ├── main.tsx
@@ -1401,18 +1538,18 @@ The `zenbu.config.ts` file is the entry point. It tells Zenbu.js where everythin
1401
1538
  import { defineConfig, definePlugin } from "@zenbujs/core/config"
1402
1539
 
1403
1540
  export default defineConfig({
1404
- db: "./.zenbu/db",
1405
1541
  uiEntrypoint: "./src/renderer",
1406
1542
  plugins: [
1407
1543
  definePlugin({
1408
1544
  name: "app",
1409
1545
  services: ["./src/main/services/*.ts"],
1410
- schema: "./src/main/schema.ts",
1411
1546
  }),
1412
1547
  ],
1413
1548
  })
1414
1549
  ```
1415
1550
 
1551
+ 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`.
1552
+
1416
1553
  ## Available scripts
1417
1554
 
1418
1555
  | Script | What it does |