@jskit-ai/agent-docs 0.1.18 → 0.1.20

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.
Files changed (29) hide show
  1. package/guide/agent/app-setup/initial-scaffolding.md +12 -8
  2. package/guide/agent/app-setup/multi-homing.md +59 -0
  3. package/guide/agent/app-setup/users.md +19 -4
  4. package/guide/agent/app-setup/working-with-the-jskit-cli.md +1 -0
  5. package/guide/agent/generators/crud-generators.md +210 -62
  6. package/guide/agent/generators/ui-generators.md +31 -0
  7. package/package.json +1 -1
  8. package/patterns/INDEX.md +3 -0
  9. package/patterns/client-requests.md +5 -0
  10. package/patterns/filters.md +4 -0
  11. package/patterns/page-redirects.md +5 -1
  12. package/patterns/server-search.md +211 -0
  13. package/reference/autogen/README.md +1 -0
  14. package/reference/autogen/packages/assistant-core.md +13 -0
  15. package/reference/autogen/packages/assistant-runtime.md +2 -0
  16. package/reference/autogen/packages/auth-provider-supabase-core.md +1 -0
  17. package/reference/autogen/packages/console-core.md +2 -0
  18. package/reference/autogen/packages/crud-core.md +12 -0
  19. package/reference/autogen/packages/crud-server-generator.md +1 -0
  20. package/reference/autogen/packages/crud-ui-generator.md +3 -0
  21. package/reference/autogen/packages/feature-server-generator.md +74 -0
  22. package/reference/autogen/packages/http-runtime.md +12 -6
  23. package/reference/autogen/packages/json-rest-api-core.md +3 -6
  24. package/reference/autogen/packages/kernel.md +1 -0
  25. package/reference/autogen/packages/ui-generator.md +2 -2
  26. package/reference/autogen/packages/users-web.md +23 -9
  27. package/reference/autogen/packages/workspaces-web.md +0 -1
  28. package/reference/autogen/tooling/create-app.md +4 -16
  29. package/reference/autogen/tooling/jskit-cli.md +14 -0
@@ -630,13 +630,11 @@ export default Object.freeze({
630
630
  kind: "runtime",
631
631
  runtime: {
632
632
  server: {
633
- providerEntrypoint: "src/server/index.js",
633
+ providerEntrypoint: "src/server/MainServiceProvider.js",
634
634
  providers: [
635
635
  {
636
- discover: {
637
- dir: "src/server/providers",
638
- pattern: "*Provider.js"
639
- }
636
+ entrypoint: "src/server/MainServiceProvider.js",
637
+ export: "MainServiceProvider"
640
638
  }
641
639
  ]
642
640
  }
@@ -651,12 +649,12 @@ export default Object.freeze({
651
649
 
652
650
  This is the moment where the scaffold stops looking like "just a Vue app". The app is declaring itself as a runtime package that JSKIT can discover, load, and mutate safely.
653
651
 
654
- For the server side, the main file to remember is `packages/main/src/server/providers/MainServiceProvider.js`. At the beginning it is almost empty, but that emptiness is useful. It means you already have a stable place to put backend runtime code as the app grows, instead of inventing ad hoc structure later.
652
+ For the server side, the main file to remember is `packages/main/src/server/MainServiceProvider.js`. It stays intentionally flat and small. That is the point: `packages/main` is the app-local composition package, not the default home for new backend feature trees.
655
653
 
656
654
  The server-side provider starts like this:
657
655
 
658
656
  ```js
659
- import { loadAppConfig } from "../support/loadAppConfig.js";
657
+ import { loadAppConfig } from "./loadAppConfig.js";
660
658
 
661
659
  class MainServiceProvider {
662
660
  static id = "local.main";
@@ -674,7 +672,13 @@ class MainServiceProvider {
674
672
  export { MainServiceProvider };
675
673
  ```
676
674
 
677
- It is deliberately small, but it already shows the pattern: register things with the app container first, then grow real backend behavior from there. The client side uses the same provider lifecycle; you already saw the matching pattern earlier in the client boot path.
675
+ It is deliberately small because it is only for app-local glue: loading config, wiring tiny app-specific behavior, and bootstrapping shared runtime concerns. When a backend capability becomes substantial, do not grow `packages/main` into a mini service tree. Generate a dedicated package instead:
676
+
677
+ ```bash
678
+ npx jskit generate feature-server-generator scaffold booking-engine
679
+ ```
680
+
681
+ That keeps the ownership boundary clear: `packages/main` stays composition-only, while real server features get their own provider, service, and optional repository seams. The client side uses the same provider lifecycle; you already saw the matching pattern earlier in the client boot path.
678
682
 
679
683
  The `.jskit/lock.json` file is also important. Treat it like JSKIT's own lock and state file. It records which runtime packages JSKIT believes are installed and which managed changes they introduced. When you use `jskit add`, `jskit update`, or generators that depend on installed package state, this file is part of the source of truth. It belongs in version control, and you should not hand-edit it.
680
684
 
@@ -47,6 +47,12 @@ npm install
47
47
  npm run db:migrate
48
48
  ```
49
49
 
50
+ **Important: This Block Is Only The Fresh Workspace Install Path**
51
+
52
+ These commands are complete only if the app was already on `tenancyMode = "personal"` when `users-web` / `users-core` were originally installed, or if you already ran the recovery `npx jskit update package users-core` step above.
53
+
54
+ If you changed tenancy after installing users, do not skip that update. The workspace packages add workspace runtime and routes, but `users-core` is what rewrites the app-local users scaffold into its workspace-aware shape.
55
+
50
56
  `workspaces-core` adds the server-side workspace runtime and schema migrations. `workspaces-web` adds the workspace-facing client surfaces, shell placements, and app-owned route files.
51
57
 
52
58
  If you want to inspect that package before installing it, this is a very good moment to use the CLI chapter's inspection command:
@@ -306,6 +312,30 @@ Concretely, that route tree works like this:
306
312
  - `w/[workspaceSlug]/admin/workspace/settings.vue` is a section shell. It does not own the actual settings fields. It owns the card frame, the left-side settings menu outlet, and the nested `<RouterView />` where child settings pages render.
307
313
  - `w/[workspaceSlug]/admin/workspace/settings/index.vue` is intentionally almost empty. Its job is to make `/admin/workspace/settings` a real route today and give you a clean place to redirect or add child settings pages later.
308
314
 
315
+ If you want to add a real workspace settings child page at this point, use the normal page generator under that route tree:
316
+
317
+ ```bash
318
+ npx jskit generate ui-generator page \
319
+ w/[workspaceSlug]/admin/workspace/settings/billing/index.vue \
320
+ --name "Billing"
321
+ ```
322
+
323
+ That command does two things:
324
+
325
+ - it creates `src/pages/w/[workspaceSlug]/admin/workspace/settings/billing/index.vue`
326
+ - it also appends the matching workspace settings menu entry into `src/placement.js`
327
+
328
+ The reason JSKIT can wire that link automatically is that the workspace settings shell already exposes a named placement outlet with a default link renderer:
329
+
330
+ ```vue
331
+ <ShellOutlet
332
+ target="admin-settings:primary-menu"
333
+ default-link-component-token="local.main.ui.surface-aware-menu-link-item"
334
+ />
335
+ ```
336
+
337
+ `target="admin-settings:primary-menu"` tells the generator which settings menu host owns those child links. `default-link-component-token="local.main.ui.surface-aware-menu-link-item"` tells it which local menu-link component to use unless you override it. So a page generated under `w/[workspaceSlug]/admin/workspace/settings/...` automatically lands in the left-side workspace settings menu without you hand-writing the placement entry.
338
+
309
339
  ### Workspace pages are prepared for missing-workspace states
310
340
 
311
341
  The starter workspace pages already use a dedicated unavailable-state helper:
@@ -558,6 +588,35 @@ That one block explains a lot of the new shell behavior.
558
588
  - the admin surface gets workspace tools in the top-right area
559
589
  - the admin workspace settings menu is now another nested placement host with both `Settings` and `Members`
560
590
 
591
+ If you want to add your own app-owned page into that top cog menu, first ask JSKIT which placement targets exist:
592
+
593
+ ```bash
594
+ npx jskit list-placements
595
+ ```
596
+
597
+ In a workspace-enabled app, that list now includes:
598
+
599
+ ```text
600
+ - admin-cog:primary-menu [package:@jskit-ai/workspaces-web:src/client/components/UsersWorkspaceToolsWidget.vue]
601
+ ```
602
+
603
+ If you want more context than the raw target list, `npx jskit show @jskit-ai/workspaces-web --details` also shows that same outlet plus the default `Settings` and `Members` entries already targeting it.
604
+
605
+ Once you know the outlet id, generate the page like this:
606
+
607
+ ```bash
608
+ npx jskit generate ui-generator page \
609
+ w/[workspaceSlug]/admin/catalogue/index.vue \
610
+ --name "Catalogue" \
611
+ --link-placement admin-cog:primary-menu
612
+ ```
613
+
614
+ That command creates `src/pages/w/[workspaceSlug]/admin/catalogue/index.vue` and appends the matching link entry into `src/placement.js`.
615
+
616
+ `--link-placement` is necessary here because this route is just a normal `admin` page. It is **not** a child page under a local host like `w/[workspaceSlug]/admin/workspace/settings.vue`, so the generator has no nested settings outlet to infer automatically. If you omit `--link-placement`, the new page link falls back to the app's default shell menu outlet instead of the cog menu.
617
+
618
+ You also do **not** need `--link-component-token` here. `admin-cog:primary-menu` already declares `local.main.ui.surface-aware-menu-link-item` as its default link renderer, so JSKIT reuses that automatically when it writes the placement entry.
619
+
561
620
  So the placement system from the shell chapter is still doing the same job as before. The app just has a richer routing and tenancy context now.
562
621
 
563
622
  ### The local client provider gets one more app-owned token
@@ -212,13 +212,17 @@ The account route itself is very small:
212
212
  <template>
213
213
  <AccountSettingsClientElement />
214
214
  </template>
215
+
216
+ <script setup>
217
+ import AccountSettingsClientElement from "@jskit-ai/users-web/client/components/AccountSettingsClientElement";
218
+ </script>
215
219
  ```
216
220
 
217
221
  That is a very JSKIT-style file.
218
222
 
219
223
  - the route policy is app-owned
220
224
  - the page wrapper is app-owned
221
- - the heavy UI is delegated to a reusable client element
225
+ - the heavy UI is delegated to a package-owned reusable client element
222
226
 
223
227
  So the route is simple, but it is already a real authenticated account screen rather than a placeholder card.
224
228
 
@@ -228,18 +232,21 @@ The account page is backed by:
228
232
 
229
233
  ```text
230
234
  src/components/account/settings/
231
- AccountSettingsClientElement.vue
232
235
  AccountSettingsProfileSection.vue
233
236
  AccountSettingsPreferencesSection.vue
234
237
  AccountSettingsNotificationsSection.vue
235
238
  ```
236
239
 
237
- The top-level `AccountSettingsClientElement.vue` uses `useAccountSettingsRuntime()` from `users-web` and wires those sections into a tabbed settings experience.
240
+ Those three section components stay app-owned so you can reshape the actual UI freely.
241
+
242
+ The host itself now lives in `users-web` and resolves every account section through the `account-settings:sections` placement target, including the default `profile`, `preferences`, and `notifications` entries.
243
+
244
+ So the screen follows the same rule as the rest of JSKIT UI: sections are added by placement rather than being hardcoded into an app-owned host component.
238
245
 
239
246
  That is worth noticing because the guide has now reached a new level of scaffolding:
240
247
 
241
248
  - earlier chapters mostly introduced shells and routes
242
- - this chapter introduces real app-owned feature UI that is already split into reusable sections
249
+ - this chapter introduces app-owned leaf section UI while the generic section host stays in the package
243
250
 
244
251
  ## Under the hood
245
252
 
@@ -336,6 +343,14 @@ Under the hood, `users-core` wires those contributors into the users-backed prof
336
343
 
337
344
  The next chapter uses exactly that pattern. `workspaces-core` registers a contributor so the workspace layer can react when a new user enters the system.
338
345
 
346
+ One forward-looking warning matters here. If this app later changes from `tenancyMode = "none"` to `personal` or `workspaces`, the app-local users scaffold written by `users-core` also needs to be refreshed. The multi-homing chapter calls out that recovery step explicitly with:
347
+
348
+ ```bash
349
+ npx jskit update package users-core
350
+ ```
351
+
352
+ That is not only a workspace-package concern. The generated `packages/users/...` scaffold itself changes shape when tenancy becomes workspace-aware.
353
+
339
354
  ## Summary
340
355
 
341
356
  This chapter is where the app stopped treating signed-in people as only Supabase identities and started treating them as real JSKIT users.
@@ -666,6 +666,7 @@ In the current CLI, that includes checks such as:
666
666
  - installed package entries in `.jskit/lock.json`
667
667
  - whether managed files recorded in the lock still exist
668
668
  - whether installed packages are still visible in the package registry
669
+ - explicit `transport` passed to high-level CRUD hooks such as `useCrudList()`, `useCrudView()`, and `useCrudAddEdit()`, where the shared CRUD resource should derive the JSON:API transport automatically
669
670
  - certain JSKIT-specific app checks, such as invalid raw `mdi-*` icon literals in Vue templates when the app uses Vuetify's `mdi-svg` iconset
670
671
  - UI verification receipts for current dirty UI files in git, via `.jskit/verification/ui.json`
671
672
 
@@ -16,8 +16,8 @@ So the real order is always:
16
16
  This chapter uses three examples, in increasing complexity:
17
17
 
18
18
  - `contacts`: the baseline full CRUD walkthrough
19
- - `addresses`: a child CRUD with its own routed list page
20
- - `comments`: a child CRUD that lives inside the parent page, with only the child routes being routed
19
+ - `addresses`: a child CRUD with its own routed list page reached from the parent record view
20
+ - `comments`: a child CRUD that lives under the parent record host as a child page
21
21
 
22
22
  The examples assume the guide app already has the database and workspace/admin setup from the earlier chapters. That is why the route roots below live under `w/[workspaceSlug]/admin/...`.
23
23
 
@@ -350,7 +350,7 @@ That is the shape to learn first.
350
350
 
351
351
  Now move to a child CRUD with its own URL.
352
352
 
353
- This is the right pattern when the child collection deserves its own list page.
353
+ This is the right pattern when the child collection deserves its own list page, but should still be reached from a specific parent record.
354
354
 
355
355
  Example route:
356
356
 
@@ -358,12 +358,23 @@ Example route:
358
358
  .../contacts/3/addresses
359
359
  ```
360
360
 
361
- In the guide app's real route tree, that becomes:
361
+ In the guide app's real route tree, the visible URL becomes:
362
362
 
363
363
  ```text
364
364
  w/[workspaceSlug]/admin/contacts/[contactId]/addresses
365
365
  ```
366
366
 
367
+ This example is intentionally **not** an in-page child list and **not** a contact subtab.
368
+
369
+ The intended UX is:
370
+
371
+ - `addresses` gets its own routed list page
372
+ - the page lives under a specific contact
373
+ - the user reaches it from an explicit link or button on the contact view
374
+ - it does **not** appear in the global left menu
375
+
376
+ That makes it different from the later `comments` example, where the child list stays inside the parent page.
377
+
367
378
  ### Step 1: create the child table
368
379
 
369
380
  Example:
@@ -382,12 +393,16 @@ CREATE TABLE addresses (
382
393
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
383
394
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
384
395
  KEY idx_addresses_workspace_id (workspace_id),
385
- KEY idx_addresses_contact_id (contact_id)
396
+ KEY idx_addresses_contact_id (contact_id),
397
+ CONSTRAINT fk_addresses_contact_id
398
+ FOREIGN KEY (contact_id) REFERENCES contacts(id)
386
399
  );
387
400
  ```
388
401
 
389
402
  The important extra column here is `contact_id`. This is what makes the CRUD a child of a contact instead of a top-level resource.
390
403
 
404
+ The foreign key is not optional in this example. It is what lets `crud-server-generator` emit `contactId` as a real lookup relation in `packages/addresses/src/shared/addressResource.js`.
405
+
391
406
  ### Step 2: scaffold the server package
392
407
 
393
408
  ```bash
@@ -398,17 +413,104 @@ npx jskit generate crud-server-generator scaffold \
398
413
  --table-name addresses
399
414
  ```
400
415
 
401
- ### Step 3: scaffold the UI at the child route root
416
+ Then install the generated local package:
417
+
418
+ ```bash
419
+ npm install
420
+ ```
421
+
422
+ If you are developing against a local JSKIT checkout, relink your app to the local packages before you run the UI:
423
+
424
+ ```bash
425
+ npm run devlinks
426
+ ```
427
+
428
+ ### Step 3: refine the generated lookup metadata by hand
429
+
430
+ Open `packages/addresses/src/shared/addressResource.js` and add `labelKey: "fullName"` to the generated `contactId` relation:
431
+
432
+ ```js
433
+ contactId: {
434
+ type: "id",
435
+ required: true,
436
+ search: true,
437
+ relation: {
438
+ kind: "lookup",
439
+ namespace: "contacts",
440
+ valueKey: "id",
441
+ labelKey: "fullName"
442
+ },
443
+ belongsTo: "contacts",
444
+ as: "contact",
445
+ ui: { formControl: "autocomplete" },
446
+ ...
447
+ }
448
+ ```
449
+
450
+ This part is still manual today.
451
+
452
+ The scaffold can infer the lookup **relationship** from the foreign key, but it cannot infer which contact field should be used as the human-readable label. Without `labelKey: "fullName"`, the page falls back to generic headings like `Addresses for Contact #1`.
453
+
454
+ ### Step 4: scaffold the UI route tree
402
455
 
403
456
  ```bash
404
457
  npx jskit generate crud-ui-generator crud \
405
458
  w/[workspaceSlug]/admin/contacts/[contactId]/addresses \
406
459
  --resource-file packages/addresses/src/shared/addressResource.js \
407
460
  --id-param addressId \
461
+ --parent-title contextual \
408
462
  --display-fields label,line1,postcode
409
463
  ```
410
464
 
411
- That gives you a proper child CRUD with its own list, view, new, and edit pages under the contact route.
465
+ That gives you a normal child route tree:
466
+
467
+ - `w/[workspaceSlug]/admin/contacts/[contactId]/addresses/index.vue`
468
+ - `w/[workspaceSlug]/admin/contacts/[contactId]/addresses/new.vue`
469
+ - `w/[workspaceSlug]/admin/contacts/[contactId]/addresses/[addressId]/index.vue`
470
+ - `w/[workspaceSlug]/admin/contacts/[contactId]/addresses/[addressId]/edit.vue`
471
+ - shared `_components` files under the same route root
472
+
473
+ ### Step 5: remove the generated shell placement by hand
474
+
475
+ `crud-ui-generator` currently generates a list-page placement block in `src/placement.js`.
476
+
477
+ For this example, that block is **not** what you want.
478
+
479
+ Delete the generated `addresses` placement block from `src/placement.js`.
480
+
481
+ It will be the block added for the `w/[workspaceSlug]/admin/contacts/[contactId]/addresses` page link.
482
+
483
+ Depending on what outlets already exist in the app, that block may try to place `Addresses` in the shell menu or in the contact subpages outlet. For this example, remove it either way and keep navigation manual from the contact view.
484
+
485
+ This is an intentional manual cleanup for now:
486
+
487
+ - keep the routed pages
488
+ - remove the auto menu link
489
+ - navigate to the page from the contact view instead
490
+
491
+ ### Step 6: add a manual link from the contact view
492
+
493
+ Open `src/pages/w/[workspaceSlug]/admin/contacts/[contactId]/index.vue` and add a button that links to `./addresses`.
494
+
495
+ The simplest version is:
496
+
497
+ ```vue
498
+ <v-btn
499
+ color="primary"
500
+ variant="tonal"
501
+ :to="{ path: view.resolveParams(UI_ADDRESSES_URL), query: $route.query }"
502
+ >
503
+ Addresses
504
+ </v-btn>
505
+ ```
506
+
507
+ Then define the URL template in the script:
508
+
509
+ ```js
510
+ const UI_ADDRESSES_URL = "./addresses";
511
+ ```
512
+
513
+ That gives you a standalone child page with a clear entry point from the parent contact view, without mixing record-scoped pages into the global shell menu.
412
514
 
413
515
  ### The important refinement: parent title handling
414
516
 
@@ -416,8 +518,8 @@ This is where nested CRUD starts to feel real.
416
518
 
417
519
  An addresses list page should not show a generic heading like `Addresses`. It should usually show whose addresses these are, for example:
418
520
 
419
- - `Jane Smith's addresses`
420
- - `Acme Pty Ltd's addresses`
521
+ - `Addresses for Jane Smith`
522
+ - `Addresses for Acme Pty Ltd`
421
523
 
422
524
  The awkward case is when there are **no child rows yet**. If the list is empty, you cannot derive the parent title from the first address record.
423
525
 
@@ -433,37 +535,36 @@ const parentTitle = useCrudListParentTitle({
433
535
  });
434
536
  ```
435
537
 
436
- That helper does the nice thing automatically:
538
+ That helper does the right thing automatically:
437
539
 
438
- - if the child list already has rows, it derives the parent title from the first child record
540
+ - if the child list already has rows, it derives the parent title from the first child record when lookup data is available
439
541
  - if the child list is empty, it loads the parent record directly
440
542
 
441
- This is exactly the pattern used in the real app codebase for nested CRUD lists like a contact's pets. It avoids the common broken state where an empty child list loses all context about the parent it belongs to.
442
-
443
- So the `addresses` example is important because it teaches the first truly practical nested-CRUD problem:
543
+ So the `addresses` example is important because it teaches the first truly practical child-page problem:
444
544
 
445
545
  - the route is child-scoped
446
546
  - the data is child-scoped
447
- - but the page still needs a reliable parent identity even when the child list is empty
547
+ - the page is reached from a parent action, not from the main shell menu
548
+ - the page still needs a reliable parent identity even when the child list is empty
448
549
 
449
- ## Example 3: `comments` in-page
550
+ ## Example 3: `comments` under the contact host
450
551
 
451
552
  This is the advanced example.
452
553
 
453
- Sometimes a child resource is real enough to deserve its own CRUD operations, but **not** important enough to deserve its own full-screen list page.
554
+ Sometimes a child resource should stay attached to the parent record, but still deserves its own routed child page.
454
555
 
455
556
  Comments are a good example:
456
557
 
457
558
  - they matter
458
559
  - they need persistence
459
- - they may need `view`, `new`, or `edit` routes
460
- - but the main screen is still the contact view page
560
+ - they benefit from their own list, `new`, `view`, and `edit` routes
561
+ - but they still belong inside the contact experience rather than as a top-level destination
461
562
 
462
563
  So the pattern is:
463
564
 
464
565
  - keep the contact page as the real host
465
- - render the comments list inside that page
466
- - route only the child operations underneath it
566
+ - keep the child route tree under that host
567
+ - let the generated placement surface the comments page as a contact child page
467
568
 
468
569
  ### Step 1: create the table
469
570
 
@@ -474,12 +575,13 @@ CREATE TABLE comments (
474
575
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
475
576
  workspace_id BIGINT UNSIGNED NOT NULL,
476
577
  contact_id BIGINT UNSIGNED NOT NULL,
477
- author_user_id BIGINT UNSIGNED NULL,
478
578
  body TEXT NOT NULL,
479
579
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
480
580
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
481
581
  KEY idx_comments_workspace_id (workspace_id),
482
- KEY idx_comments_contact_id (contact_id)
582
+ KEY idx_comments_contact_id (contact_id),
583
+ CONSTRAINT fk_comments_contact_id
584
+ FOREIGN KEY (contact_id) REFERENCES contacts(id)
483
585
  );
484
586
  ```
485
587
 
@@ -493,7 +595,45 @@ npx jskit generate crud-server-generator scaffold \
493
595
  --table-name comments
494
596
  ```
495
597
 
496
- ### Step 3: make the parent page able to host routed children
598
+ Then install the generated local package:
599
+
600
+ ```bash
601
+ npm install
602
+ ```
603
+
604
+ If you are developing against a local JSKIT checkout, relink the app before you run the UI:
605
+
606
+ ```bash
607
+ npm run devlinks
608
+ ```
609
+
610
+ ### Step 3: refine the generated lookup metadata by hand
611
+
612
+ Open `packages/comments/src/shared/commentResource.js` and add `labelKey: "fullName"` to the generated `contactId` relation:
613
+
614
+ ```js
615
+ contactId: {
616
+ type: "id",
617
+ required: true,
618
+ search: true,
619
+ relation: {
620
+ kind: "lookup",
621
+ namespace: "contacts",
622
+ valueKey: "id",
623
+ labelKey: "fullName"
624
+ },
625
+ belongsTo: "contacts",
626
+ as: "contact",
627
+ ui: { formControl: "autocomplete" },
628
+ ...
629
+ }
630
+ ```
631
+
632
+ This is the same manual refinement used in the `addresses` example: the scaffold can infer the lookup relationship from the foreign key, but it cannot infer which parent field should be used as the display label.
633
+
634
+ The next command also makes the parent-title mode explicit on purpose. That way changing the heading behavior later is just changing the value and rerunning with `--force`, not adding a new flag.
635
+
636
+ ### Step 4: make the parent page able to host routed children
497
637
 
498
638
  If the contact view page should stay visible while child comment routes render underneath it, first upgrade it into a routed host:
499
639
 
@@ -509,63 +649,71 @@ That is the point where the comments example deliberately overlaps with the `ui-
509
649
  - CRUD scaffolding for the comments resource
510
650
  - a routed host in the parent contact page
511
651
 
512
- ### Step 4: generate only the routed child operations
652
+ ### Step 4.5: make `comments` the default child page
513
653
 
514
- Now generate the comments UI without a list page:
654
+ This is a very common next step.
515
655
 
516
- ```bash
517
- npx jskit generate crud-ui-generator crud \
518
- w/[workspaceSlug]/admin/contacts/[contactId]/index/comments \
519
- --resource-file packages/comments/src/shared/commentResource.js \
520
- --operations view,new,edit \
521
- --id-param commentId
656
+ If the contact page should open directly on `Comments`, add an explicit redirect to the contact host page:
657
+
658
+ ```vue
659
+ <script setup>
660
+ import { redirectToChild } from "@jskit-ai/kernel/client/pageRedirects";
661
+
662
+ definePage({
663
+ redirect: redirectToChild("comments")
664
+ });
665
+ </script>
522
666
  ```
523
667
 
524
- This is the key move.
668
+ In this example, that edit belongs in:
525
669
 
526
- By leaving `list` out:
670
+ ```text
671
+ src/pages/w/[workspaceSlug]/admin/contacts/[contactId]/index.vue
672
+ ```
527
673
 
528
- - you do **not** generate a standalone comments list page
529
- - the host contact page stays responsible for showing the comment list
530
- - the generated child routes still handle things like `new`, `view`, and `edit`
674
+ This is the recommended pattern. Do **not** try to infer the default child page from placement order or "the first tab". Keep it explicit.
531
675
 
532
- ### What the host page does
676
+ When this redirect is present:
533
677
 
534
- The host page then renders the comment list directly, usually with lower-level CRUD composables such as:
678
+ - opening `/w/<workspaceSlug>/admin/contacts/<contactId>` lands on `/w/<workspaceSlug>/admin/contacts/<contactId>/comments`
679
+ - the parent contact page still stays visible
680
+ - the child page renders underneath it
535
681
 
536
- - `useCrudList()`
537
- - `useCrudAddEdit()`
538
- - `useCrudView()` when needed
682
+ That last point matters. The contact page is still the host page. The redirect only changes which child route becomes the default landing destination.
539
683
 
540
- There is one more runtime worth knowing about early:
684
+ ### Step 5: generate the full child CRUD page
541
685
 
542
- - `useCommand()`
686
+ Now generate the comments UI under the contact host:
543
687
 
544
- Use it for live actions that are **not** full forms, such as:
688
+ ```bash
689
+ npx jskit generate crud-ui-generator crud \
690
+ w/[workspaceSlug]/admin/contacts/[contactId]/index/comments \
691
+ --resource-file packages/comments/src/shared/commentResource.js \
692
+ --id-param commentId \
693
+ --parent-title none \
694
+ --display-fields body
695
+ ```
545
696
 
546
- - checkbox toggles
547
- - archive / reopen buttons
548
- - quick PATCH / POST / DELETE actions on one record
697
+ Do **not** pass `--operations view,new,edit` here. That would omit the list page and leave you with only child operation routes.
549
698
 
550
- So the practical split is:
699
+ `--parent-title none` is the important difference from `addresses`.
551
700
 
552
- - `useCrudList()` / `useCrudView()`
553
- - routed list/view loading
554
- - `useCrudAddEdit()`
555
- - real create/edit forms
556
- - `useCommand()`
557
- - live actions inside the page
701
+ The contact host page already shows the parent identity, so a generated heading like `Comments for Tony Mobily` is redundant. If you later decide you do want that heading, rerun the same command with `--parent-title contextual --force`.
558
702
 
559
- The `todo` app uses that last pattern for "mark item done" checkboxes. The deeper best-practices explanation is in [Advanced CRUDs](/guide/generators/advanced-cruds).
703
+ This command gives you:
560
704
 
561
- That is the same general pattern already used in the real app for embedded child records like pet notes: the record list lives inside the main view page, while routed child pages handle the operations that still deserve their own URLs.
705
+ - `w/[workspaceSlug]/admin/contacts/[contactId]/index/comments/index.vue`
706
+ - `w/[workspaceSlug]/admin/contacts/[contactId]/index/comments/new.vue`
707
+ - `w/[workspaceSlug]/admin/contacts/[contactId]/index/comments/[commentId]/index.vue`
708
+ - `w/[workspaceSlug]/admin/contacts/[contactId]/index/comments/[commentId]/edit.vue`
562
709
 
563
- This pattern is useful when the child records are supporting detail rather than a destination in their own right.
710
+ Because this route root lives under `.../[contactId]/index/comments`, the generated placement is the desired one here: `Comments` appears as a child page under the contact host.
564
711
 
565
712
  That is the real lesson of the `comments` example:
566
713
 
567
- - a child CRUD does **not** have to mean "make another full-screen list page"
568
- - you can keep the parent page as the main experience and route only the child operations that benefit from their own URLs
714
+ - the contact page stays the routed host
715
+ - the child CRUD gets its own list page
716
+ - the route still stays under the parent record experience
569
717
 
570
718
  ## When `scaffold-field` matters
571
719
 
@@ -597,8 +745,8 @@ It patches the canonical `schema` inside the shared resource file. That matters
597
745
  The important thing is not memorizing commands. It is learning the three shapes:
598
746
 
599
747
  - `contacts`: a normal top-level CRUD
600
- - `addresses`: a child CRUD with its own routed list page
601
- - `comments`: a child CRUD that lives inside the parent page, with only the child operations routed
748
+ - `addresses`: a child CRUD with its own routed list page reached from the parent record view
749
+ - `comments`: a child CRUD that lives under the parent record host as a child page
602
750
 
603
751
  Once you understand those three shapes, the two generator packages stop feeling like separate tools.
604
752
 
@@ -257,6 +257,37 @@ The route page is still a normal page file. What changes is the inferred placeme
257
257
 
258
258
  So JSKIT is not switching to a different generator. It is switching the inferred placement behavior because the route tree now has a routed host above the new page.
259
259
 
260
+ ### Making a child page the default landing route
261
+
262
+ This is important enough to state explicitly: if a routed host has child pages, and you want the bare parent URL to open one child immediately, use an explicit redirect.
263
+
264
+ Do **not** try to make the app "guess the first tab". Do **not** infer it from placement order. Keep the target explicit.
265
+
266
+ The standard pattern is:
267
+
268
+ ```vue
269
+ <script setup>
270
+ import { redirectToChild } from "@jskit-ai/kernel/client/pageRedirects";
271
+
272
+ definePage({
273
+ redirect: redirectToChild("exports")
274
+ });
275
+ </script>
276
+ ```
277
+
278
+ If this is placed on the host page, opening `/reports` lands on `/reports/exports`.
279
+
280
+ This is also the right pattern when the host page still renders shared content such as a title, tabs, summary panel, or `RouterView`. The parent route remains the host, and the child page simply becomes the default destination under it.
281
+
282
+ Why this is the recommended pattern:
283
+
284
+ - the destination is explicit and stable
285
+ - it survives later placement reordering
286
+ - it does not depend on which child link happens to render first
287
+ - it is easy to change later by editing one child segment
288
+
289
+ This is one of the most common things people want once they start using child-page hosts. Treat it as normal JSKIT routing, not a special hack.
290
+
260
291
  ## Nested pages under an `index.vue` host
261
292
 
262
293
  Once `Reports` is a host, create a child page under it:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/agent-docs",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "Distributed JSKIT agent workflows, guides, and generated reference maps.",
5
5
  "type": "module",
6
6
  "files": [