@cat-factory/app 0.207.0 → 0.208.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -92
- package/app/components/fragments/FragmentLibraryManager.vue +4 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +18 -6
- package/app/components/layout/IntegrationsHub.vue +3 -1
- package/app/components/pipeline/PipelineBuilder.vue +9 -2
- package/app/components/tutorial/TutorialPrompt.vue +20 -12
- package/app/composables/useTutorialTours.ts +9 -5
- package/app/docs/architecture.md +4 -4
- package/app/docs/consumer-extensions.md +47 -47
- package/app/modular/nav-contributions.ts +21 -9
- package/app/modular/tutorial-tours.spec.ts +190 -11
- package/app/modular/tutorial-tours.ts +226 -5
- package/app/utils/tutorial.spec.ts +19 -0
- package/app/utils/tutorial.ts +31 -1
- package/i18n/locales/de.json +101 -2
- package/i18n/locales/en.json +101 -2
- package/i18n/locales/es.json +101 -2
- package/i18n/locales/fr.json +101 -2
- package/i18n/locales/he.json +101 -2
- package/i18n/locales/it.json +101 -2
- package/i18n/locales/ja.json +101 -2
- package/i18n/locales/pl.json +101 -2
- package/i18n/locales/tr.json +101 -2
- package/i18n/locales/uk.json +101 -2
- package/package.json +2 -2
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// The tutorial launch prompt: asks once on first launch whether the user wants a guided tour,
|
|
3
|
-
// listing the tours this board can actually run right now (first-party + consumer,
|
|
4
|
-
// against the same gates the nav uses), so it grows with the catalog rather than
|
|
5
|
-
// tours.
|
|
3
|
+
// listing the first-run tours this board can actually run right now (first-party + consumer,
|
|
4
|
+
// resolved against the same gates the nav uses), so it grows with the catalog rather than
|
|
5
|
+
// hard-coding tours.
|
|
6
6
|
//
|
|
7
|
-
// It is the OFFER, not the library: the full list —
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
7
|
+
// It is the OFFER, not the library: the full list — the platform walkthroughs that are kept out
|
|
8
|
+
// of this question (`offeredAtLaunch: false`), plus the ones this board can't run yet and what
|
|
9
|
+
// would unlock them — is `TutorialCatalogue.vue`, one button away in the footer and permanently
|
|
10
|
+
// reachable from the sidebar's Help section. That split is why this stays a short, answerable
|
|
11
|
+
// question instead of growing into a browsing surface, and why it reads `offered` rather than
|
|
12
|
+
// every startable tour.
|
|
11
13
|
//
|
|
12
14
|
// The decision semantics live in the store: starting a tour or "No thanks" is SAVED (the
|
|
13
15
|
// prompt never auto-opens again), while closing without answering defers to next launch.
|
|
@@ -15,7 +17,7 @@ import { TUTORIAL_ACTION_KEYS } from '~/utils/tutorial'
|
|
|
15
17
|
|
|
16
18
|
const { t } = useI18n()
|
|
17
19
|
const tutorial = useTutorialStore()
|
|
18
|
-
const {
|
|
20
|
+
const { offered } = useTutorialTours()
|
|
19
21
|
// Start / Resume / Repeat is decided in one place for both surfaces — see `useTutorialLaunch`.
|
|
20
22
|
const { actionFor, launch } = useTutorialLaunch()
|
|
21
23
|
|
|
@@ -41,7 +43,7 @@ const undecided = computed(() => tutorial.decision === null)
|
|
|
41
43
|
<p class="text-sm text-slate-300">{{ t('tutorial.prompt.intro') }}</p>
|
|
42
44
|
<ul class="space-y-2">
|
|
43
45
|
<li
|
|
44
|
-
v-for="tour in
|
|
46
|
+
v-for="tour in offered"
|
|
45
47
|
:key="tour.id"
|
|
46
48
|
class="flex items-center gap-3 rounded-lg border border-slate-800 bg-slate-900/60 p-3"
|
|
47
49
|
>
|
|
@@ -75,9 +77,15 @@ const undecided = computed(() => tutorial.decision === null)
|
|
|
75
77
|
</UButton>
|
|
76
78
|
</li>
|
|
77
79
|
</ul>
|
|
78
|
-
<!-- Every tour gated away
|
|
79
|
-
than
|
|
80
|
-
|
|
80
|
+
<!-- Every first-run tour gated away: say so rather than showing an unexplained empty
|
|
81
|
+
list. The copy points at the catalogue rather than declaring the deployment empty,
|
|
82
|
+
because with the split this state no longer implies there is nothing to take: what
|
|
83
|
+
is missing is the FIRST-RUN arc, and the catalogue-only walkthroughs gate on a
|
|
84
|
+
permission that this user may well hold. (Unreachable with the built-in catalog
|
|
85
|
+
alone, where `board-basics` requires nothing at all — but a consumer's own slot
|
|
86
|
+
filter can produce it, and "no tours exist" would be the wrong thing to say then.)
|
|
87
|
+
The footer's browse button is the way on, so it stays. -->
|
|
88
|
+
<p v-if="offered.length === 0" class="text-sm text-slate-400">
|
|
81
89
|
{{ t('tutorial.prompt.empty') }}
|
|
82
90
|
</p>
|
|
83
91
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { computed } from 'vue'
|
|
2
2
|
import { useReactiveSlots } from '@modular-vue/runtime'
|
|
3
3
|
import { createSharedComposables } from '@modular-vue/vue'
|
|
4
|
-
import { resolveTourCatalogue } from '~/utils/tutorial'
|
|
4
|
+
import { isLaunchOffer, resolveTourCatalogue } from '~/utils/tutorial'
|
|
5
5
|
import type { TutorialCatalogueEntry, TutorialTour } from '~/utils/tutorial'
|
|
6
6
|
import type { AppDeps } from '~/modular/registry'
|
|
7
7
|
import type { AppSlots } from '~/modular/nav-contributions'
|
|
@@ -21,10 +21,13 @@ const { useOptional } = createSharedComposables<AppDeps>()
|
|
|
21
21
|
/**
|
|
22
22
|
* The tutorial catalog as this board sees it, resolved ONCE for every surface that reads it.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
24
|
+
* Three views over one resolution, and the differences between them are the point:
|
|
25
25
|
*
|
|
26
|
-
* - `tours` — what can be started right now. The
|
|
27
|
-
*
|
|
26
|
+
* - `tours` — what can be started right now. The overlay resolves a running tour from these,
|
|
27
|
+
* exactly as when this gating lived in `navSlotFilter`.
|
|
28
|
+
* - `offered` — the subset the launch prompt asks about: startable AND part of the first-run
|
|
29
|
+
* arc (see `TutorialTour.offeredAtLaunch`). The prompt is one answerable question, so it
|
|
30
|
+
* stays the delivery loop even as the catalog grows to cover the platform surfaces.
|
|
28
31
|
* - `catalogue` — EVERY tour this deployment ships, each carrying why it is or isn't
|
|
29
32
|
* available. The catalogue surface needs the unavailable ones: a list that quietly omits
|
|
30
33
|
* four of six tours is indistinguishable from a deployment that ships two, and the user it
|
|
@@ -42,5 +45,6 @@ export function useTutorialTours() {
|
|
|
42
45
|
const tours = computed<TutorialTour[]>(() =>
|
|
43
46
|
catalogue.value.filter((entry) => entry.availability === 'ready').map((entry) => entry.tour),
|
|
44
47
|
)
|
|
45
|
-
|
|
48
|
+
const offered = computed<TutorialTour[]>(() => tours.value.filter(isLaunchOffer))
|
|
49
|
+
return { tours, offered, catalogue }
|
|
46
50
|
}
|
package/app/docs/architecture.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Frontend architecture
|
|
1
|
+
# Frontend architecture: state & data flow
|
|
2
2
|
|
|
3
3
|
How the SPA stays in sync with the backend. The app is a **thin client**: it holds
|
|
4
4
|
no business logic, calls the Worker for every mutation, and hydrates its stores
|
|
@@ -14,11 +14,11 @@ REST (useApi) ─────────────▶ Worker ────
|
|
|
14
14
|
stores (Pinia) ◀── patch ── useWorkspaceStream ◀── WebSocket push (events hub)
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
- **Read path
|
|
17
|
+
- **Read path**: the `workspace` store loads the full snapshot and fans it into
|
|
18
18
|
`board`, `pipelines`, `execution`, `spend`, etc.
|
|
19
|
-
- **Write path
|
|
19
|
+
- **Write path**: components call `useApi` → Worker; the response (or a pushed
|
|
20
20
|
event) patches the relevant store. No optimistic business logic.
|
|
21
|
-
- **Live path
|
|
21
|
+
- **Live path**: `useWorkspaceStream` opens one WebSocket to
|
|
22
22
|
`GET /workspaces/:ws/events?token=…`, patches `execution` / `agentRuns` /
|
|
23
23
|
`board` as events arrive, and refreshes on reconnect to reconcile anything
|
|
24
24
|
missed.
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# Extending the SPA from a consumer deployment
|
|
2
2
|
|
|
3
3
|
A deployment that consumes this layer (`extends: ['@cat-factory/app']`) can contribute
|
|
4
|
-
its own **components**
|
|
5
|
-
palette data
|
|
4
|
+
its own **components** (result windows, navigation entries, inspector panels, agent-kind
|
|
5
|
+
palette data) **without forking the layer**. This is the frontend counterpart of the
|
|
6
6
|
backend's public registries (`registerAgentKind`, `registerGate`; see
|
|
7
7
|
[`backend/docs/custom-agents.md`](../../../backend/docs/custom-agents.md)). The governing
|
|
8
8
|
principle is the same: **zero host edits for a consumer extension**.
|
|
9
9
|
|
|
10
|
-
A worked, end-to-end example ships in the template deployment
|
|
11
|
-
[`deploy/frontend/app/`](../../../deploy/frontend) (the `acme:security` module)
|
|
10
|
+
A worked, end-to-end example ships in the template deployment:
|
|
11
|
+
[`deploy/frontend/app/`](../../../deploy/frontend) (the `acme:security` module): the
|
|
12
12
|
frontend analogue of the backend
|
|
13
13
|
[`@cat-factory/example-custom-agent`](../../../backend/internal/example-custom-agent)
|
|
14
14
|
package. Read this guide alongside it.
|
|
@@ -31,13 +31,13 @@ import AcmeSecurityReport from '../components/acme/AcmeSecurityReport.vue'
|
|
|
31
31
|
export default defineNuxtPlugin(() => {
|
|
32
32
|
registerAppModule(
|
|
33
33
|
defineModule({
|
|
34
|
-
id: 'acme:security', // namespaced
|
|
34
|
+
id: 'acme:security', // namespaced - see "Rules" below
|
|
35
35
|
version: '1.0.0',
|
|
36
36
|
slots: {
|
|
37
37
|
resultViews: [{ id: 'acme:security-report', component: AcmeSecurityReport }],
|
|
38
|
-
agentKinds: [/* palette entries
|
|
39
|
-
nav: [/* sidebar / command-palette destinations
|
|
40
|
-
inspectorPanels: [/* per-block detail panels
|
|
38
|
+
agentKinds: [/* palette entries - see "Agent kinds" */],
|
|
39
|
+
nav: [/* sidebar / command-palette destinations - see "Navigation" */],
|
|
40
|
+
inspectorPanels: [/* per-block detail panels - see "Inspector panels" */],
|
|
41
41
|
},
|
|
42
42
|
}),
|
|
43
43
|
)
|
|
@@ -49,9 +49,9 @@ export default defineNuxtPlugin(() => {
|
|
|
49
49
|
- **`enforce: 'post'` is load-bearing.** The layer's own install plugin is `enforce:
|
|
50
50
|
'post'`, and Nuxt runs layer plugins before the consuming app's plugins within one
|
|
51
51
|
enforce bucket. So your registration plugin must run in the **default** (or `pre`) bucket
|
|
52
|
-
|
|
52
|
+
, i.e. **do not** put `enforce: 'post'` on it, or it registers too late and is silently
|
|
53
53
|
missed.
|
|
54
|
-
- **`defineModule` / the slot-entry types come from `@modular-vue/core
|
|
54
|
+
- **`defineModule` / the slot-entry types come from `@modular-vue/core`**: add it to your
|
|
55
55
|
deployment's `dependencies`.
|
|
56
56
|
|
|
57
57
|
## The landed seams
|
|
@@ -70,7 +70,7 @@ export default defineNuxtPlugin(() => {
|
|
|
70
70
|
| Locale strings | (i18n) | `i18n/locales/*.json` in the deployment | `@nuxtjs/i18n` layer deep-merge |
|
|
71
71
|
|
|
72
72
|
A `nav` entry may also declare `advanced: true`, which hides it in **basic** interface mode
|
|
73
|
-
(the shipped default) exactly as it does for the first-party destinations
|
|
73
|
+
(the shipped default) exactly as it does for the first-party destinations: see
|
|
74
74
|
[the layer README](../../README.md#interface-modes-basic--advanced). Use it for a power-user
|
|
75
75
|
destination; the flag is independent of `gate`, so both must pass for the item to render.
|
|
76
76
|
|
|
@@ -80,7 +80,7 @@ Backend data selects a frontend component, joined by a namespaced id:
|
|
|
80
80
|
|
|
81
81
|
1. A backend agent kind (registered on `AgentKindRegistry`, e.g.
|
|
82
82
|
`@cat-factory/example-custom-agent`'s `security-auditor`) arrives in the workspace
|
|
83
|
-
snapshot with `presentation.resultView: '<ns>:<name>'
|
|
83
|
+
snapshot with `presentation.resultView: '<ns>:<name>'`: **or** you code-ship the kind's
|
|
84
84
|
palette entry via the `agentKinds` slot (as the example does, to give an existing kind a
|
|
85
85
|
bespoke window).
|
|
86
86
|
2. You contribute the component to `resultViews` under the SAME id.
|
|
@@ -107,7 +107,7 @@ nullish subject (the boot-time validation resolve passes `null`).
|
|
|
107
107
|
|
|
108
108
|
### External tools + workspace metadata (`externalTools`, `workspaceMetadataFields`)
|
|
109
109
|
|
|
110
|
-
Put your OWN web applications
|
|
110
|
+
Put your OWN web applications (a map editor, an asset pipeline, an admin console) in the
|
|
111
111
|
sidebar's **External tools** section, and open each one _already scoped to what the user is
|
|
112
112
|
looking at_. That second half is the point of the seam; a static link needs no registration.
|
|
113
113
|
|
|
@@ -132,25 +132,25 @@ workspaceMetadataFields: [{ key: 'gameId', label: 'Game id', placeholder: 'zork'
|
|
|
132
132
|
```
|
|
133
133
|
|
|
134
134
|
- **`url` is a string or a RESOLVER** `(ctx) => string | null`. The context carries `userId`,
|
|
135
|
-
`userEmail`, `workspaceId`, `workspaceName` and `metadata
|
|
135
|
+
`userEmail`, `workspaceId`, `workspaceName` and `metadata`: the custom workspace fields you
|
|
136
136
|
declared. It is read at CLICK time, so a value a teammate fills in while the sidebar is open
|
|
137
137
|
takes effect without a reload.
|
|
138
138
|
- **Clicking opens a separate page** (`target=_blank`, `noopener`). The resolved URL must be
|
|
139
139
|
`http(s)`: anything else is refused rather than handed to the browser, because the string
|
|
140
140
|
reaches `window.open` and a `javascript:` URL would run in the SPA's own origin.
|
|
141
141
|
- **Declare `requiredMetadata` for the fields your resolver needs.** An unconfigured workspace
|
|
142
|
-
then gets "fill in `gameId` on the Metadata tab" instead of a generic failure
|
|
142
|
+
then gets "fill in `gameId` on the Metadata tab" instead of a generic failure, and the tool
|
|
143
143
|
stays LISTED, because the person looking at the sidebar is usually the one who can fix it. A
|
|
144
144
|
resolver that returns `null` reports separately ("this tool gave no address"), since that one
|
|
145
145
|
is yours to fix, not the operator's.
|
|
146
146
|
- **Treat every `ctx.metadata` value as untrusted input.** A workspace admin types these in, so a
|
|
147
|
-
value is operator-supplied text that happens to be length-bounded
|
|
147
|
+
value is operator-supplied text that happens to be length-bounded, not a constant you chose.
|
|
148
148
|
Set it as a query parameter or an `encodeURIComponent`'d path segment, as above. Never build the
|
|
149
149
|
ORIGIN from one: `` `https://${ctx.metadata.region}.acme.dev` `` with `region` set to
|
|
150
150
|
`evil.com/x?a=` resolves to a URL on someone else's host, and the `http(s)` allow-list cannot
|
|
151
151
|
tell that apart from the link you meant.
|
|
152
152
|
- **A resolver that THROWS costs only its own item.** It is caught and reported as a fourth
|
|
153
|
-
reason (`resolver-failed`) with the cause logged to the console
|
|
153
|
+
reason (`resolver-failed`) with the cause logged to the console: the sidebar, the palette and
|
|
154
154
|
the toolbar all render from one catalog, so an uncaught throw would otherwise blank all three.
|
|
155
155
|
Do not rely on it: `requiredMetadata` is how you say a field must be there.
|
|
156
156
|
- **`gate` and `advanced`** work exactly as on a `nav` entry; both must pass.
|
|
@@ -158,27 +158,27 @@ workspaceMetadataFields: [{ key: 'gameId', label: 'Game id', placeholder: 'zork'
|
|
|
158
158
|
**The metadata half** is a deployment-declared FIELD list (here) whose VALUES are per workspace,
|
|
159
159
|
typed in under _Workspace settings → Metadata_ and persisted on the workspace settings row. The
|
|
160
160
|
tab appears only where a deployment declares fields. Keys must be identifier-shaped
|
|
161
|
-
(`^[A-Za-z][A-Za-z0-9_.-]{0,63}
|
|
161
|
+
(`^[A-Za-z][A-Za-z0-9_.-]{0,63}$`: the backend refuses anything else); a malformed or duplicate
|
|
162
162
|
key is dropped with a dev-console warning rather than rendered. `type: 'select'` renders a picker
|
|
163
163
|
over your `options`; everything is stored as a string.
|
|
164
164
|
|
|
165
165
|
Two rules the editor keeps, and any other writer of the bag should too: a CLEARED field drops its
|
|
166
166
|
key (so "unset" never reads as "set to nothing" in a resolver), and a save carries through any
|
|
167
|
-
stored key the current build does not declare
|
|
167
|
+
stored key the current build does not declare; the update replaces the whole bag, so a value
|
|
168
168
|
written under a field you have since retired must not be deleted by an unrelated save.
|
|
169
169
|
|
|
170
170
|
Values are readable anywhere in the SPA via `useWorkspaceSettingsStore().settings.metadata`.
|
|
171
171
|
|
|
172
172
|
### Custom task types (`taskTypes`)
|
|
173
173
|
|
|
174
|
-
Model a proprietary work item
|
|
174
|
+
Model a proprietary work item (an "incident", "pentest", "compliance-audit") as a first-class
|
|
175
175
|
task type, the create-task twin of an agent kind. Contribute `{ taskType: '<ns>:<name>',
|
|
176
176
|
presentation: { label, icon, color, description }, fields?, defaultPipelineId?, formPanel? }` to
|
|
177
177
|
the `taskTypes` slot (see `acme:incident` in the example module). The SPA merges it into the
|
|
178
178
|
create-task picker and the card-badge catalog:
|
|
179
179
|
|
|
180
180
|
- **`presentation`** drives the create-task picker entry and the `TaskCard` type badge (resolved
|
|
181
|
-
through the pure `taskTypeMeta` read-model
|
|
181
|
+
through the pure `taskTypeMeta` read-model: the `agentKindMeta` twin). An UNREGISTERED
|
|
182
182
|
namespaced type (a stale row after your extension is removed) degrades to the `feature`
|
|
183
183
|
presentation, so a leftover string never breaks a card.
|
|
184
184
|
- **`fields`** are descriptor-driven create-form inputs (`text` / `textarea` / `number` /
|
|
@@ -198,20 +198,20 @@ task created with it round-trips with zero host edits.
|
|
|
198
198
|
> (namespaced id, well-formed `formPanel`, a `defaultPipelineId` that resolves to a real pipeline).
|
|
199
199
|
> A CODE-shipped `taskTypes` entry is trusted and **not** validated (like a code-shipped agent kind):
|
|
200
200
|
> a malformed `taskType`/`formPanel` id or a `defaultPipelineId` naming no real pipeline fails
|
|
201
|
-
> silently
|
|
201
|
+
> silently; the type just won't pre-select a pipeline and an unpaired `formPanel` degrades to the
|
|
202
202
|
> descriptor `fields`. Prefer backend registration when you want the fail-fast guardrail.
|
|
203
203
|
|
|
204
204
|
### Top-level overlays (`appOverlays`)
|
|
205
205
|
|
|
206
|
-
A nav item's `run` closure
|
|
206
|
+
A nav item's `run` closure, or any consumer code, often needs to open a full-screen panel of
|
|
207
207
|
its own: a dashboard, a wizard, a settings surface. The layer's first-party modals are
|
|
208
208
|
hand-mounted in `pages/index.vue`, which a consumer can't edit, so the `appOverlays` slot + the
|
|
209
209
|
single `<AppOverlayHost>` are the seam:
|
|
210
210
|
|
|
211
211
|
1. Contribute `{ id: '<ns>:<name>', component }` to the `appOverlays` slot (see
|
|
212
212
|
`acme:security-dashboard-overlay` in the example module).
|
|
213
|
-
2. Open it from anywhere with the auto-imported `useAppOverlays().open('<ns>:<name>', subject?)
|
|
214
|
-
|
|
213
|
+
2. Open it from anywhere with the auto-imported `useAppOverlays().open('<ns>:<name>', subject?)`:
|
|
214
|
+
typically a nav item's `run` closure. The optional `subject` is any value your overlay
|
|
215
215
|
renders against (e.g. a block id); it reaches the component as a `subject` prop.
|
|
216
216
|
3. `<AppOverlayHost>` resolves the slot with `resolveComponentRegistry` (the same pick-one
|
|
217
217
|
primitive `resultViews` uses) and mounts the matching component, wiring its `close` emit to
|
|
@@ -219,16 +219,16 @@ single `<AppOverlayHost>` are the seam:
|
|
|
219
219
|
|
|
220
220
|
It is a **pick-one** host: opening a second overlay replaces the first, and `close()` clears it.
|
|
221
221
|
Compose the shared `ResultWindowShell` (via `#components`) for chrome so your overlay inherits
|
|
222
|
-
focus-trap / scroll-lock / shared-stack Escape
|
|
223
|
-
(`open('<ns>:x')` with no registered component
|
|
222
|
+
focus-trap / scroll-lock / shared-stack Escape: emit `close` from its `@close`. A dangling open
|
|
223
|
+
(`open('<ns>:x')` with no registered component, e.g. a stale closure after the extension was
|
|
224
224
|
removed) degrades to nothing (a dev-console warning names the id), never a crash. Duplicate ids
|
|
225
225
|
across modules throw at boot, like every other slot.
|
|
226
226
|
|
|
227
227
|
> **Scope.** This seam is for CONSUMER overlays. The layer's own ~34 first-party modals stay
|
|
228
|
-
> hand-mounted in `index.vue` and are migrated only opportunistically
|
|
228
|
+
> hand-mounted in `index.vue` and are migrated only opportunistically: don't reach for
|
|
229
229
|
> `appOverlays` to replace a first-party fast-path modal.
|
|
230
230
|
|
|
231
|
-
## Reuse the shared building blocks
|
|
231
|
+
## Reuse the shared building blocks: don't reinvent them
|
|
232
232
|
|
|
233
233
|
The layer ships window/inspector primitives you compose instead of hand-rolling chrome or
|
|
234
234
|
re-deriving the "which run is this / how did the model do" facts. **Composables** (and the
|
|
@@ -236,25 +236,25 @@ re-deriving the "which run is this / how did the model do" facts. **Composables*
|
|
|
236
236
|
**components** must be named through the `#components` virtual module (see the boxed note
|
|
237
237
|
below). Compose these:
|
|
238
238
|
|
|
239
|
-
| Building block | Reference it as | What it gives you
|
|
240
|
-
| ----------------------------- | ----------------------------------------- |
|
|
241
|
-
| `ResultWindowShell` | `#components` → `PanelsResultWindowShell` | The shared modal chrome for a result window
|
|
242
|
-
| `StepRunMeta` | `#components` → `PanelsStepRunMeta` | **The shared run-details metadata block** every agent window reuses: step position, live duration, model, run id, and the LLM model-activity rollup. Drop it into your window's sidebar
|
|
243
|
-
| `MarkdownProse` | `#components` → `CommonMarkdownProse` | Render an agent's prose output as markdown.
|
|
244
|
-
| `CopyButton` | `#components` → `CommonCopyButton` | The shared copy-to-clipboard affordance.
|
|
245
|
-
| `InspectorSection` | `#components` → `PanelsInspectorSection` | The collapsible inspector-section shell (chevron header, count, hint) so a consumer panel reads like a built-in one.
|
|
246
|
-
| `useResultView(id)` | auto-imported | The window seam contract: `{ open, blockId, instanceId, stepIndex, close }` (+ an `onOpen` loader for windows that fetch, and an `onClose` flush). Escape is owned by the shell, not here.
|
|
247
|
-
| `useResultViewRunMeta(id, …)` | auto-imported | The `StepRunMeta` prop bundle (`{ step, instanceId, position, totalSteps, runFailed, failureAt }`), resolved for BOTH ways a window opens. A window reachable off-path
|
|
248
|
-
| `usePanelSubject<T>()` | `@modular-vue/core` | Read the block injected into an inspector panel by `<PanelsOutlet>`.
|
|
249
|
-
| `useAppOverlays()` | auto-imported | Open / close your own top-level overlays: `{ open(id, subject?), close(), active }`. The store-free seam a nav `run` closure uses to open an `appOverlays`-slot component (see "Top-level overlays").
|
|
239
|
+
| Building block | Reference it as | What it gives you |
|
|
240
|
+
| ----------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
241
|
+
| `ResultWindowShell` | `#components` → `PanelsResultWindowShell` | The shared modal chrome for a result window: backdrop, header (icon/title/subtitle), a `#header-extras` slot, close button, and the modal _behaviour_ (focus-trap + return, body-scroll lock, shared-stack Escape via `useModalBehavior`). Pass `stepRef` to surface the shared "restart from here" control. It also renders the universal per-step trailing sections (agent effort, pre-PR validation, binary outputs) off the ACTIVE step, so your window inherits them and must not re-render them itself. |
|
|
242
|
+
| `StepRunMeta` | `#components` → `PanelsStepRunMeta` | **The shared run-details metadata block** every agent window reuses: step position, live duration, model, run id, and the LLM model-activity rollup. Drop it into your window's sidebar, never reinvent run metadata. |
|
|
243
|
+
| `MarkdownProse` | `#components` → `CommonMarkdownProse` | Render an agent's prose output as markdown. |
|
|
244
|
+
| `CopyButton` | `#components` → `CommonCopyButton` | The shared copy-to-clipboard affordance. |
|
|
245
|
+
| `InspectorSection` | `#components` → `PanelsInspectorSection` | The collapsible inspector-section shell (chevron header, count, hint) so a consumer panel reads like a built-in one. |
|
|
246
|
+
| `useResultView(id)` | auto-imported | The window seam contract: `{ open, blockId, instanceId, stepIndex, close }` (+ an `onOpen` loader for windows that fetch, and an `onClose` flush). Escape is owned by the shell, not here. |
|
|
247
|
+
| `useResultViewRunMeta(id, …)` | auto-imported | The `StepRunMeta` prop bundle (`{ step, instanceId, position, totalSteps, runFailed, failureAt }`), resolved for BOTH ways a window opens. A window reachable off-path (from a board card or the inspector) carries no `stepIndex`, so wiring `StepRunMeta` straight off `useResultView` leaves it blank on exactly that route; this resolves the block's live run and the step your view id declares instead. |
|
|
248
|
+
| `usePanelSubject<T>()` | `@modular-vue/core` | Read the block injected into an inspector panel by `<PanelsOutlet>`. |
|
|
249
|
+
| `useAppOverlays()` | auto-imported | Open / close your own top-level overlays: `{ open(id, subject?), close(), active }`. The store-free seam a nav `run` closure uses to open an `appOverlays`-slot component (see "Top-level overlays"). |
|
|
250
250
|
|
|
251
251
|
> **Reference layer components through `#components`, not bare tags.** Nuxt auto-registers a
|
|
252
252
|
> layer's components under a **path-derived** name (`components/panels/ResultWindowShell.vue`
|
|
253
253
|
> → `PanelsResultWindowShell`), and only rewrites bare `<ResultWindowShell>` tags inside the
|
|
254
254
|
> layer's own SFCs. A bare tag in a **consumer** SFC resolves to nothing and silently renders
|
|
255
|
-
> as an unknown element
|
|
255
|
+
> as an unknown element: its `<slot>` children still appear, so a shallow test can pass while
|
|
256
256
|
> the shared chrome (and its `data-testid`) never mounts. Import the ones you use from
|
|
257
|
-
> `#components` (Nuxt's stable virtual registry
|
|
257
|
+
> `#components` (Nuxt's stable virtual registry: **not** a deep path into the layer's
|
|
258
258
|
> `app/components/*`), aliasing them back to the short names for readable templates:
|
|
259
259
|
>
|
|
260
260
|
> ```ts
|
|
@@ -274,13 +274,13 @@ below). Compose these:
|
|
|
274
274
|
|
|
275
275
|
Some state the engine records on a step is deliberately NOT a result view's job, because the
|
|
276
276
|
record's scope is wider than any one kind: the agent's effort self-assessment, the pre-PR
|
|
277
|
-
validation report, and
|
|
277
|
+
validation report, and (for a `binary-output` generator) the artifacts it declared it stored
|
|
278
278
|
(`step.binaryOutputs`). `ResultWindowShell` resolves the active step itself and renders each as a
|
|
279
279
|
collapsible trailing section, and the generic step-detail panel renders the same components for a
|
|
280
280
|
step whose kind declares no window at all.
|
|
281
281
|
|
|
282
282
|
So a generator kind should declare a result view for its OWN output (or none), and leave the
|
|
283
|
-
artifact list alone
|
|
283
|
+
artifact list alone: you get it either way, on every entry point, with no id to register. A
|
|
284
284
|
window that renders it again just shows it twice.
|
|
285
285
|
|
|
286
286
|
The example `AcmeSecurityReport.vue` window is a full demonstration: it imports
|
|
@@ -293,19 +293,19 @@ reads the auditor's structured assessment straight off `step.custom`.
|
|
|
293
293
|
Ship your strings under your own namespace in the deployment's `i18n/locales/*.json` (e.g.
|
|
294
294
|
`acme.*`). `@nuxtjs/i18n` is layer-aware and **deep-merges** them into the layer catalog, so
|
|
295
295
|
`t('acme.securityReport.title')` resolves in your components with no config change. The
|
|
296
|
-
layer's typed-key and locale-parity guards govern only the layer's own keys
|
|
296
|
+
layer's typed-key and locale-parity guards govern only the layer's own keys: your namespace
|
|
297
297
|
is yours.
|
|
298
298
|
|
|
299
299
|
## Rules that hold across every seam
|
|
300
300
|
|
|
301
301
|
- **Namespacing.** Every consumer-authored id is `<ns>:<name>`. Built-ins are never
|
|
302
|
-
shadowable
|
|
302
|
+
shadowable: the merge logic drops a consumer entry whose id collides with a built-in
|
|
303
303
|
(see the agents store).
|
|
304
304
|
- **Fail fast at boot, degrade at runtime.** Duplicate ids across first-party + consumer
|
|
305
305
|
modules throw when the layer resolves the merged slots at startup; missing pairings and
|
|
306
306
|
unknown wire ids degrade with a dev-console warning, never a crash.
|
|
307
307
|
- **Never crash on stale data.** An id that arrives on the wire (a `resultView`, an agent
|
|
308
|
-
kind) after its extension was removed must degrade to a defined rendering
|
|
308
|
+
kind) after its extension was removed must degrade to a defined rendering: extensions get
|
|
309
309
|
uninstalled while persisted rows outlive them.
|
|
310
310
|
- **The remote manifest is DATA only.** Components never travel the wire; per-workspace
|
|
311
311
|
variability comes from which capabilities the snapshot lists, not from which modules are
|
|
@@ -609,16 +609,32 @@ export const navigationModule = defineModule({
|
|
|
609
609
|
slots: { nav: [...NAV_CONTRIBUTIONS] },
|
|
610
610
|
})
|
|
611
611
|
|
|
612
|
+
/**
|
|
613
|
+
* Does a shell render this contribution under `gates`?
|
|
614
|
+
*
|
|
615
|
+
* The two axes a destination is gated on, in ONE place. They are independent and BOTH must
|
|
616
|
+
* pass: an `advanced` item is dropped in basic mode, and every item still answers to its own
|
|
617
|
+
* `gate`. Order doesn't matter (it's a conjunction) but the tier is checked first, since it's
|
|
618
|
+
* the cheaper read.
|
|
619
|
+
*
|
|
620
|
+
* Named rather than inlined in {@link navSlotFilter} because a second reader has to agree with
|
|
621
|
+
* it exactly: a tutorial tour whose step CLICKS a nav entry declares the requirement that
|
|
622
|
+
* renders it, and `tutorial-tours.spec.ts` pairs the two through this function. Spelling the
|
|
623
|
+
* conjunction out there instead would be a copy that keeps passing while this one changes —
|
|
624
|
+
* and the drift it would miss (an entry gaining a gate clause, or being marked `advanced` and
|
|
625
|
+
* so leaving the DEFAULT interface tier) is precisely a tour offered to a user who then finds
|
|
626
|
+
* no such control.
|
|
627
|
+
*/
|
|
628
|
+
export function navItemVisible(item: NavContribution, gates: NavGates): boolean {
|
|
629
|
+
return (item.advanced ? gates.advancedMode : true) && (item.gate ? item.gate(gates) : true)
|
|
630
|
+
}
|
|
631
|
+
|
|
612
632
|
/**
|
|
613
633
|
* Reactive RBAC/availability/interface-tier filter over the merged `nav` slot. Reads
|
|
614
634
|
* `deps.gates.*` (the reactive gate service) per item, so evaluated inside
|
|
615
635
|
* `useReactiveSlots` it re-runs when a permission, connection, or the interface
|
|
616
636
|
* mode flips. Passed to `installModularApp` as the global `slotFilter`.
|
|
617
637
|
*
|
|
618
|
-
* The two axes are independent and BOTH must pass: an `advanced` item is dropped in
|
|
619
|
-
* basic mode, and every item still answers to its own `gate`. Order doesn't matter
|
|
620
|
-
* (it's a conjunction) but the tier is checked first, since it's the cheaper read.
|
|
621
|
-
*
|
|
622
638
|
* Typed against `AppSlots` (not the generic `SlotFilter`) so it matches the
|
|
623
639
|
* filter shape the runtime infers for this registry. `deps` is widened to an
|
|
624
640
|
* optional `gates` to avoid importing `AppDeps` (which would be circular).
|
|
@@ -631,11 +647,7 @@ export function navSlotFilter(slots: AppSlots, deps: { gates?: NavGates }): AppS
|
|
|
631
647
|
...slots,
|
|
632
648
|
// No gates service wired (tests / bare install) ⇒ show everything, matching
|
|
633
649
|
// the dev-open "absent access allows all" backend parity.
|
|
634
|
-
nav: gates
|
|
635
|
-
? nav.filter(
|
|
636
|
-
(i) => (i.advanced ? gates.advancedMode : true) && (i.gate ? i.gate(gates) : true),
|
|
637
|
-
)
|
|
638
|
-
: nav,
|
|
650
|
+
nav: gates ? nav.filter((i) => navItemVisible(i, gates)) : nav,
|
|
639
651
|
// `tutorialTours` is deliberately NOT filtered here, unlike every other gated slot. A
|
|
640
652
|
// `SlotFilter` can only DROP, and the tutorial catalogue's whole job is to explain what
|
|
641
653
|
// was dropped — which tour this board can't run yet, and what would unlock it. That is a
|