@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.
- package/app/components/board/AddTaskModal.vue +146 -3
- package/app/components/board/nodes/TaskCard.vue +29 -1
- package/app/components/settings/WorkspaceSettingsPanel.vue +14 -8
- package/app/docs/consumer-extensions.md +41 -8
- package/app/modular/agent-kinds.spec.ts +1 -39
- package/app/modular/agent-kinds.ts +7 -58
- package/app/modular/capabilities.spec.ts +88 -0
- package/app/modular/capabilities.ts +77 -0
- package/app/modular/nav-contributions.spec.ts +2 -0
- package/app/modular/registry.ts +8 -1
- package/app/modular/slots.ts +13 -1
- package/app/modular/task-types.ts +27 -0
- package/app/plugins/modular.client.ts +8 -3
- package/app/stores/agents.spec.ts +13 -9
- package/app/stores/agents.ts +16 -17
- package/app/stores/board/context.ts +46 -0
- package/app/stores/board/mutations.ts +362 -0
- package/app/stores/board/removal.ts +172 -0
- package/app/stores/board.ts +16 -523
- package/app/stores/taskTypes.spec.ts +100 -0
- package/app/stores/taskTypes.ts +88 -0
- package/app/stores/workspace.ts +14 -4
- package/app/types/domain.ts +23 -0
- package/app/utils/catalog.ts +107 -0
- package/package.json +2 -2
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { RemoteModuleManifest } from '@modular-vue/core'
|
|
2
|
+
import type { CustomAgentKind, CustomTaskType } from '~/types/domain'
|
|
3
|
+
import type { AppSlots } from './slots'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The per-workspace capability manifest (modular-vue adoption slice 2 + the
|
|
7
|
+
* frontend-extension-mechanism initiative, slice B).
|
|
8
|
+
*
|
|
9
|
+
* A deployment's BACKEND-registered capabilities arrive in the workspace snapshot as wire data
|
|
10
|
+
* — `customAgentKinds` (slice 2) AND `customTaskTypes` (slice B). Rather than mutating
|
|
11
|
+
* module-global catalogs, BOTH are folded into ONE {@link RemoteModuleManifest} whose slots carry
|
|
12
|
+
* the data. cat-factory holds ONE active manifest per workspace (swapped on snapshot), so each
|
|
13
|
+
* store reads its OWN slot off the shared manifest — the sanctioned single-active-manifest shape
|
|
14
|
+
* (`mergeRemoteManifests` is for holding several at once, which we don't). CODE-shipped consumer
|
|
15
|
+
* capabilities instead enter via the static `agentKinds` / `taskTypes` slots (a `registerAppModule`
|
|
16
|
+
* module); each store merges its slot + its manifest half.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** The stable id for the per-workspace capability manifest built from the snapshot. */
|
|
20
|
+
export const WORKSPACE_CAPABILITIES_MANIFEST_ID = 'cat-factory:workspace-capabilities'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A deterministic, key-order-independent content signature covering BOTH capability lists, used as
|
|
24
|
+
* the manifest `version`. The workspace snapshot re-delivers the SAME deployment capabilities on
|
|
25
|
+
* every board-event refresh (a full `workspace.refresh()` re-hydrates each time), so a
|
|
26
|
+
* content-derived version lets each store skip re-projecting an UNCHANGED catalog — otherwise every
|
|
27
|
+
* refresh would replace its read-model and needlessly invalidate every `agentKindMeta` /
|
|
28
|
+
* `taskTypeMeta` consumer. It still changes (and swaps wholesale) when a different workspace's
|
|
29
|
+
* capabilities genuinely differ. Serialized as fixed-order tuples of only the fields that affect
|
|
30
|
+
* display/pairing, so a re-serialization with reordered object keys can't spuriously differ.
|
|
31
|
+
*/
|
|
32
|
+
export function workspaceCapabilitiesVersion(
|
|
33
|
+
kinds: readonly CustomAgentKind[],
|
|
34
|
+
taskTypes: readonly CustomTaskType[],
|
|
35
|
+
): string {
|
|
36
|
+
return JSON.stringify([
|
|
37
|
+
kinds.map((k) => [
|
|
38
|
+
k.kind,
|
|
39
|
+
k.container,
|
|
40
|
+
k.presentation.label,
|
|
41
|
+
k.presentation.icon,
|
|
42
|
+
k.presentation.color,
|
|
43
|
+
k.presentation.description,
|
|
44
|
+
k.presentation.category ?? null,
|
|
45
|
+
k.presentation.resultView ?? null,
|
|
46
|
+
]),
|
|
47
|
+
taskTypes.map((t) => [
|
|
48
|
+
t.taskType,
|
|
49
|
+
t.presentation.label,
|
|
50
|
+
t.presentation.icon,
|
|
51
|
+
t.presentation.color,
|
|
52
|
+
t.presentation.description,
|
|
53
|
+
t.defaultPipelineId ?? null,
|
|
54
|
+
t.formPanel ?? null,
|
|
55
|
+
// The descriptor list affects the create-form, so fold its shape into the signature too.
|
|
56
|
+
(t.fields ?? []).map((f) => [f.key, f.type, f.label, f.required ?? false]),
|
|
57
|
+
]),
|
|
58
|
+
])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Model the snapshot's `customAgentKinds` + `customTaskTypes` as a single remote capability
|
|
63
|
+
* manifest. The `version` is a {@link workspaceCapabilitiesVersion content signature over both
|
|
64
|
+
* lists} so identical snapshots produce an identical manifest — which each store's
|
|
65
|
+
* `hydrateCapabilities` uses to no-op an unchanged re-hydrate. Swapped wholesale (not diffed) when
|
|
66
|
+
* the content genuinely changes.
|
|
67
|
+
*/
|
|
68
|
+
export function buildWorkspaceCapabilitiesManifest(
|
|
69
|
+
kinds: readonly CustomAgentKind[],
|
|
70
|
+
taskTypes: readonly CustomTaskType[],
|
|
71
|
+
): RemoteModuleManifest<AppSlots> {
|
|
72
|
+
return {
|
|
73
|
+
id: WORKSPACE_CAPABILITIES_MANIFEST_ID,
|
|
74
|
+
version: workspaceCapabilitiesVersion(kinds, taskTypes),
|
|
75
|
+
slots: { agentKinds: [...kinds], taskTypes: [...taskTypes] },
|
|
76
|
+
}
|
|
77
|
+
}
|
package/app/modular/registry.ts
CHANGED
|
@@ -101,7 +101,14 @@ 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: {
|
|
104
|
+
slots: {
|
|
105
|
+
nav: [],
|
|
106
|
+
resultViews: [],
|
|
107
|
+
agentKinds: [],
|
|
108
|
+
inspectorPanels: [],
|
|
109
|
+
taskTypes: [],
|
|
110
|
+
taskTypeFormPanels: [],
|
|
111
|
+
},
|
|
105
112
|
}).use(journeysPlugin())
|
|
106
113
|
for (const mod of [...FIRST_PARTY_MODULES, ...extraModules, ...consumerModules]) {
|
|
107
114
|
registry.register(mod)
|
package/app/modular/slots.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Component } from 'vue'
|
|
2
2
|
import type { ComponentEntry, PanelEntry } from '@modular-vue/core'
|
|
3
|
-
import type { Block, CustomAgentKind } from '~/types/domain'
|
|
3
|
+
import type { Block, CustomAgentKind, CustomTaskType } from '~/types/domain'
|
|
4
4
|
import type { NavContribution } from './nav-contributions'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -23,6 +23,14 @@ import type { NavContribution } from './nav-contributions'
|
|
|
23
23
|
* predicate. Replaces `InspectorPanel.vue`'s level/type `v-if` fan; a consumer
|
|
24
24
|
* contributes its own panels to the SAME slot via `registerAppModule`. This is
|
|
25
25
|
* the slot key `definePanelGroup<Block>('inspectorPanels')` names.
|
|
26
|
+
* - `taskTypes` (extension slice B) — CODE-shipped custom task types a consumer
|
|
27
|
+
* module contributes (the create-task-picker + card-badge data half). BACKEND-
|
|
28
|
+
* registered types arrive separately in the shared capability manifest read by
|
|
29
|
+
* the task-types store — see `stores/taskTypes.ts`. Symmetric with `agentKinds`.
|
|
30
|
+
* - `taskTypeFormPanels` (extension slice B) — a bespoke create-task-form section
|
|
31
|
+
* per custom task type, addressed by the type's `formPanel` id and paired via
|
|
32
|
+
* `resolveComponentRegistry` (same shape as `resultViews`); shown INSTEAD of the
|
|
33
|
+
* descriptor-driven `fields`. An unpaired id degrades to the descriptor fields.
|
|
26
34
|
*
|
|
27
35
|
* The index signature is mutable (`unknown[]`) to satisfy the runtime's
|
|
28
36
|
* `SlotMap` constraint while `unknown[]` still meets `useReactiveSlots`'
|
|
@@ -33,6 +41,8 @@ export interface AppSlots {
|
|
|
33
41
|
resultViews: ResultViewContribution[]
|
|
34
42
|
agentKinds: CustomAgentKind[]
|
|
35
43
|
inspectorPanels: PanelEntry<Block>[]
|
|
44
|
+
taskTypes: CustomTaskType[]
|
|
45
|
+
taskTypeFormPanels: ResultViewContribution[]
|
|
36
46
|
[key: string]: unknown[]
|
|
37
47
|
}
|
|
38
48
|
|
|
@@ -43,5 +53,7 @@ export interface AppSlots {
|
|
|
43
53
|
* helpers index and pair it with the wire-delivered `presentation.resultView`
|
|
44
54
|
* id — the sanctioned "backend data selects a code-shipped, locally-registered
|
|
45
55
|
* component" pairing (see the modular-vue Remote Capability Manifests guide).
|
|
56
|
+
* Reused for `taskTypeFormPanels` (a bespoke create-form section paired by a
|
|
57
|
+
* custom task type's `formPanel` id).
|
|
46
58
|
*/
|
|
47
59
|
export type ResultViewContribution = ComponentEntry<Component>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CustomTaskType, TaskTypeMeta } from '~/types/domain'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom task-type projection (frontend-extension-mechanism initiative, slice B — the frontend
|
|
5
|
+
* analogue of `customKindToArchetype` for agent kinds).
|
|
6
|
+
*
|
|
7
|
+
* A deployment's BACKEND-registered task types arrive in the workspace snapshot as
|
|
8
|
+
* `customTaskTypes` (wire data), folded into the shared per-workspace capability manifest (see
|
|
9
|
+
* `./capabilities.ts`). CODE-shipped consumer task types instead enter via the static `taskTypes`
|
|
10
|
+
* slot (a `registerAppModule` module); the task-types store merges both. This module holds only
|
|
11
|
+
* the wire→display projection they share.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Project a wire {@link CustomTaskType} onto the frontend's display {@link TaskTypeMeta}. A custom
|
|
16
|
+
* type carries a LITERAL label (from the wire presentation), unlike a built-in type whose label is
|
|
17
|
+
* an i18n key — so the projected meta sets `label` (not `labelKey`); the renderer resolves the
|
|
18
|
+
* display string accordingly (`labelKey ? t(labelKey) : label`).
|
|
19
|
+
*/
|
|
20
|
+
export function customTaskTypeToMeta(t: CustomTaskType): TaskTypeMeta {
|
|
21
|
+
return {
|
|
22
|
+
taskType: t.taskType,
|
|
23
|
+
icon: t.presentation.icon,
|
|
24
|
+
color: t.presentation.color,
|
|
25
|
+
label: t.presentation.label,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
environmentSetupPersistence,
|
|
17
17
|
} from '~/modular/journeys/environmentSetup'
|
|
18
18
|
import type { AppSlots, ResultViewContribution } from '~/modular/slots'
|
|
19
|
-
import type { Block, CustomAgentKind } from '~/types/domain'
|
|
19
|
+
import type { Block, CustomAgentKind, CustomTaskType } from '~/types/domain'
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Wire the modular-vue registry into the Nuxt app (slice 0 of the modular-vue
|
|
@@ -98,9 +98,14 @@ export default defineNuxtPlugin({
|
|
|
98
98
|
// selected. `<PanelsOutlet>` re-resolves reactively per subject; this only
|
|
99
99
|
// validates the wiring.
|
|
100
100
|
resolvePanels((slots.inspectorPanels ?? []) as PanelEntry<Block>[], null)
|
|
101
|
-
//
|
|
102
|
-
//
|
|
101
|
+
// Same fail-fast for the custom-task-type form-panel registry (extension slice B): a
|
|
102
|
+
// duplicate `formPanel` id across the first-party + consumer modules throws at BOOT
|
|
103
|
+
// rather than the first time the create-task form opens a custom type's section.
|
|
104
|
+
resolveComponentRegistry((slots.taskTypeFormPanels ?? []) as ResultViewContribution[])
|
|
105
|
+
// Consumer agent kinds + task types contributed as CODE to the static `agentKinds` /
|
|
106
|
+
// `taskTypes` slots (module slots resolve once, so the static base is the full set).
|
|
103
107
|
useAgentsStore().registerConsumerKinds((slots.agentKinds ?? []) as CustomAgentKind[])
|
|
108
|
+
useTaskTypesStore().registerConsumerTaskTypes((slots.taskTypes ?? []) as CustomTaskType[])
|
|
104
109
|
return { provide: { modular: manifest } }
|
|
105
110
|
},
|
|
106
111
|
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { useAgentsStore } from './agents'
|
|
3
|
+
import { buildWorkspaceCapabilitiesManifest } from '~/modular/capabilities'
|
|
3
4
|
import {
|
|
4
5
|
__resetCustomAgentKindMetaForTest,
|
|
5
6
|
agentKindMeta,
|
|
@@ -23,6 +24,11 @@ const backendKind = (
|
|
|
23
24
|
},
|
|
24
25
|
})
|
|
25
26
|
|
|
27
|
+
/** Hydrate the store's backend-manifest half with `kinds` (via the shared capability manifest). */
|
|
28
|
+
const hydrate = (agents: ReturnType<typeof useAgentsStore>, kinds: CustomAgentKind[]): void => {
|
|
29
|
+
agents.hydrateCapabilities(buildWorkspaceCapabilitiesManifest(kinds, []))
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
describe('agents store — custom-kind catalog (slice 2)', () => {
|
|
27
33
|
it('exposes only the built-in archetypes before any custom kind is hydrated', () => {
|
|
28
34
|
const agents = useAgentsStore()
|
|
@@ -37,9 +43,7 @@ describe('agents store — custom-kind catalog (slice 2)', () => {
|
|
|
37
43
|
expect(isKnownAgentKind('acme-audit')).toBe(false)
|
|
38
44
|
expect(agentKindMeta('acme-audit').label).toBe('Agent') // generic fallback
|
|
39
45
|
|
|
40
|
-
agents
|
|
41
|
-
backendKind('acme-audit', { category: 'review', resultView: 'acme:audit' }),
|
|
42
|
-
])
|
|
46
|
+
hydrate(agents, [backendKind('acme-audit', { category: 'review', resultView: 'acme:audit' })])
|
|
43
47
|
|
|
44
48
|
// Palette + kind lookup both see it now (sync-flush projection — no tick needed).
|
|
45
49
|
expect(agents.archetypes.some((a) => a.kind === 'acme-audit')).toBe(true)
|
|
@@ -52,7 +56,7 @@ describe('agents store — custom-kind catalog (slice 2)', () => {
|
|
|
52
56
|
|
|
53
57
|
it('never lets a custom kind shadow a built-in kind', () => {
|
|
54
58
|
const agents = useAgentsStore()
|
|
55
|
-
agents
|
|
59
|
+
hydrate(agents, [backendKind('coder', { label: 'Evil Coder' })])
|
|
56
60
|
// `coder` is a built-in; the custom entry is dropped, the built-in wins.
|
|
57
61
|
expect(agents.customArchetypes.some((a) => a.kind === 'coder')).toBe(false)
|
|
58
62
|
expect(agentKindMeta('coder').label).not.toBe('Evil Coder')
|
|
@@ -61,7 +65,7 @@ describe('agents store — custom-kind catalog (slice 2)', () => {
|
|
|
61
65
|
it('merges CODE-shipped consumer kinds with BACKEND manifest kinds, de-duplicated', () => {
|
|
62
66
|
const agents = useAgentsStore()
|
|
63
67
|
agents.registerConsumerKinds([backendKind('acme-consumer')])
|
|
64
|
-
agents
|
|
68
|
+
hydrate(agents, [backendKind('acme-backend'), backendKind('acme-consumer')])
|
|
65
69
|
const kinds = agents.customArchetypes.map((a) => a.kind)
|
|
66
70
|
expect(kinds).toContain('acme-consumer')
|
|
67
71
|
expect(kinds).toContain('acme-backend')
|
|
@@ -71,20 +75,20 @@ describe('agents store — custom-kind catalog (slice 2)', () => {
|
|
|
71
75
|
|
|
72
76
|
it('no-ops a re-hydrate of identical content (stable projection, content-versioned manifest)', () => {
|
|
73
77
|
const agents = useAgentsStore()
|
|
74
|
-
agents
|
|
78
|
+
hydrate(agents, [backendKind('acme-x', { resultView: 'acme:x' })])
|
|
75
79
|
const first = agents.customArchetypes
|
|
76
80
|
// A new snapshot re-delivering structurally-equal kinds (fresh objects) must not
|
|
77
81
|
// recompute the merged catalog — the content-derived manifest version is unchanged, so
|
|
78
82
|
// `hydrateCustomKinds` skips the swap and the computed keeps its cached identity.
|
|
79
|
-
agents
|
|
83
|
+
hydrate(agents, [backendKind('acme-x', { resultView: 'acme:x' })])
|
|
80
84
|
expect(agents.customArchetypes).toBe(first)
|
|
81
85
|
})
|
|
82
86
|
|
|
83
87
|
it('swaps the backend catalog wholesale on re-hydrate (per-workspace manifest)', () => {
|
|
84
88
|
const agents = useAgentsStore()
|
|
85
|
-
agents
|
|
89
|
+
hydrate(agents, [backendKind('ws1-kind')])
|
|
86
90
|
expect(agents.customArchetypes.some((a) => a.kind === 'ws1-kind')).toBe(true)
|
|
87
|
-
agents
|
|
91
|
+
hydrate(agents, [backendKind('ws2-kind')])
|
|
88
92
|
expect(agents.customArchetypes.some((a) => a.kind === 'ws1-kind')).toBe(false)
|
|
89
93
|
expect(agents.customArchetypes.some((a) => a.kind === 'ws2-kind')).toBe(true)
|
|
90
94
|
expect(isKnownAgentKind('ws1-kind')).toBe(false)
|
package/app/stores/agents.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { computed, ref, watch } from 'vue'
|
|
3
3
|
import type { RemoteModuleManifest } from '@modular-vue/core'
|
|
4
|
-
import {
|
|
4
|
+
import { customKindToArchetype } from '~/modular/agent-kinds'
|
|
5
5
|
import type { AppSlots } from '~/modular/slots'
|
|
6
6
|
import {
|
|
7
7
|
AGENT_ARCHETYPES,
|
|
@@ -21,9 +21,9 @@ import type { AgentArchetype, AgentKind, CustomAgentKind } from '~/types/domain'
|
|
|
21
21
|
* - the built-in archetypes (static);
|
|
22
22
|
* - CONSUMER-shipped kinds contributed as CODE via the modular `agentKinds`
|
|
23
23
|
* slot (`registerConsumerKinds`, fed once at boot from the resolved manifest);
|
|
24
|
-
* - the deployment's BACKEND-registered kinds,
|
|
24
|
+
* - the deployment's BACKEND-registered kinds, read from the shared per-workspace
|
|
25
25
|
* {@link RemoteModuleManifest} swapped per workspace snapshot
|
|
26
|
-
* (`
|
|
26
|
+
* (`hydrateCapabilities`, reading its own `agentKinds` slot — single-active-manifest shape).
|
|
27
27
|
*
|
|
28
28
|
* The merged custom catalog is projected back into `catalog.ts`'s
|
|
29
29
|
* {@link setCustomAgentKindMeta} read-model so the pure `agentKindMeta` /
|
|
@@ -77,7 +77,7 @@ export const useAgentsStore = defineStore('agents', () => {
|
|
|
77
77
|
|
|
78
78
|
// Keep `catalog.ts`'s pure-util projection in sync with the merged custom
|
|
79
79
|
// catalog so `agentKindMeta` / `isKnownAgentKind` resolve custom kinds. Sync
|
|
80
|
-
// flush so an imperative read right after `
|
|
80
|
+
// flush so an imperative read right after `hydrateCapabilities` (e.g. the run
|
|
81
81
|
// dispatch resolving a custom kind's `resultView`) sees the fresh catalog with
|
|
82
82
|
// no tick gap. The watch lives in the store's effect scope (disposed with it).
|
|
83
83
|
watch(customByKind, (map) => setCustomAgentKindMeta(map), { immediate: true, flush: 'sync' })
|
|
@@ -119,20 +119,19 @@ export const useAgentsStore = defineStore('agents', () => {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
/**
|
|
122
|
-
* Hydrate the deployment's BACKEND-registered custom kinds from
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* {@link AGENT_BY_KIND} directly.
|
|
122
|
+
* Hydrate the deployment's BACKEND-registered custom kinds from the shared per-workspace
|
|
123
|
+
* capability manifest (built by the workspace store from the snapshot, carrying both `agentKinds`
|
|
124
|
+
* + `taskTypes`; this store reads only its own `agentKinds` slot). Swapped wholesale per
|
|
125
|
+
* workspace. Replaces the old `registerCustomKinds` that mutated {@link AGENT_BY_KIND} directly.
|
|
126
126
|
*
|
|
127
|
-
* The snapshot re-delivers the same deployment kinds on every board refresh, so
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
127
|
+
* The snapshot re-delivers the same deployment kinds on every board refresh, so skip the swap —
|
|
128
|
+
* and the downstream projection invalidation of every `agentKindMeta` consumer — when the
|
|
129
|
+
* content-derived manifest version is unchanged. A genuinely different workspace's capabilities
|
|
130
|
+
* change the version and swap.
|
|
131
131
|
*/
|
|
132
|
-
function
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
capabilitiesManifest.value = next
|
|
132
|
+
function hydrateCapabilities(manifest: RemoteModuleManifest<AppSlots>) {
|
|
133
|
+
if (capabilitiesManifest.value?.version === manifest.version) return
|
|
134
|
+
capabilitiesManifest.value = manifest
|
|
136
135
|
}
|
|
137
136
|
|
|
138
137
|
return {
|
|
@@ -141,6 +140,6 @@ export const useAgentsStore = defineStore('agents', () => {
|
|
|
141
140
|
get,
|
|
142
141
|
addAgent,
|
|
143
142
|
registerConsumerKinds,
|
|
144
|
-
|
|
143
|
+
hydrateCapabilities,
|
|
145
144
|
}
|
|
146
145
|
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Ref } from 'vue'
|
|
2
|
+
import type { Block } from '~/types/domain'
|
|
3
|
+
|
|
4
|
+
/** A detached subtree captured before an optimistic delete, restored on failure. */
|
|
5
|
+
export interface RemovalSnapshot {
|
|
6
|
+
/** The removed block + all its descendants, in their original order. */
|
|
7
|
+
removed: Block[]
|
|
8
|
+
/**
|
|
9
|
+
* Survivors whose `dependsOn`/`epicId`/`initiativeId` lost an edge to a removed block
|
|
10
|
+
* (originals to restore on rollback).
|
|
11
|
+
*/
|
|
12
|
+
edges: { id: string; dependsOn: string[]; epicId: string | null; initiativeId: string | null }[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A still-pending optimistic delete: its snapshot, the deferred-commit timer, and its workspace. */
|
|
16
|
+
export interface PendingRemoval {
|
|
17
|
+
snap: RemovalSnapshot
|
|
18
|
+
timer: ReturnType<typeof setTimeout>
|
|
19
|
+
wsId: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* How long a deleted block stays undoable. The backend delete is DEFERRED for this
|
|
24
|
+
* window (a real "undo", not a client illusion) — the block is hidden immediately but
|
|
25
|
+
* only actually deleted once the window elapses, so undo just cancels the pending call.
|
|
26
|
+
*/
|
|
27
|
+
export const UNDO_WINDOW_MS = 6000
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Shared state + injected dependencies the board-store write factories close over. Created once in
|
|
31
|
+
* the `board` store setup and threaded into {@link createBoardMutations} / {@link createBoardRemoval}
|
|
32
|
+
* so the split operations stay behaviourally identical to the original single-closure store — the
|
|
33
|
+
* factories are cohesive extractions purely to keep each function within the size budget, not new
|
|
34
|
+
* seams. `api`/`toast`/`tr` are the store's own resolved handles (a store runs outside a component
|
|
35
|
+
* `setup`, so `tr` bridges to the Nuxt app's global i18n instance).
|
|
36
|
+
*/
|
|
37
|
+
export interface BoardWriteContext {
|
|
38
|
+
blocks: Ref<Block[]>
|
|
39
|
+
getBlock: (id: string) => Block | undefined
|
|
40
|
+
upsert: (block: Block) => void
|
|
41
|
+
pendingRemovals: Map<string, PendingRemoval>
|
|
42
|
+
pendingDoomed: Set<string>
|
|
43
|
+
api: ReturnType<typeof useApi>
|
|
44
|
+
toast: ReturnType<typeof useToast>
|
|
45
|
+
tr: (key: string, params?: Record<string, unknown>) => string
|
|
46
|
+
}
|