@cat-factory/app 0.135.2 → 0.136.0

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,26 +1,10 @@
1
1
  <script setup lang="ts">
2
+ import { PanelsOutlet } from '@modular-vue/core'
2
3
  import type { Block, BlockStatus } from '~/types/domain'
3
4
  import { blockTypeMeta, STATUS_META } from '~/utils/catalog'
4
5
  import { pipelineAllowedForManualStart } from '~/utils/pipeline'
5
- import TaskContextDocs from '~/components/documents/TaskContextDocs.vue'
6
- import TaskContextIssues from '~/components/tasks/TaskContextIssues.vue'
7
- import TaskAgentConfig from '~/components/panels/inspector/TaskAgentConfig.vue'
8
- import ServiceTestConfig from '~/components/panels/inspector/ServiceTestConfig.vue'
9
- import ServiceFragments from '~/components/panels/inspector/ServiceFragments.vue'
10
- import ServiceReleaseHealthConfig from '~/components/panels/inspector/ServiceReleaseHealthConfig.vue'
11
- import ServiceTestSecrets from '~/components/panels/inspector/ServiceTestSecrets.vue'
12
- import FrontendConfig from '~/components/panels/inspector/FrontendConfig.vue'
13
- import ServiceConnections from '~/components/panels/inspector/ServiceConnections.vue'
14
- import ContainerSummary from '~/components/panels/inspector/ContainerSummary.vue'
15
- import TaskDependencies from '~/components/panels/inspector/TaskDependencies.vue'
16
- import TaskStructure from '~/components/panels/inspector/TaskStructure.vue'
17
- import TaskRunSettings from '~/components/panels/inspector/TaskRunSettings.vue'
18
- import TaskExecution from '~/components/panels/inspector/TaskExecution.vue'
19
- import TaskEstimateBadge from '~/components/panels/inspector/TaskEstimateBadge.vue'
20
- import EpicChildren from '~/components/panels/inspector/EpicChildren.vue'
21
- import InitiativeInspector from '~/components/panels/inspector/InitiativeInspector.vue'
6
+ import { inspectorPanels } from '~/modular/panels/inspector.logic'
22
7
  import IconButton from '~/components/common/IconButton.vue'
23
- import RecurringScheduleSettings from '~/components/panels/inspector/RecurringScheduleSettings.vue'
24
8
  import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
25
9
  import AgentStopButton from '~/components/board/AgentStopButton.vue'
26
10
 
@@ -76,8 +60,6 @@ watch(
76
60
  )
77
61
  const isContainer = computed(() => level.value === 'frame' || level.value === 'module')
78
62
  const isTask = computed(() => level.value === 'task')
79
- const isEpic = computed(() => level.value === 'epic')
80
- const isInitiative = computed(() => level.value === 'initiative')
81
63
 
82
64
  const instance = computed(() => execution.getInstance(block.value?.executionId))
83
65
  const typeMeta = computed(() => (block.value ? blockTypeMeta(block.value.type) : null))
@@ -481,14 +463,9 @@ const showOriginalDescription = ref(false)
481
463
  </UButton>
482
464
  </div>
483
465
 
484
- <!-- task: context documents -->
485
- <TaskContextDocs v-if="isTask" :key="`context-docs-${block.id}`" :block="block" />
486
-
487
- <!-- task: context issues (tracker) -->
488
- <TaskContextIssues v-if="isTask" :key="`context-issues-${block.id}`" :block="block" />
489
-
490
466
  <!-- service (frame): navigate the prescriptive spec tree (+ Gherkin scenarios when
491
- the spec is on the repo's default branch) -->
467
+ the spec is on the repo's default branch). A shell affordance, not a
468
+ level-keyed body panel, so it stays here above the panel outlet. -->
492
469
  <UButton
493
470
  v-if="isFrame"
494
471
  block
@@ -501,60 +478,28 @@ const showOriginalDescription = ref(false)
501
478
  {{ t('panels.inspector.viewRequirements') }}
502
479
  </UButton>
503
480
 
504
- <!-- service / module: tasks summary -->
505
- <ContainerSummary v-if="isContainer" :key="`container-${block.id}`" :block="block" />
506
- <!-- frontend (frame): build/serve/mock config + backend bindings (board links) -->
507
- <FrontendConfig
508
- v-if="isFrame && block.type === 'frontend'"
509
- :key="`frontend-${block.id}`"
510
- :block="block"
511
- />
512
-
513
- <!-- service (frame): directed connections to the other services it uses (board links) -->
514
- <ServiceConnections
515
- v-if="isFrame && block.type === 'service'"
516
- :key="`connections-${block.id}`"
517
- :block="block"
481
+ <!-- The level/type-keyed inspector body: the `inspectorPanels` panel group
482
+ (slice 4 of the modular-vue adoption). `<PanelsOutlet>` renders every
483
+ panel whose `when(block)` matches, ordered, with the selected block
484
+ injected as the subject (read via `usePanelSubject` in each panel's
485
+ wrapper). Replaces the pre-slice-4 `v-if` fan; `subject-key` is the block
486
+ id, so switching selections remounts panel content (matching the old
487
+ per-panel `:key`). A consumer contributes its own panels to the SAME
488
+ group via `registerAppModule`.
489
+
490
+ The `subject` cast is an upstream typing quirk, not a modelling escape
491
+ hatch: `<PanelsOutlet>` declares `subject` as `PropType<unknown>` with
492
+ `default: null`, which Volar narrows to `null`, so passing a typed
493
+ `Block | null` is rejected at compile time. `unknown` is the real
494
+ runtime contract; `as any` is the minimal unblock until the binding
495
+ types the prop explicitly (filed upstream — see the slice-4 residuals
496
+ in docs/initiatives/modular-vue-slice4-upstream-zones.md). -->
497
+ <PanelsOutlet
498
+ :group="inspectorPanels"
499
+ :subject="(block ?? null) as any"
500
+ :subject-key="block?.id ?? ''"
518
501
  />
519
502
 
520
- <!-- service (frame): test infra + provisioning configuration -->
521
- <ServiceTestConfig v-if="isFrame" :key="`test-config-${block.id}`" :block="block" />
522
-
523
- <!-- service (frame): SENSITIVE test credentials (sealed, injected out of band) -->
524
- <ServiceTestSecrets v-if="isFrame" :key="`test-secrets-${block.id}`" :block="block" />
525
-
526
- <!-- service (frame): best-practice fragments for code-aware agents -->
527
- <ServiceFragments v-if="isFrame" :key="`fragments-${block.id}`" :block="block" />
528
-
529
- <!-- service (frame): post-release-health monitor/SLO mapping -->
530
- <ServiceReleaseHealthConfig
531
- v-if="isFrame"
532
- :key="`release-health-${block.id}`"
533
- :block="block"
534
- />
535
-
536
- <!-- task: the live execution surface first (open by default), then the estimate,
537
- then the collapsed configuration sections (dependencies, run settings, agent
538
- config, structure) so a running task reads top-down without scrolling. -->
539
- <!-- Keyed by block id so a manual collapse/expand doesn't leak across task
540
- selections: switching tasks re-mounts each section back to its default state
541
- (e.g. the live Execution section is open again for the newly selected task). -->
542
- <template v-else-if="isTask">
543
- <RecurringScheduleSettings :key="`schedule-${block.id}`" :block="block" />
544
- <TaskExecution :key="`execution-${block.id}`" :block="block" />
545
- <TaskEstimateBadge :key="`estimate-${block.id}`" :block="block" />
546
- <TaskDependencies :key="`deps-${block.id}`" :block="block" />
547
- <TaskRunSettings :key="`run-settings-${block.id}`" :block="block" />
548
- <TaskAgentConfig :key="`agent-config-${block.id}`" :block="block" />
549
- <TaskStructure :key="`structure-${block.id}`" :block="block" />
550
- </template>
551
-
552
- <!-- epic: the full tree of member tasks, grouped by service → module -->
553
- <EpicChildren v-else-if="isEpic" :key="`epic-${block.id}`" :block="block" />
554
-
555
- <!-- initiative: status + goal, run-planning + tracker controls -->
556
- <InitiativeInspector v-else-if="isInitiative" :block="block" />
557
-
558
503
  <!-- Locked-run explanation: a disabled task Run button reads as a dead lock unless
559
504
  it says what's holding it. Named here (and on the button title) so the blocking
560
505
  dependencies are visible to pointer, keyboard, and touch alike — a native title
@@ -45,7 +45,12 @@ const ALL_GATES: NavGates = {
45
45
  isAccountAdmin: true,
46
46
  }
47
47
 
48
- const slots = (): AppSlots => ({ nav: [...NAV_CONTRIBUTIONS], resultViews: [], agentKinds: [] })
48
+ const slots = (): AppSlots => ({
49
+ nav: [...NAV_CONTRIBUTIONS],
50
+ resultViews: [],
51
+ agentKinds: [],
52
+ inspectorPanels: [],
53
+ })
49
54
  const ids = (s: unknown) => (s as AppSlots).nav.map((i) => i.id)
50
55
 
51
56
  describe('navSlotFilter', () => {
@@ -0,0 +1,99 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { resolvePanels } from '@modular-vue/core'
3
+ import type { PanelEntry } from '@modular-vue/core'
4
+ import type { Block, BlockLevel, BlockType } from '~/types/domain'
5
+ import { INSPECTOR_PANEL_IDS, INSPECTOR_PANEL_SPECS } from './inspector.logic'
6
+
7
+ /**
8
+ * Pins the inspector panel group's gating + ordering (slice 4 of the modular-vue
9
+ * adoption) against the pre-slice-4 `InspectorPanel.vue` `v-if` fan, using the
10
+ * SAME pure `resolvePanels` the host resolves through. A stub component per spec
11
+ * stands in for the real SFC (the gating/order is component-agnostic).
12
+ */
13
+
14
+ const ENTRIES: PanelEntry<Block>[] = INSPECTOR_PANEL_SPECS.map((spec) => ({
15
+ id: spec.id,
16
+ order: spec.order,
17
+ when: spec.when,
18
+ // A trivial functional component stands in for the real SFC — the gating/order
19
+ // under test is component-agnostic; `() => null` satisfies the engine's
20
+ // `UiComponent` (a callable) without pulling a Vue runtime into the unit test.
21
+ component: () => null,
22
+ }))
23
+
24
+ const block = (level: BlockLevel, type: BlockType = 'service'): Block =>
25
+ ({ id: `b-${level}-${type}`, level, type }) as unknown as Block
26
+
27
+ const visibleIds = (b: Block | null) => resolvePanels(ENTRIES, b).map((e) => e.id)
28
+
29
+ describe('inspector panel group', () => {
30
+ it('every id has exactly one spec (no dup, no gap)', () => {
31
+ const specIds = INSPECTOR_PANEL_SPECS.map((s) => s.id).sort()
32
+ expect(specIds).toEqual([...INSPECTOR_PANEL_IDS].sort())
33
+ expect(new Set(specIds).size).toBe(specIds.length)
34
+ })
35
+
36
+ it('a service frame shows the container + service panels, ordered, no frontend-config', () => {
37
+ expect(visibleIds(block('frame', 'service'))).toEqual([
38
+ 'container-summary',
39
+ 'service-connections',
40
+ 'service-test-config',
41
+ 'service-test-secrets',
42
+ 'service-fragments',
43
+ 'service-release-health',
44
+ ])
45
+ })
46
+
47
+ it('a frontend frame swaps connections for frontend-config', () => {
48
+ expect(visibleIds(block('frame', 'frontend'))).toEqual([
49
+ 'container-summary',
50
+ 'frontend-config',
51
+ 'service-test-config',
52
+ 'service-test-secrets',
53
+ 'service-fragments',
54
+ 'service-release-health',
55
+ ])
56
+ })
57
+
58
+ it('a module shows only the container summary', () => {
59
+ expect(visibleIds(block('module'))).toEqual(['container-summary'])
60
+ })
61
+
62
+ it('a task shows the task body in the pre-slice-4 order', () => {
63
+ expect(visibleIds(block('task'))).toEqual([
64
+ 'task-context-docs',
65
+ 'task-context-issues',
66
+ 'recurring-schedule',
67
+ 'task-execution',
68
+ 'task-estimate',
69
+ 'task-dependencies',
70
+ 'task-run-settings',
71
+ 'task-agent-config',
72
+ 'task-structure',
73
+ ])
74
+ })
75
+
76
+ it('epic and initiative each show their single inspector', () => {
77
+ expect(visibleIds(block('epic'))).toEqual(['epic-children'])
78
+ expect(visibleIds(block('initiative'))).toEqual(['initiative-inspector'])
79
+ })
80
+
81
+ it('no subject selected resolves to no panels', () => {
82
+ expect(visibleIds(null)).toEqual([])
83
+ })
84
+
85
+ // Locks the contract the client plugin's boot fail-fast depends on: it calls
86
+ // `resolvePanels(mergedEntries, null)` once at startup so a duplicate panel id
87
+ // (e.g. a consumer module colliding with a first-party id) throws at BOOT rather
88
+ // than the first time a block is selected. That only works because `resolvePanels`
89
+ // runs its duplicate-id check BEFORE the null-subject short-circuit — pin it here
90
+ // so an upstream reorder (dedup after the null guard) can't silently disarm the
91
+ // boot check.
92
+ it('throws on a duplicate panel id even with a null subject (boot fail-fast)', () => {
93
+ const dup: PanelEntry<Block>[] = [
94
+ { id: 'container-summary', order: 1, when: () => true, component: () => null },
95
+ { id: 'container-summary', order: 2, when: () => true, component: () => null },
96
+ ]
97
+ expect(() => resolvePanels(dup, null)).toThrow(/duplicate panel id "container-summary"/)
98
+ })
99
+ })
@@ -0,0 +1,113 @@
1
+ import { definePanelGroup } from '@modular-vue/core'
2
+ import type { Block } from '~/types/domain'
3
+
4
+ /**
5
+ * Pure definition of the block-inspector panel group (slice 4 of the modular-vue
6
+ * adoption — docs/initiatives/modular-vue-adoption.md).
7
+ *
8
+ * The inspector used to be a 631-line `v-if` fan in `InspectorPanel.vue` that
9
+ * switched its body on the selected block's `level` (frame/module/task/epic/
10
+ * initiative) and, inside the frame branch, on `type` (frontend/service). Slice 4
11
+ * turns that fan into the upstream **subject-keyed panels** primitive: each body
12
+ * sub-panel is a `PanelEntry<Block>` contributed to the `inspectorPanels` slot,
13
+ * and `<PanelsOutlet>` renders every entry whose `when(block)` passes, ordered by
14
+ * `order`, with the selected block injected as the subject. A consumer deployment
15
+ * contributes its OWN inspector panels (e.g. for a custom block type) to the SAME
16
+ * slot via `registerAppModule`, with zero host edits.
17
+ *
18
+ * This file is deliberately free of `.vue`/`defineModule` imports so the gating +
19
+ * ordering is unit-tested directly (`inspector.logic.spec.ts`) via the pure
20
+ * `resolvePanels`; the component wiring (which imports the sub-panel SFCs) lives
21
+ * in `inspector.ts`, registered from the client plugin — the same split as the
22
+ * result-views and journey modules. (`definePanelGroup` is a pure `{ slotKey }`
23
+ * token, so the group handle belongs here where every call site shares it.)
24
+ */
25
+
26
+ /** The slot key the inspector panel entries are contributed under (and that the
27
+ * group handle names). Also the `AppSlots` key — keep the two in step. */
28
+ export const INSPECTOR_PANELS_SLOT = 'inspectorPanels' as const
29
+
30
+ /** The typed panel-group handle — `usePanels(inspectorPanels, block)` /
31
+ * `<PanelsOutlet :group="inspectorPanels" :subject="block">` resolve against it. */
32
+ export const inspectorPanels = definePanelGroup<Block>(INSPECTOR_PANELS_SLOT)
33
+
34
+ /** Every built-in inspector panel id. A const union so the component map in
35
+ * `inspector.ts` is exhaustive (a spec with no component is a compile error, not
36
+ * a silently-dropped panel). */
37
+ export const INSPECTOR_PANEL_IDS = [
38
+ // task body (rendered in this order under the identity block)
39
+ 'task-context-docs',
40
+ 'task-context-issues',
41
+ 'recurring-schedule',
42
+ 'task-execution',
43
+ 'task-estimate',
44
+ 'task-dependencies',
45
+ 'task-run-settings',
46
+ 'task-agent-config',
47
+ 'task-structure',
48
+ // service / module body
49
+ 'container-summary',
50
+ 'frontend-config',
51
+ 'service-connections',
52
+ 'service-test-config',
53
+ 'service-test-secrets',
54
+ 'service-fragments',
55
+ 'service-release-health',
56
+ // epic / initiative body
57
+ 'epic-children',
58
+ 'initiative-inspector',
59
+ ] as const
60
+
61
+ export type InspectorPanelId = (typeof INSPECTOR_PANEL_IDS)[number]
62
+
63
+ /** The gating + ordering for one built-in inspector panel — the `PanelEntry`
64
+ * minus its component (attached in `inspector.ts`). `when` is the predicate
65
+ * lifted verbatim from the panel's old `v-if`; `order` preserves the pre-slice-4
66
+ * render order within a level (levels never overlap, so cross-level order is
67
+ * irrelevant and the values are just grouped by level). */
68
+ export interface InspectorPanelSpec {
69
+ id: InspectorPanelId
70
+ order: number
71
+ when: (block: Block) => boolean
72
+ }
73
+
74
+ const isTask = (b: Block) => b.level === 'task'
75
+ const isFrame = (b: Block) => b.level === 'frame'
76
+ /** frame OR module — the "container" panels. */
77
+ const isContainer = (b: Block) => b.level === 'frame' || b.level === 'module'
78
+
79
+ /**
80
+ * The built-in panel specs. The `when` predicates and orders reproduce
81
+ * `InspectorPanel.vue`'s pre-slice-4 behaviour exactly (pinned by
82
+ * `inspector.logic.spec.ts`):
83
+ * - task body: context docs/issues, then schedule → execution → estimate →
84
+ * dependencies → run-settings → agent-config → structure.
85
+ * - service/module body: container summary (frame|module), then the frame-only
86
+ * panels, with frontend-config / service-connections further gated on `type`.
87
+ * - epic / initiative bodies: their single inspector.
88
+ *
89
+ * The frame-only "view requirements" button and the cross-cutting run banners
90
+ * (`AgentFailureCard` / `AgentStopButton`) stay in the host shell — they are not
91
+ * level-keyed body panels, so converting them would add ceremony without
92
+ * extensibility value.
93
+ */
94
+ export const INSPECTOR_PANEL_SPECS: readonly InspectorPanelSpec[] = [
95
+ { id: 'task-context-docs', order: 10, when: isTask },
96
+ { id: 'task-context-issues', order: 20, when: isTask },
97
+ { id: 'recurring-schedule', order: 30, when: isTask },
98
+ { id: 'task-execution', order: 40, when: isTask },
99
+ { id: 'task-estimate', order: 50, when: isTask },
100
+ { id: 'task-dependencies', order: 60, when: isTask },
101
+ { id: 'task-run-settings', order: 70, when: isTask },
102
+ { id: 'task-agent-config', order: 80, when: isTask },
103
+ { id: 'task-structure', order: 90, when: isTask },
104
+ { id: 'container-summary', order: 110, when: isContainer },
105
+ { id: 'frontend-config', order: 120, when: (b) => isFrame(b) && b.type === 'frontend' },
106
+ { id: 'service-connections', order: 130, when: (b) => isFrame(b) && b.type === 'service' },
107
+ { id: 'service-test-config', order: 140, when: isFrame },
108
+ { id: 'service-test-secrets', order: 150, when: isFrame },
109
+ { id: 'service-fragments', order: 160, when: isFrame },
110
+ { id: 'service-release-health', order: 170, when: isFrame },
111
+ { id: 'epic-children', order: 200, when: (b) => b.level === 'epic' },
112
+ { id: 'initiative-inspector', order: 210, when: (b) => b.level === 'initiative' },
113
+ ]
@@ -0,0 +1,101 @@
1
+ import { type Component, defineComponent, h } from 'vue'
2
+ import { defineModule, usePanelSubject } from '@modular-vue/core'
3
+ import type { PanelEntry } from '@modular-vue/core'
4
+ import type { Block } from '~/types/domain'
5
+
6
+ /** The engine's opaque component type on a `PanelEntry` (the neutral `UiComponent`,
7
+ * which isn't exported by name). A Vue component is a valid one; the outlet renders
8
+ * it as a Vue component. We reference it structurally so no cast reaches for `any`. */
9
+ type PanelComponent = PanelEntry<Block>['component']
10
+ import {
11
+ INSPECTOR_PANELS_SLOT,
12
+ INSPECTOR_PANEL_SPECS,
13
+ type InspectorPanelId,
14
+ } from '~/modular/panels/inspector.logic'
15
+ import TaskContextDocs from '~/components/documents/TaskContextDocs.vue'
16
+ import TaskContextIssues from '~/components/tasks/TaskContextIssues.vue'
17
+ import RecurringScheduleSettings from '~/components/panels/inspector/RecurringScheduleSettings.vue'
18
+ import TaskExecution from '~/components/panels/inspector/TaskExecution.vue'
19
+ import TaskEstimateBadge from '~/components/panels/inspector/TaskEstimateBadge.vue'
20
+ import TaskDependencies from '~/components/panels/inspector/TaskDependencies.vue'
21
+ import TaskRunSettings from '~/components/panels/inspector/TaskRunSettings.vue'
22
+ import TaskAgentConfig from '~/components/panels/inspector/TaskAgentConfig.vue'
23
+ import TaskStructure from '~/components/panels/inspector/TaskStructure.vue'
24
+ import ContainerSummary from '~/components/panels/inspector/ContainerSummary.vue'
25
+ import FrontendConfig from '~/components/panels/inspector/FrontendConfig.vue'
26
+ import ServiceConnections from '~/components/panels/inspector/ServiceConnections.vue'
27
+ import ServiceTestConfig from '~/components/panels/inspector/ServiceTestConfig.vue'
28
+ import ServiceTestSecrets from '~/components/panels/inspector/ServiceTestSecrets.vue'
29
+ import ServiceFragments from '~/components/panels/inspector/ServiceFragments.vue'
30
+ import ServiceReleaseHealthConfig from '~/components/panels/inspector/ServiceReleaseHealthConfig.vue'
31
+ import EpicChildren from '~/components/panels/inspector/EpicChildren.vue'
32
+ import InitiativeInspector from '~/components/panels/inspector/InitiativeInspector.vue'
33
+
34
+ /**
35
+ * Component wiring for the block-inspector panel group (slice 4). The gating +
36
+ * ordering live in `inspector.logic.ts` (pure, unit-tested); this file attaches
37
+ * the sub-panel SFCs and registers the module. It imports `.vue`, so — like
38
+ * `result-views.ts` and the journey step module — it is registered from the
39
+ * client plugin via `extraModules`, keeping the unit-tested `registry.ts` import
40
+ * graph SFC-free.
41
+ */
42
+
43
+ /**
44
+ * Bridge a `block`-prop sub-panel onto the subject-keyed panels primitive.
45
+ * `<PanelsOutlet>` injects the selected block as the panel *subject* (a `subject`
46
+ * prop + a `provide`), but the existing inspector sub-panels take a `block` prop
47
+ * (and several — `TaskExecution`, `ServiceTestConfig` — are reused OUTSIDE the
48
+ * inspector, so their prop can't be renamed). So each entry's component is this
49
+ * thin wrapper: it reads the injected subject via `usePanelSubject` and forwards
50
+ * it as `:block`. The host sets `<PanelsOutlet :subject-key="b => b.id">`, so the
51
+ * wrapper remounts on block switch — matching the pre-slice-4 per-panel
52
+ * `:key="…-${block.id}"`.
53
+ */
54
+ function blockPanel(component: Component, id: InspectorPanelId): PanelComponent {
55
+ return defineComponent({
56
+ name: `InspectorPanel__${id}`,
57
+ setup() {
58
+ const block = usePanelSubject<Block>()
59
+ return () => h(component, { block: block.value })
60
+ },
61
+ }) as unknown as PanelComponent
62
+ }
63
+
64
+ /** Exhaustive id → sub-panel map. Typed `Record<InspectorPanelId, …>` so adding a
65
+ * spec without a component (or vice-versa) fails the typecheck. */
66
+ const COMPONENTS: Record<InspectorPanelId, Component> = {
67
+ 'task-context-docs': TaskContextDocs,
68
+ 'task-context-issues': TaskContextIssues,
69
+ 'recurring-schedule': RecurringScheduleSettings,
70
+ 'task-execution': TaskExecution,
71
+ 'task-estimate': TaskEstimateBadge,
72
+ 'task-dependencies': TaskDependencies,
73
+ 'task-run-settings': TaskRunSettings,
74
+ 'task-agent-config': TaskAgentConfig,
75
+ 'task-structure': TaskStructure,
76
+ 'container-summary': ContainerSummary,
77
+ 'frontend-config': FrontendConfig,
78
+ 'service-connections': ServiceConnections,
79
+ 'service-test-config': ServiceTestConfig,
80
+ 'service-test-secrets': ServiceTestSecrets,
81
+ 'service-fragments': ServiceFragments,
82
+ 'service-release-health': ServiceReleaseHealthConfig,
83
+ 'epic-children': EpicChildren,
84
+ 'initiative-inspector': InitiativeInspector,
85
+ }
86
+
87
+ /** The built-in `PanelEntry`s: each spec's gating/order + its wrapped component. */
88
+ export const INSPECTOR_PANEL_ENTRIES: PanelEntry<Block>[] = INSPECTOR_PANEL_SPECS.map((spec) => ({
89
+ id: spec.id,
90
+ order: spec.order,
91
+ when: spec.when,
92
+ component: blockPanel(COMPONENTS[spec.id], spec.id),
93
+ }))
94
+
95
+ /** The first-party module: contributes the built-in panels to the `inspectorPanels`
96
+ * slot. Registered by the client plugin (`extraModules`). */
97
+ export const inspectorPanelsModule = defineModule({
98
+ id: 'cat-factory:inspector-panels',
99
+ version: '1.0.0',
100
+ slots: { [INSPECTOR_PANELS_SLOT]: INSPECTOR_PANEL_ENTRIES },
101
+ })
@@ -101,7 +101,7 @@ export function createAppRegistry(
101
101
  // are registered from the client plugin via `extraModules` + `registerJourney`.
102
102
  const registry = createRegistry<AppDeps, AppSlots>({
103
103
  services: { gates: deps.gates },
104
- slots: { nav: [], resultViews: [], agentKinds: [] },
104
+ slots: { nav: [], resultViews: [], agentKinds: [], inspectorPanels: [] },
105
105
  }).use(journeysPlugin())
106
106
  for (const mod of [...FIRST_PARTY_MODULES, ...extraModules, ...consumerModules]) {
107
107
  registry.register(mod)
@@ -1,6 +1,6 @@
1
1
  import type { Component } from 'vue'
2
- import type { ComponentEntry } from '@modular-vue/core'
3
- import type { CustomAgentKind } from '~/types/domain'
2
+ import type { ComponentEntry, PanelEntry } from '@modular-vue/core'
3
+ import type { Block, CustomAgentKind } from '~/types/domain'
4
4
  import type { NavContribution } from './nav-contributions'
5
5
 
6
6
  /**
@@ -17,6 +17,12 @@ import type { NavContribution } from './nav-contributions'
17
17
  * contributes (the palette/catalog data half). BACKEND-registered kinds
18
18
  * arrive separately as a {@link RemoteModuleManifest} read in the agents
19
19
  * store — see `stores/agents.ts`.
20
+ * - `inspectorPanels` (slice 4) — the subject-keyed detail panels of the block
21
+ * inspector ({@link PanelEntry} over a `Block`), rendered all-matching + ordered
22
+ * by `<PanelsOutlet>` and gated per-block by each entry's `when(block)`
23
+ * predicate. Replaces `InspectorPanel.vue`'s level/type `v-if` fan; a consumer
24
+ * contributes its own panels to the SAME slot via `registerAppModule`. This is
25
+ * the slot key `definePanelGroup<Block>('inspectorPanels')` names.
20
26
  *
21
27
  * The index signature is mutable (`unknown[]`) to satisfy the runtime's
22
28
  * `SlotMap` constraint while `unknown[]` still meets `useReactiveSlots`'
@@ -26,6 +32,7 @@ export interface AppSlots {
26
32
  nav: NavContribution[]
27
33
  resultViews: ResultViewContribution[]
28
34
  agentKinds: CustomAgentKind[]
35
+ inspectorPanels: PanelEntry<Block>[]
29
36
  [key: string]: unknown[]
30
37
  }
31
38
 
@@ -1,20 +1,22 @@
1
1
  import { installModularApp } from '@modular-vue/nuxt/runtime'
2
2
  import type { ApplicationManifest } from '@modular-vue/runtime'
3
3
  import type { NavigationItem } from '@modular-frontend/core'
4
- import { resolveComponentRegistry } from '@modular-vue/core'
4
+ import { resolveComponentRegistry, resolvePanels } from '@modular-vue/core'
5
+ import type { PanelEntry } from '@modular-vue/core'
5
6
  import { provideJourneyRuntime } from '@modular-vue/journeys'
6
7
  import type { JourneyRuntime } from '@modular-vue/journeys'
7
8
  import { createAppRegistry } from '~/modular/registry'
8
9
  import { navSlotFilter } from '~/modular/nav-contributions'
9
10
  import { createNavGates } from '~/modular/nav-gates'
10
11
  import { resultViewsModule } from '~/modular/result-views'
12
+ import { inspectorPanelsModule } from '~/modular/panels/inspector'
11
13
  import {
12
14
  environmentSetupJourney,
13
15
  environmentSetupModule,
14
16
  environmentSetupPersistence,
15
17
  } from '~/modular/journeys/environmentSetup'
16
18
  import type { AppSlots, ResultViewContribution } from '~/modular/slots'
17
- import type { CustomAgentKind } from '~/types/domain'
19
+ import type { Block, CustomAgentKind } from '~/types/domain'
18
20
 
19
21
  /**
20
22
  * Wire the modular-vue registry into the Nuxt app (slice 0 of the modular-vue
@@ -58,6 +60,7 @@ export default defineNuxtPlugin({
58
60
  // resolves (inside `installModularApp`).
59
61
  const registry = createAppRegistry({ gates: createNavGates() }, [
60
62
  resultViewsModule,
63
+ inspectorPanelsModule,
61
64
  environmentSetupModule,
62
65
  ])
63
66
  registry.registerJourney(environmentSetupJourney, {
@@ -88,6 +91,13 @@ export default defineNuxtPlugin({
88
91
  // clear stack. The slot is static after this resolve, so `StepResultViewHost`'s own
89
92
  // reactive re-resolve is a cheap memoized read that this has already validated.
90
93
  resolveComponentRegistry((slots.resultViews ?? []) as ResultViewContribution[])
94
+ // Same fail-fast for the inspector panel group (slice 4): resolve the merged
95
+ // `inspectorPanels` slot once with a null subject so a duplicate panel id across
96
+ // the first-party + consumer modules throws at BOOT (`resolvePanels` dedupes/
97
+ // throws before the null-subject guard) rather than the first time a block is
98
+ // selected. `<PanelsOutlet>` re-resolves reactively per subject; this only
99
+ // validates the wiring.
100
+ resolvePanels((slots.inspectorPanels ?? []) as PanelEntry<Block>[], null)
91
101
  // Consumer agent kinds contributed as CODE to the static `agentKinds` slot
92
102
  // (module slots resolve once, so the static base is the full set).
93
103
  useAgentsStore().registerConsumerKinds((slots.agentKinds ?? []) as CustomAgentKind[])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.135.2",
3
+ "version": "0.136.0",
4
4
  "description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,12 +18,12 @@
18
18
  "access": "public"
19
19
  },
20
20
  "dependencies": {
21
- "@modular-frontend/core": "^0.3.0",
22
- "@modular-vue/core": "^1.2.0",
23
- "@modular-vue/journeys": "^1.2.0",
24
- "@modular-vue/nuxt": "^0.3.0",
25
- "@modular-vue/runtime": "^1.3.0",
26
- "@modular-vue/vue": "^1.2.0",
21
+ "@modular-frontend/core": "^0.4.0",
22
+ "@modular-vue/core": "^1.3.0",
23
+ "@modular-vue/journeys": "^1.3.0",
24
+ "@modular-vue/nuxt": "^0.4.0",
25
+ "@modular-vue/runtime": "^1.4.1",
26
+ "@modular-vue/vue": "^1.3.0",
27
27
  "@nuxt/ui": "^4.10.0",
28
28
  "@nuxtjs/i18n": "^10.4.1",
29
29
  "@pinia/nuxt": "^1.0.1",