@cat-factory/app 0.147.6 → 0.148.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.
@@ -0,0 +1,88 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref, watch } from 'vue'
3
+ import type { RemoteModuleManifest } from '@modular-vue/core'
4
+ import { customTaskTypeToMeta } from '~/modular/task-types'
5
+ import type { AppSlots } from '~/modular/slots'
6
+ import { TASK_TYPE_META, setCustomTaskTypeMeta } from '~/utils/catalog'
7
+ import type { CustomTaskType, TaskTypeMeta } from '~/types/domain'
8
+
9
+ /**
10
+ * The custom task-type catalog (frontend-extension-mechanism initiative, slice B) — the exact twin
11
+ * of the agents store (`stores/agents.ts`), which is the template for every capability catalog.
12
+ *
13
+ * Reactive union of two sources, neither mutating the built-in {@link TASK_TYPE_META} const:
14
+ * - CONSUMER-shipped task types contributed as CODE via the modular `taskTypes` slot
15
+ * (`registerConsumerTaskTypes`, fed once at boot from the resolved manifest);
16
+ * - the deployment's BACKEND-registered task types, folded into the single per-workspace
17
+ * {@link RemoteModuleManifest} the workspace store swaps per snapshot (`hydrateCapabilities`,
18
+ * reading its OWN `taskTypes` slot off the shared manifest).
19
+ *
20
+ * The merged custom catalog is projected back into `catalog.ts`'s {@link setCustomTaskTypeMeta}
21
+ * read-model so the pure `taskTypeMeta` lookup (used by the card badge + create-task picker)
22
+ * resolves a custom type reactively without importing this store.
23
+ */
24
+ export const useTaskTypesStore = defineStore('taskTypes', () => {
25
+ // CODE-shipped consumer task types from the static `taskTypes` slot (fed once at boot by the
26
+ // modular install plugin — module slots are resolved once).
27
+ const consumerTaskTypes = ref<CustomTaskType[]>([])
28
+ // The active per-workspace capability manifest (shared with the agents store), or null before
29
+ // the first hydrate. This store reads only its own `taskTypes` slot off it.
30
+ const capabilitiesManifest = ref<RemoteModuleManifest<AppSlots> | null>(null)
31
+
32
+ /**
33
+ * The merged CUSTOM task types (consumer-slot → backend-manifest), de-duplicated and never
34
+ * shadowing a BUILT-IN type. A namespaced custom id can't collide with a bare built-in, but the
35
+ * built-in drop is kept for parity with the agents store (and to be robust to a malformed id).
36
+ */
37
+ const customTaskTypes = computed<CustomTaskType[]>(() => {
38
+ const seen = new Set<string>()
39
+ const out: CustomTaskType[] = []
40
+ const add = (t: CustomTaskType) => {
41
+ if (t.taskType in TASK_TYPE_META || seen.has(t.taskType)) return
42
+ seen.add(t.taskType)
43
+ out.push(t)
44
+ }
45
+ for (const t of consumerTaskTypes.value) add(t)
46
+ for (const t of capabilitiesManifest.value?.slots?.taskTypes ?? []) add(t)
47
+ return out
48
+ })
49
+
50
+ /** The custom types indexed by id, for a per-type lookup (e.g. the create-form field descriptors). */
51
+ const byTaskType = computed<Record<string, CustomTaskType>>(() =>
52
+ Object.fromEntries(customTaskTypes.value.map((t) => [t.taskType, t])),
53
+ )
54
+
55
+ // Keep `catalog.ts`'s pure-util projection in sync with the merged custom catalog so
56
+ // `taskTypeMeta` resolves custom types. Sync flush so an imperative read right after
57
+ // `hydrateCapabilities` sees the fresh catalog with no tick gap (mirrors the agents store).
58
+ const customMetaMap = computed<Record<string, TaskTypeMeta>>(() =>
59
+ Object.fromEntries(customTaskTypes.value.map((t) => [t.taskType, customTaskTypeToMeta(t)])),
60
+ )
61
+ watch(customMetaMap, (map) => setCustomTaskTypeMeta(map), { immediate: true, flush: 'sync' })
62
+
63
+ /** The registered custom task type for `taskType`, or undefined. */
64
+ function get(taskType: string): CustomTaskType | undefined {
65
+ return byTaskType.value[taskType]
66
+ }
67
+
68
+ /**
69
+ * Register the deployment's CODE-shipped consumer task types — the resolved modular `taskTypes`
70
+ * slot, fed once by the install plugin. Idempotent replace (module slots resolve once).
71
+ */
72
+ function registerConsumerTaskTypes(types: readonly CustomTaskType[]) {
73
+ consumerTaskTypes.value = [...types]
74
+ }
75
+
76
+ /**
77
+ * Hydrate the deployment's BACKEND-registered capabilities from the shared per-workspace manifest
78
+ * (built by the workspace store from the snapshot, carrying both `agentKinds` + `taskTypes`).
79
+ * Skips the swap — and the downstream projection invalidation — when the content-derived manifest
80
+ * version is unchanged, exactly like the agents store.
81
+ */
82
+ function hydrateCapabilities(manifest: RemoteModuleManifest<AppSlots>) {
83
+ if (capabilitiesManifest.value?.version === manifest.version) return
84
+ capabilitiesManifest.value = manifest
85
+ }
86
+
87
+ return { customTaskTypes, get, registerConsumerTaskTypes, hydrateCapabilities }
88
+ })
@@ -25,6 +25,8 @@ import { useRecurringPipelinesStore } from '~/stores/recurringPipelines'
25
25
  import { useInitiativesStore } from '~/stores/initiative'
26
26
  import { useServicesStore } from '~/stores/services'
27
27
  import { useAgentsStore } from '~/stores/agents'
28
+ import { useTaskTypesStore } from '~/stores/taskTypes'
29
+ import { buildWorkspaceCapabilitiesManifest } from '~/modular/capabilities'
28
30
  import { useSkillsStore } from '~/stores/skills'
29
31
  import { useTrackerStore } from '~/stores/tracker'
30
32
  import { useRequirementsStore } from '~/stores/requirements'
@@ -169,10 +171,18 @@ export const useWorkspaceStore = defineStore(
169
171
  useInitiativesStore().hydratePresets(snapshot.initiativePresets)
170
172
  useTrackerStore().hydrate(snapshot.trackerSettings)
171
173
  useServicesStore().hydrate(snapshot.mounts ?? [], snapshot.serviceCatalog ?? [])
172
- // Hydrate the deployment's backend-registered custom agent kinds as the workspace's
173
- // remote capability manifest, so a proprietary kind renders as a first-class palette
174
- // block + result view. Swapped wholesale per workspace (no global-catalog mutation).
175
- useAgentsStore().hydrateCustomKinds(snapshot.customAgentKinds ?? [])
174
+ // Hydrate the deployment's backend-registered capabilities from ONE shared per-workspace
175
+ // remote capability manifest carrying BOTH custom agent kinds AND custom task types (its
176
+ // version covers both, so an unchanged snapshot no-ops both stores). Each store reads its
177
+ // own slot off it, so a proprietary agent kind renders as a first-class palette block +
178
+ // result view and a proprietary task type as a first-class create-task choice + card badge.
179
+ // Swapped wholesale per workspace (no global-catalog mutation).
180
+ const capabilities = buildWorkspaceCapabilitiesManifest(
181
+ snapshot.customAgentKinds ?? [],
182
+ snapshot.customTaskTypes ?? [],
183
+ )
184
+ useAgentsStore().hydrateCapabilities(capabilities)
185
+ useTaskTypesStore().hydrateCapabilities(capabilities)
176
186
  // The account's repo-sourced Claude Skills catalog (shared across its workspaces), so the
177
187
  // pipeline builder's per-step skill picker has its options. A straight replace.
178
188
  useSkillsStore().hydrate(snapshot.skills ?? [])
@@ -59,6 +59,10 @@ export type {
59
59
  AgentKind,
60
60
  AgentCategory,
61
61
  CustomAgentKind,
62
+ CustomTaskType,
63
+ TaskTypePresentation,
64
+ TaskTypeFieldDescriptor,
65
+ TaskTypeFieldOption,
62
66
  Pipeline,
63
67
  PipelinePurpose,
64
68
  SpendStatus,
@@ -117,6 +121,25 @@ export interface AgentArchetype {
117
121
  resultView?: string
118
122
  }
119
123
 
124
+ /**
125
+ * Display metadata for a task TYPE (the card badge + create-task picker), resolved through the
126
+ * `taskTypeMeta` read-model. A BUILT-IN type carries an i18n {@link labelKey}; a CUSTOM
127
+ * (deployment-registered) type carries a literal {@link label} from the wire presentation. The
128
+ * renderer resolves the display string as `labelKey ? t(labelKey) : label`. Frontend-only.
129
+ */
130
+ export interface TaskTypeMeta {
131
+ /** The task type id this meta describes. */
132
+ taskType: string
133
+ /** iconify name (lucide). */
134
+ icon: string
135
+ /** tailwind-ish accent token used across the card badge / picker. */
136
+ color: string
137
+ /** i18n key for a BUILT-IN type's label; absent for a custom type. */
138
+ labelKey?: string
139
+ /** Literal label for a CUSTOM type (from the wire presentation); absent for a built-in. */
140
+ label?: string
141
+ }
142
+
120
143
  /** Level-of-detail buckets driven by the canvas zoom level. Shallow → deep:
121
144
  * `far`/`mid`/`close` govern a service frame (chip → card → opened with tasks);
122
145
  * `steps`/`subtasks` drill spatially into an individual task — revealing its
@@ -5,6 +5,7 @@ import type {
5
5
  AgentKind,
6
6
  BlockStatus,
7
7
  BlockType,
8
+ TaskTypeMeta,
8
9
  } from '~/types/domain'
9
10
 
10
11
  /** Simple unique id helper (fine for a client-only prototype). */
@@ -698,6 +699,112 @@ export function isKnownAgentKind(kind: string): boolean {
698
699
  return kind in AGENT_BY_KIND || kind in SYSTEM_AGENT_META || kind in customAgentKindMeta.value
699
700
  }
700
701
 
702
+ // ---------------------------------------------------------------------------
703
+ // Task-type presentation (frontend-extension-mechanism initiative, slice B) — the exact twin of
704
+ // the agent-kind read-model above: a frozen BUILT-IN map + a reactive CUSTOM projection fed by the
705
+ // task-types store, resolved through {@link taskTypeMeta} so the card badge + create-task picker
706
+ // render a built-in OR a deployment-registered custom type — and an UNREGISTERED namespaced type
707
+ // (stale data after an extension was removed) degrades to the `feature` presentation, never a crash.
708
+ // ---------------------------------------------------------------------------
709
+
710
+ /**
711
+ * The BUILT-IN task-type presentations, keyed by type. Icons mirror the create-task picker's old
712
+ * hardcoded list; `labelKey` is the i18n key the renderer resolves (built-in labels are localized,
713
+ * unlike a custom type's literal wire label). `recurring` is not human-creatable but is stamped on
714
+ * the reused schedule block, so it needs a badge too.
715
+ */
716
+ export const TASK_TYPE_META: Record<string, TaskTypeMeta> = {
717
+ feature: {
718
+ taskType: 'feature',
719
+ icon: 'i-lucide-sparkles',
720
+ color: '#38bdf8',
721
+ labelKey: 'board.addTask.types.feature',
722
+ },
723
+ bug: {
724
+ taskType: 'bug',
725
+ icon: 'i-lucide-bug',
726
+ color: '#f87171',
727
+ labelKey: 'board.addTask.types.bug',
728
+ },
729
+ document: {
730
+ taskType: 'document',
731
+ icon: 'i-lucide-file-text',
732
+ color: '#c084fc',
733
+ labelKey: 'board.addTask.types.document',
734
+ },
735
+ spike: {
736
+ taskType: 'spike',
737
+ icon: 'i-lucide-flask-conical',
738
+ color: '#fbbf24',
739
+ labelKey: 'board.addTask.types.spike',
740
+ },
741
+ review: {
742
+ taskType: 'review',
743
+ icon: 'i-lucide-clipboard-check',
744
+ color: '#34d399',
745
+ labelKey: 'board.addTask.types.review',
746
+ },
747
+ ralph: {
748
+ taskType: 'ralph',
749
+ icon: 'i-lucide-infinity',
750
+ color: '#a78bfa',
751
+ labelKey: 'board.addTask.types.ralph',
752
+ },
753
+ recurring: {
754
+ taskType: 'recurring',
755
+ icon: 'i-lucide-repeat',
756
+ color: '#94a3b8',
757
+ labelKey: 'board.addTask.types.recurring',
758
+ },
759
+ }
760
+
761
+ /**
762
+ * Reactive read-model of the deployment's CUSTOM task types (consumer-slot + backend
763
+ * remote-manifest), kept in sync by the task-types store — the exact twin of
764
+ * {@link customAgentKindMeta}. The store is the source of truth; this `shallowRef` is its
765
+ * synchronous projection so {@link taskTypeMeta} / {@link isKnownTaskType} resolve a custom type
766
+ * WITHOUT importing the store (circular) or mutating {@link TASK_TYPE_META}.
767
+ */
768
+ const customTaskTypeMeta = shallowRef<Record<string, TaskTypeMeta>>({})
769
+
770
+ /** Replace the custom task-type projection (called only by the task-types store). */
771
+ export function setCustomTaskTypeMeta(map: Record<string, TaskTypeMeta>): void {
772
+ customTaskTypeMeta.value = map
773
+ }
774
+
775
+ /** Test-only: clear the custom task-type projection so a spec starts from built-ins only. */
776
+ export function __resetCustomTaskTypeMetaForTest(): void {
777
+ customTaskTypeMeta.value = {}
778
+ }
779
+
780
+ /**
781
+ * Resolve display metadata for ANY task type — a BUILT-IN ({@link TASK_TYPE_META}), a deployment
782
+ * CUSTOM type (projected into {@link customTaskTypeMeta} by the task-types store), or an
783
+ * UNREGISTERED namespaced one (stale data after its extension was removed) — ALWAYS returning a
784
+ * usable icon/color/label. An unregistered/unknown type falls back to the `feature` presentation
785
+ * (its icon/color) but keeps the raw id as its literal label, so a leftover string renders a badge
786
+ * instead of breaking the card. Reading `customTaskTypeMeta` reactively means a component computed
787
+ * re-runs when the custom catalog changes.
788
+ */
789
+ export function taskTypeMeta(taskType: string | undefined): TaskTypeMeta {
790
+ if (!taskType) return TASK_TYPE_META.feature!
791
+ return (
792
+ TASK_TYPE_META[taskType] ??
793
+ customTaskTypeMeta.value[taskType] ?? {
794
+ taskType,
795
+ icon: TASK_TYPE_META.feature!.icon,
796
+ color: TASK_TYPE_META.feature!.color,
797
+ // Unregistered namespaced type: no i18n key, so show its raw id rather than a blank badge.
798
+ label: taskType,
799
+ }
800
+ )
801
+ }
802
+
803
+ /** Whether a task type is known to this build — a built-in or a projected custom type. */
804
+ export function isKnownTaskType(taskType: string): boolean {
805
+ return taskType in TASK_TYPE_META || taskType in customTaskTypeMeta.value
806
+ }
807
+
701
808
  type BlockTypeMeta = { label: string; icon: string; accent: string }
702
809
 
703
810
  /** Visual metadata for each architecture block type. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.147.6",
3
+ "version": "0.148.1",
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",
@@ -40,7 +40,7 @@
40
40
  "valibot": "^1.4.2",
41
41
  "vue": "3.5.40",
42
42
  "wretch": "^3.0.9",
43
- "@cat-factory/contracts": "0.154.2"
43
+ "@cat-factory/contracts": "0.156.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@toad-contracts/testing": "0.3.2",