@cat-factory/app 0.87.4 → 0.87.5
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/panels/inspector/ServiceFragments.vue +11 -15
- package/app/components/panels/inspector/TaskStructure.vue +15 -15
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +3 -9
- package/app/utils/fragmentPicker.spec.ts +60 -0
- package/app/utils/fragmentPicker.ts +32 -0
- package/package.json +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
2
3
|
import type { Block } from '~/types/domain'
|
|
3
4
|
import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
|
|
5
|
+
import { buildFragmentPickerGroups } from '~/utils/fragmentPicker'
|
|
4
6
|
|
|
5
7
|
// Service-level best-practice fragments (frame blocks). These are the programming
|
|
6
8
|
// standards/guidelines for the whole service; at run time their bodies are folded
|
|
@@ -19,8 +21,6 @@ const { t } = useI18n()
|
|
|
19
21
|
|
|
20
22
|
onMounted(() => fragments.ensureLoaded())
|
|
21
23
|
|
|
22
|
-
type MenuItem = { label: string; icon?: string; onSelect: () => void }
|
|
23
|
-
|
|
24
24
|
// An id the catalog no longer resolves (removed/suppressed after selection) still
|
|
25
25
|
// renders — labelled by its raw id — so it stays visible and removable.
|
|
26
26
|
const selectedFragments = computed(() =>
|
|
@@ -32,8 +32,8 @@ const selectedFragments = computed(() =>
|
|
|
32
32
|
// A trailing group that jumps from "attach a fragment" to authoring/editing the
|
|
33
33
|
// library itself (board tier always; account tier when accounts are enabled).
|
|
34
34
|
// Open to every member — managing fragments is not an admin-only action.
|
|
35
|
-
const manageItems = computed<
|
|
36
|
-
const items:
|
|
35
|
+
const manageItems = computed<DropdownMenuItem[]>(() => {
|
|
36
|
+
const items: DropdownMenuItem[] = [
|
|
37
37
|
{
|
|
38
38
|
label: t('inspector.fragments.manageBoard'),
|
|
39
39
|
icon: 'i-lucide-book-marked',
|
|
@@ -50,18 +50,14 @@ const manageItems = computed<MenuItem[]>(() => {
|
|
|
50
50
|
return items
|
|
51
51
|
})
|
|
52
52
|
|
|
53
|
-
// Picker menu: every pool fragment not already selected, grouped
|
|
54
|
-
// the management links appended as the final group.
|
|
55
|
-
const fragmentMenu = computed<
|
|
53
|
+
// Picker menu: every pool fragment not already selected, grouped into labelled
|
|
54
|
+
// per-category sections, with the management links appended as the final group.
|
|
55
|
+
const fragmentMenu = computed<DropdownMenuItem[][]>(() => {
|
|
56
56
|
const selected = new Set(props.block.serviceFragmentIds ?? [])
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
items.push({ label: f.title, onSelect: () => addFragment(f.id) })
|
|
62
|
-
groups.set(f.category, items)
|
|
63
|
-
}
|
|
64
|
-
return [...groups.values(), manageItems.value]
|
|
57
|
+
return [
|
|
58
|
+
...buildFragmentPickerGroups(fragments.fragments, (id) => selected.has(id), addFragment),
|
|
59
|
+
manageItems.value,
|
|
60
|
+
]
|
|
65
61
|
})
|
|
66
62
|
|
|
67
63
|
function addFragment(id: string) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
2
3
|
import type { Block } from '~/types/domain'
|
|
3
4
|
import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
|
|
5
|
+
import { buildFragmentPickerGroups } from '~/utils/fragmentPicker'
|
|
4
6
|
|
|
5
7
|
const props = defineProps<{ block: Block }>()
|
|
6
8
|
|
|
@@ -14,8 +16,6 @@ const { t } = useI18n()
|
|
|
14
16
|
// task inspector mounts — mirrors ServiceFragments; ensureLoaded is a no-op while current.
|
|
15
17
|
onMounted(() => fragments.ensureLoaded())
|
|
16
18
|
|
|
17
|
-
type MenuItem = { label: string; icon?: string; onSelect: () => void }
|
|
18
|
-
|
|
19
19
|
// ---- best-practice prompt fragments ----------------------------------------
|
|
20
20
|
// Selected fragments, resolved against the catalog. An id the catalog no longer
|
|
21
21
|
// resolves (removed/suppressed after selection) still renders — labelled by its
|
|
@@ -29,8 +29,8 @@ const selectedFragments = computed(() =>
|
|
|
29
29
|
// A trailing group that jumps from "attach a fragment" to authoring/editing the
|
|
30
30
|
// library itself (board tier always; account tier when accounts are enabled).
|
|
31
31
|
// Open to every member — managing fragments is not an admin-only action.
|
|
32
|
-
const manageItems = computed<
|
|
33
|
-
const items:
|
|
32
|
+
const manageItems = computed<DropdownMenuItem[]>(() => {
|
|
33
|
+
const items: DropdownMenuItem[] = [
|
|
34
34
|
{
|
|
35
35
|
label: t('inspector.fragments.manageBoard'),
|
|
36
36
|
icon: 'i-lucide-book-marked',
|
|
@@ -48,18 +48,18 @@ const manageItems = computed<MenuItem[]>(() => {
|
|
|
48
48
|
})
|
|
49
49
|
|
|
50
50
|
// Picker menu: fragments suitable for this block's type, not already selected,
|
|
51
|
-
// grouped
|
|
52
|
-
// links appended as the final group.
|
|
53
|
-
const fragmentMenu = computed<
|
|
51
|
+
// grouped into labelled per-category sections so the dropdown reads like the catalog,
|
|
52
|
+
// with the management links appended as the final group.
|
|
53
|
+
const fragmentMenu = computed<DropdownMenuItem[][]>(() => {
|
|
54
54
|
const selected = new Set(props.block.fragmentIds ?? [])
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
return [
|
|
56
|
+
...buildFragmentPickerGroups(
|
|
57
|
+
fragments.forBlockType(props.block.type),
|
|
58
|
+
(id) => selected.has(id),
|
|
59
|
+
addFragment,
|
|
60
|
+
),
|
|
61
|
+
manageItems.value,
|
|
62
|
+
]
|
|
63
63
|
})
|
|
64
64
|
|
|
65
65
|
function addFragment(id: string) {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// services — each owns its selection from creation. Persisted via the
|
|
7
7
|
// serviceFragmentDefaults store (the backend replaces the whole list on each change).
|
|
8
8
|
import { onMounted, ref } from 'vue'
|
|
9
|
+
import { buildFragmentPickerGroups } from '~/utils/fragmentPicker'
|
|
9
10
|
|
|
10
11
|
const { t } = useI18n()
|
|
11
12
|
const fragments = useFragmentsStore()
|
|
@@ -24,17 +25,10 @@ const selected = computed(() =>
|
|
|
24
25
|
defaults.fragmentIds.map((id) => fragments.getFragment(id) ?? { id, title: id, summary: '' }),
|
|
25
26
|
)
|
|
26
27
|
|
|
27
|
-
// Pool fragments not already in the default set, grouped
|
|
28
|
+
// Pool fragments not already in the default set, grouped into labelled per-category sections.
|
|
28
29
|
const menu = computed(() => {
|
|
29
30
|
const chosen = new Set(defaults.fragmentIds)
|
|
30
|
-
|
|
31
|
-
for (const f of fragments.fragments) {
|
|
32
|
-
if (chosen.has(f.id)) continue
|
|
33
|
-
const items = groups.get(f.category) ?? []
|
|
34
|
-
items.push({ label: f.title, onSelect: () => add(f.id) })
|
|
35
|
-
groups.set(f.category, items)
|
|
36
|
-
}
|
|
37
|
-
return [...groups.values()]
|
|
31
|
+
return buildFragmentPickerGroups(fragments.fragments, (id) => chosen.has(id), add)
|
|
38
32
|
})
|
|
39
33
|
|
|
40
34
|
async function save(ids: string[]) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import type { PromptFragment } from '~/types/domain'
|
|
3
|
+
import { buildFragmentPickerGroups } from './fragmentPicker'
|
|
4
|
+
|
|
5
|
+
const frag = (id: string, category: string, title = id): PromptFragment =>
|
|
6
|
+
({ id, version: '1.0.0', title, category, summary: '', body: '' }) as PromptFragment
|
|
7
|
+
|
|
8
|
+
const pool: PromptFragment[] = [
|
|
9
|
+
frag('node.best-practices', 'Node', 'Node best practices'),
|
|
10
|
+
frag('node.performance', 'Node', 'Node performance'),
|
|
11
|
+
frag('style.anti-llmisms', 'Writing style', 'Avoid LLM tells'),
|
|
12
|
+
frag('style.concise-actionable', 'Writing style', 'Concise and actionable'),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
describe('buildFragmentPickerGroups', () => {
|
|
16
|
+
it('buckets fragments into one labelled group per category (technical + writing-style tracks)', () => {
|
|
17
|
+
const groups = buildFragmentPickerGroups(
|
|
18
|
+
pool,
|
|
19
|
+
() => false,
|
|
20
|
+
() => {},
|
|
21
|
+
)
|
|
22
|
+
// One group per category, each led by a non-interactive `type: 'label'` heading.
|
|
23
|
+
expect(groups.map((g) => g[0])).toEqual([
|
|
24
|
+
{ type: 'label', label: 'Node' },
|
|
25
|
+
{ type: 'label', label: 'Writing style' },
|
|
26
|
+
])
|
|
27
|
+
// The heading is followed by that category's item labels, in pool order.
|
|
28
|
+
expect(groups[0]!.slice(1).map((i) => i.label)).toEqual([
|
|
29
|
+
'Node best practices',
|
|
30
|
+
'Node performance',
|
|
31
|
+
])
|
|
32
|
+
expect(groups[1]!.slice(1).map((i) => i.label)).toEqual([
|
|
33
|
+
'Avoid LLM tells',
|
|
34
|
+
'Concise and actionable',
|
|
35
|
+
])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('omits already-selected fragments, dropping a category that empties out', () => {
|
|
39
|
+
const selected = new Set(['style.anti-llmisms', 'style.concise-actionable'])
|
|
40
|
+
const groups = buildFragmentPickerGroups(
|
|
41
|
+
pool,
|
|
42
|
+
(id) => selected.has(id),
|
|
43
|
+
() => {},
|
|
44
|
+
)
|
|
45
|
+
// Writing style fully selected → its category disappears entirely (no empty labelled group).
|
|
46
|
+
expect(groups.map((g) => g[0])).toEqual([{ type: 'label', label: 'Node' }])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('invokes onSelect with the fragment id when an item is chosen', () => {
|
|
50
|
+
const picked: string[] = []
|
|
51
|
+
const groups = buildFragmentPickerGroups(
|
|
52
|
+
pool,
|
|
53
|
+
() => false,
|
|
54
|
+
(id) => picked.push(id),
|
|
55
|
+
)
|
|
56
|
+
// Fire the first real item under the first category (index 1, past the label heading).
|
|
57
|
+
;(groups[0]![1] as { onSelect: () => void }).onSelect()
|
|
58
|
+
expect(picked).toEqual(['node.best-practices'])
|
|
59
|
+
})
|
|
60
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
2
|
+
import type { PromptFragment } from '~/types/domain'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build the category-grouped groups for a fragment "add" dropdown: the pool fragments not
|
|
6
|
+
* already selected, bucketed by `category`, each bucket prefixed with a non-interactive
|
|
7
|
+
* `type: 'label'` heading so the catalog reads as labelled sections instead of one flat,
|
|
8
|
+
* undifferentiated list. This matters now that the catalog spans distinct tracks that a
|
|
9
|
+
* single block can pin together — the technical collections (Node / React / …) AND the
|
|
10
|
+
* document Writing-style fragments — so the headings keep the longer, mixed list navigable.
|
|
11
|
+
*
|
|
12
|
+
* Category order follows first appearance in `pool`; empty categories are dropped. Each
|
|
13
|
+
* inner array is one Nuxt UI menu group (rendered divider-separated); callers append their
|
|
14
|
+
* own trailing groups (e.g. the library-management links) after the returned groups.
|
|
15
|
+
*/
|
|
16
|
+
export function buildFragmentPickerGroups(
|
|
17
|
+
pool: PromptFragment[],
|
|
18
|
+
isSelected: (id: string) => boolean,
|
|
19
|
+
onSelect: (id: string) => void,
|
|
20
|
+
): DropdownMenuItem[][] {
|
|
21
|
+
const groups = new Map<string, DropdownMenuItem[]>()
|
|
22
|
+
for (const f of pool) {
|
|
23
|
+
if (isSelected(f.id)) continue
|
|
24
|
+
const items = groups.get(f.category) ?? []
|
|
25
|
+
items.push({ label: f.title, onSelect: () => onSelect(f.id) })
|
|
26
|
+
groups.set(f.category, items)
|
|
27
|
+
}
|
|
28
|
+
return [...groups.entries()].map(([category, items]): DropdownMenuItem[] => [
|
|
29
|
+
{ type: 'label', label: category },
|
|
30
|
+
...items,
|
|
31
|
+
])
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.87.
|
|
3
|
+
"version": "0.87.5",
|
|
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",
|