@cat-factory/app 0.143.1 → 0.144.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 +22 -83
- package/app/components/fragments/FragmentSelector.vue +121 -0
- package/app/components/panels/inspector/ServiceFragments.vue +15 -91
- package/app/components/panels/inspector/TaskStructure.vue +13 -95
- package/app/components/prReview/PrReviewWindow.vue +87 -5
- package/app/stores/board.ts +4 -1
- package/i18n/locales/de.json +3 -1
- package/i18n/locales/en.json +3 -1
- package/i18n/locales/es.json +3 -1
- package/i18n/locales/fr.json +3 -1
- package/i18n/locales/he.json +3 -1
- package/i18n/locales/it.json +3 -1
- package/i18n/locales/ja.json +3 -1
- package/i18n/locales/pl.json +3 -1
- package/i18n/locales/tr.json +3 -1
- package/i18n/locales/uk.json +3 -1
- package/package.json +2 -2
|
@@ -21,12 +21,11 @@ import type {
|
|
|
21
21
|
TaskTypeFields,
|
|
22
22
|
} from '~/types/domain'
|
|
23
23
|
import { DOC_KINDS, DOC_KIND_FIELDS } from '~/types/domain'
|
|
24
|
-
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
25
24
|
import ContextDocumentPicker from '~/components/documents/ContextDocumentPicker.vue'
|
|
26
25
|
import ContextIssuePicker from '~/components/tasks/ContextIssuePicker.vue'
|
|
26
|
+
import FragmentSelector from '~/components/fragments/FragmentSelector.vue'
|
|
27
27
|
import { riskPolicyOptionLabel, riskPolicySummary } from '~/utils/riskPolicy'
|
|
28
28
|
import { pipelineAllowedForManualStart } from '~/utils/pipeline'
|
|
29
|
-
import { buildFragmentPickerGroups } from '~/utils/fragmentPicker'
|
|
30
29
|
|
|
31
30
|
const ui = useUiStore()
|
|
32
31
|
const board = useBoardStore()
|
|
@@ -37,7 +36,6 @@ const modelPresets = useModelPresetsStore()
|
|
|
37
36
|
const pipelines = usePipelinesStore()
|
|
38
37
|
const agentConfig = useAgentConfigStore()
|
|
39
38
|
const fragments = useFragmentsStore()
|
|
40
|
-
const accounts = useAccountsStore()
|
|
41
39
|
const toast = useToast()
|
|
42
40
|
const { t } = useI18n()
|
|
43
41
|
|
|
@@ -314,50 +312,10 @@ const selectedModelPresetLabel = computed(() => {
|
|
|
314
312
|
})
|
|
315
313
|
|
|
316
314
|
// ---- best-practice prompt fragments (pinned at creation) -------------------
|
|
317
|
-
// The
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
const
|
|
321
|
-
fragmentIds.value.map((id) => fragments.getFragment(id) ?? { id, title: id, summary: '' }),
|
|
322
|
-
)
|
|
323
|
-
// A trailing group linking out to the fragment library (board tier always; account tier when
|
|
324
|
-
// enabled) — managing fragments is open to every member, exactly like the inspector picker.
|
|
325
|
-
const fragmentManageItems = computed<DropdownMenuItem[]>(() => {
|
|
326
|
-
const items: DropdownMenuItem[] = [
|
|
327
|
-
{
|
|
328
|
-
label: t('inspector.fragments.manageBoard'),
|
|
329
|
-
icon: 'i-lucide-book-marked',
|
|
330
|
-
onSelect: () => ui.openFragmentLibrary(),
|
|
331
|
-
},
|
|
332
|
-
]
|
|
333
|
-
if (accounts.enabled) {
|
|
334
|
-
items.push({
|
|
335
|
-
label: t('inspector.fragments.manageAccount'),
|
|
336
|
-
icon: 'i-lucide-users',
|
|
337
|
-
onSelect: () => ui.openAccountSettings('fragments'),
|
|
338
|
-
})
|
|
339
|
-
}
|
|
340
|
-
return items
|
|
341
|
-
})
|
|
342
|
-
// Picker menu: fragments appropriate to the enclosing frame's block type (the "scope"), not
|
|
343
|
-
// already selected, grouped by category, with the management links appended as the final group.
|
|
344
|
-
// Falls back to `service` before a frame resolves so the catalog is still browsable.
|
|
345
|
-
const fragmentMenu = computed<DropdownMenuItem[][]>(() => {
|
|
346
|
-
const selected = new Set(fragmentIds.value)
|
|
347
|
-
return [
|
|
348
|
-
...buildFragmentPickerGroups(
|
|
349
|
-
fragments.forBlockType(frame.value?.type ?? 'service'),
|
|
350
|
-
(id) => selected.has(id),
|
|
351
|
-
(id) => {
|
|
352
|
-
if (!fragmentIds.value.includes(id)) fragmentIds.value = [...fragmentIds.value, id]
|
|
353
|
-
},
|
|
354
|
-
),
|
|
355
|
-
fragmentManageItems.value,
|
|
356
|
-
]
|
|
357
|
-
})
|
|
358
|
-
function removeFragment(id: string) {
|
|
359
|
-
fragmentIds.value = fragmentIds.value.filter((x) => x !== id)
|
|
360
|
-
}
|
|
315
|
+
// The pool the shared <FragmentSelector> offers: fragments appropriate to the enclosing frame's
|
|
316
|
+
// block type (the "scope"). Falls back to `service` before a frame resolves so the catalog is
|
|
317
|
+
// still browsable.
|
|
318
|
+
const fragmentPool = computed(() => fragments.forBlockType(frame.value?.type ?? 'service'))
|
|
361
319
|
|
|
362
320
|
// Hide UI-testing pipelines (`tester-ui` / `visual-confirmation`) when the target frame has no
|
|
363
321
|
// UI to exercise — they'd be refused server-side (see utils/pipeline + the backend gate). Also
|
|
@@ -544,7 +502,11 @@ watch(open, (isOpen) => {
|
|
|
544
502
|
docOutlineHints.value = ''
|
|
545
503
|
reviewPrRef.value = ''
|
|
546
504
|
reviewFocus.value = ''
|
|
547
|
-
|
|
505
|
+
// Pre-seed the best-practice fragments from the enclosing service's standards, so a new task
|
|
506
|
+
// ships with its service's fragments already selected (and freely add/removable here). The task
|
|
507
|
+
// OWNS this selection from creation — the engine folds exactly these, without re-unioning the
|
|
508
|
+
// service's set, so removing one here actually drops it for this task.
|
|
509
|
+
fragmentIds.value = [...(frame.value?.serviceFragmentIds ?? [])]
|
|
548
510
|
for (const key of Object.keys(docKindFieldValues) as DocKindFieldKey[])
|
|
549
511
|
delete docKindFieldValues[key]
|
|
550
512
|
riskPolicyId.value = ''
|
|
@@ -664,7 +626,10 @@ async function add() {
|
|
|
664
626
|
...(Object.keys(agentConfigValues.value).length
|
|
665
627
|
? { agentConfig: agentConfigValues.value }
|
|
666
628
|
: {}),
|
|
667
|
-
|
|
629
|
+
// Always send the (service-seeded, then user-edited) selection — including an empty list,
|
|
630
|
+
// which means "the user cleared the inherited picks" and must be honoured rather than
|
|
631
|
+
// re-seeded from the service. The task owns its fragments from here.
|
|
632
|
+
fragmentIds: [...fragmentIds.value],
|
|
668
633
|
...(technical.value ? { technical: true } : {}),
|
|
669
634
|
})
|
|
670
635
|
if (block) {
|
|
@@ -1058,41 +1023,15 @@ async function add() {
|
|
|
1058
1023
|
</div>
|
|
1059
1024
|
</div>
|
|
1060
1025
|
|
|
1061
|
-
<!-- Best-practice fragments pinned on the task at creation, scoped to the frame's type.
|
|
1026
|
+
<!-- Best-practice fragments pinned on the task at creation, scoped to the frame's type.
|
|
1027
|
+
Pre-seeded from the enclosing service's standards; the task owns them from here. -->
|
|
1062
1028
|
<div class="space-y-2">
|
|
1063
|
-
<
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
color="neutral"
|
|
1070
|
-
variant="soft"
|
|
1071
|
-
size="xs"
|
|
1072
|
-
icon="i-lucide-plus"
|
|
1073
|
-
trailing-icon="i-lucide-chevron-down"
|
|
1074
|
-
>
|
|
1075
|
-
{{ t('board.addTask.attach') }}
|
|
1076
|
-
</UButton>
|
|
1077
|
-
</UDropdownMenu>
|
|
1078
|
-
</div>
|
|
1079
|
-
<div v-if="selectedFragments.length" class="flex flex-wrap gap-1">
|
|
1080
|
-
<UBadge
|
|
1081
|
-
v-for="f in selectedFragments"
|
|
1082
|
-
:key="f.id"
|
|
1083
|
-
color="primary"
|
|
1084
|
-
variant="subtle"
|
|
1085
|
-
size="sm"
|
|
1086
|
-
class="cursor-pointer"
|
|
1087
|
-
:title="f.summary"
|
|
1088
|
-
@click="removeFragment(f.id)"
|
|
1089
|
-
>
|
|
1090
|
-
{{ f.title }}<UIcon name="i-lucide-x" class="ms-0.5 h-3 w-3" />
|
|
1091
|
-
</UBadge>
|
|
1092
|
-
</div>
|
|
1093
|
-
<p v-else class="text-[11px] text-slate-500">
|
|
1094
|
-
{{ t('board.addTask.bestPracticesHint') }}
|
|
1095
|
-
</p>
|
|
1029
|
+
<FragmentSelector
|
|
1030
|
+
v-model="fragmentIds"
|
|
1031
|
+
:pool="fragmentPool"
|
|
1032
|
+
:label="t('board.addTask.bestPractices')"
|
|
1033
|
+
:empty-text="t('board.addTask.bestPracticesHint')"
|
|
1034
|
+
/>
|
|
1096
1035
|
</div>
|
|
1097
1036
|
|
|
1098
1037
|
<!-- Context documents (ungated; Attach disabled until a source is connected). -->
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Shared best-practice prompt-fragment picker: a category-grouped "add" dropdown (with links out
|
|
3
|
+
// to the fragment library) plus the currently-selected fragments as removable badges.
|
|
4
|
+
// Presentational + `v-model`-driven — the caller owns WHERE the selection lives (a task's
|
|
5
|
+
// `fragmentIds`, a service's `serviceFragmentIds`, or a not-yet-created task's local draft) and
|
|
6
|
+
// binds the id list. Authored once and reused by the create-task form and the task/service
|
|
7
|
+
// inspectors so the three pickers can't drift. Ids the catalog no longer resolves still render
|
|
8
|
+
// (labelled by their raw id) so they stay visible and removable.
|
|
9
|
+
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
10
|
+
import type { PromptFragment } from '~/types/domain'
|
|
11
|
+
import { buildFragmentPickerGroups } from '~/utils/fragmentPicker'
|
|
12
|
+
|
|
13
|
+
const props = withDefaults(
|
|
14
|
+
defineProps<{
|
|
15
|
+
/** The selected fragment ids (`v-model`). */
|
|
16
|
+
modelValue: string[]
|
|
17
|
+
/** Candidate pool to offer — e.g. `fragments.forBlockType(type)` or the whole catalog. */
|
|
18
|
+
pool: PromptFragment[]
|
|
19
|
+
/** Optional label rendered to the left of the add button (omit when a section supplies it). */
|
|
20
|
+
label?: string
|
|
21
|
+
/** Optional text shown when nothing is selected. */
|
|
22
|
+
emptyText?: string
|
|
23
|
+
}>(),
|
|
24
|
+
{ label: '', emptyText: '' },
|
|
25
|
+
)
|
|
26
|
+
const emit = defineEmits<{ 'update:modelValue': [string[]] }>()
|
|
27
|
+
|
|
28
|
+
const fragments = useFragmentsStore()
|
|
29
|
+
const ui = useUiStore()
|
|
30
|
+
const accounts = useAccountsStore()
|
|
31
|
+
const { t } = useI18n()
|
|
32
|
+
|
|
33
|
+
// The catalog is per-board and invalidated on a workspace switch; (re)load it lazily — a no-op
|
|
34
|
+
// while current.
|
|
35
|
+
onMounted(() => fragments.ensureLoaded())
|
|
36
|
+
|
|
37
|
+
const selectedFragments = computed(() =>
|
|
38
|
+
props.modelValue.map((id) => fragments.getFragment(id) ?? { id, title: id, summary: '' }),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
// A trailing group that jumps from "attach a fragment" to authoring/editing the library itself
|
|
42
|
+
// (board tier always; account tier when accounts are enabled). Open to every member.
|
|
43
|
+
const manageItems = computed<DropdownMenuItem[]>(() => {
|
|
44
|
+
const items: DropdownMenuItem[] = [
|
|
45
|
+
{
|
|
46
|
+
label: t('inspector.fragments.manageBoard'),
|
|
47
|
+
icon: 'i-lucide-book-marked',
|
|
48
|
+
onSelect: () => ui.openFragmentLibrary(),
|
|
49
|
+
},
|
|
50
|
+
]
|
|
51
|
+
if (accounts.enabled) {
|
|
52
|
+
items.push({
|
|
53
|
+
label: t('inspector.fragments.manageAccount'),
|
|
54
|
+
icon: 'i-lucide-users',
|
|
55
|
+
onSelect: () => ui.openAccountSettings('fragments'),
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
return items
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Picker menu: pool fragments not already selected, grouped into labelled per-category sections,
|
|
62
|
+
// with the management links appended as the final group.
|
|
63
|
+
const fragmentMenu = computed<DropdownMenuItem[][]>(() => {
|
|
64
|
+
const selected = new Set(props.modelValue)
|
|
65
|
+
return [
|
|
66
|
+
...buildFragmentPickerGroups(props.pool, (id) => selected.has(id), addFragment),
|
|
67
|
+
manageItems.value,
|
|
68
|
+
]
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
function addFragment(id: string) {
|
|
72
|
+
if (props.modelValue.includes(id)) return
|
|
73
|
+
emit('update:modelValue', [...props.modelValue, id])
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function removeFragment(id: string) {
|
|
77
|
+
emit(
|
|
78
|
+
'update:modelValue',
|
|
79
|
+
props.modelValue.filter((x) => x !== id),
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<template>
|
|
85
|
+
<div>
|
|
86
|
+
<div class="mb-1 flex items-center justify-between gap-2">
|
|
87
|
+
<span v-if="label" class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
88
|
+
{{ label }}
|
|
89
|
+
</span>
|
|
90
|
+
<span v-else />
|
|
91
|
+
<UDropdownMenu :items="fragmentMenu">
|
|
92
|
+
<UButton
|
|
93
|
+
size="xs"
|
|
94
|
+
variant="ghost"
|
|
95
|
+
color="neutral"
|
|
96
|
+
icon="i-lucide-plus"
|
|
97
|
+
trailing-icon="i-lucide-chevron-down"
|
|
98
|
+
data-testid="fragment-add"
|
|
99
|
+
/>
|
|
100
|
+
</UDropdownMenu>
|
|
101
|
+
</div>
|
|
102
|
+
<div v-if="selectedFragments.length" class="flex flex-wrap gap-1">
|
|
103
|
+
<UBadge
|
|
104
|
+
v-for="f in selectedFragments"
|
|
105
|
+
:key="f.id"
|
|
106
|
+
color="primary"
|
|
107
|
+
variant="subtle"
|
|
108
|
+
size="sm"
|
|
109
|
+
class="cursor-pointer"
|
|
110
|
+
:title="f.summary"
|
|
111
|
+
data-testid="fragment-badge"
|
|
112
|
+
@click="removeFragment(f.id)"
|
|
113
|
+
>
|
|
114
|
+
{{ f.title }}<UIcon name="i-lucide-x" class="ms-0.5 h-3 w-3" />
|
|
115
|
+
</UBadge>
|
|
116
|
+
</div>
|
|
117
|
+
<div v-else-if="emptyText" class="text-[11px] text-slate-500">
|
|
118
|
+
{{ emptyText }}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</template>
|
|
@@ -1,76 +1,22 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
3
2
|
import type { Block } from '~/types/domain'
|
|
4
3
|
import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
|
|
5
|
-
import
|
|
4
|
+
import FragmentSelector from '~/components/fragments/FragmentSelector.vue'
|
|
6
5
|
|
|
7
6
|
// Service-level best-practice fragments (frame blocks). These are the programming
|
|
8
|
-
// standards/guidelines for the whole service
|
|
9
|
-
// into the
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// (the add-service modal); the inspector leaves it collapsed.
|
|
7
|
+
// standards/guidelines for the whole service: they SEED a new task's own selection at creation,
|
|
8
|
+
// and at run time their bodies fold into the frame's own `code-aware` runs. Drawn from the
|
|
9
|
+
// board's merged fragment catalog (built-in ∪ registered ∪ account ∪ workspace). `defaultOpen`
|
|
10
|
+
// expands the section on surfaces that embed this as the primary content (the add-service
|
|
11
|
+
// modal); the inspector leaves it collapsed. The shared <FragmentSelector> renders the picker.
|
|
14
12
|
const props = defineProps<{ block: Block; defaultOpen?: boolean }>()
|
|
15
13
|
|
|
16
14
|
const board = useBoardStore()
|
|
17
15
|
const fragments = useFragmentsStore()
|
|
18
|
-
const ui = useUiStore()
|
|
19
|
-
const accounts = useAccountsStore()
|
|
20
16
|
const { t } = useI18n()
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// An id the catalog no longer resolves (removed/suppressed after selection) still
|
|
25
|
-
// renders — labelled by its raw id — so it stays visible and removable.
|
|
26
|
-
const selectedFragments = computed(() =>
|
|
27
|
-
(props.block.serviceFragmentIds ?? []).map(
|
|
28
|
-
(id) => fragments.getFragment(id) ?? { id, title: id, summary: '' },
|
|
29
|
-
),
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
// A trailing group that jumps from "attach a fragment" to authoring/editing the
|
|
33
|
-
// library itself (board tier always; account tier when accounts are enabled).
|
|
34
|
-
// Open to every member — managing fragments is not an admin-only action.
|
|
35
|
-
const manageItems = computed<DropdownMenuItem[]>(() => {
|
|
36
|
-
const items: DropdownMenuItem[] = [
|
|
37
|
-
{
|
|
38
|
-
label: t('inspector.fragments.manageBoard'),
|
|
39
|
-
icon: 'i-lucide-book-marked',
|
|
40
|
-
onSelect: () => ui.openFragmentLibrary(),
|
|
41
|
-
},
|
|
42
|
-
]
|
|
43
|
-
if (accounts.enabled) {
|
|
44
|
-
items.push({
|
|
45
|
-
label: t('inspector.fragments.manageAccount'),
|
|
46
|
-
icon: 'i-lucide-users',
|
|
47
|
-
onSelect: () => ui.openAccountSettings('fragments'),
|
|
48
|
-
})
|
|
49
|
-
}
|
|
50
|
-
return items
|
|
51
|
-
})
|
|
52
|
-
|
|
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
|
-
const selected = new Set(props.block.serviceFragmentIds ?? [])
|
|
57
|
-
return [
|
|
58
|
-
...buildFragmentPickerGroups(fragments.fragments, (id) => selected.has(id), addFragment),
|
|
59
|
-
manageItems.value,
|
|
60
|
-
]
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
function addFragment(id: string) {
|
|
64
|
-
const list = props.block.serviceFragmentIds ? [...props.block.serviceFragmentIds] : []
|
|
65
|
-
if (!list.includes(id)) list.push(id)
|
|
66
|
-
board.updateBlock(props.block.id, { serviceFragmentIds: list })
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function removeFragment(id: string) {
|
|
70
|
-
if (!props.block.serviceFragmentIds) return
|
|
71
|
-
board.updateBlock(props.block.id, {
|
|
72
|
-
serviceFragmentIds: props.block.serviceFragmentIds.filter((x) => x !== id),
|
|
73
|
-
})
|
|
18
|
+
function setFragments(ids: string[]) {
|
|
19
|
+
board.updateBlock(props.block.id, { serviceFragmentIds: ids })
|
|
74
20
|
}
|
|
75
21
|
</script>
|
|
76
22
|
|
|
@@ -78,36 +24,14 @@ function removeFragment(id: string) {
|
|
|
78
24
|
<InspectorSection
|
|
79
25
|
:title="t('inspector.fragments.serviceTitle')"
|
|
80
26
|
:hint="t('inspector.fragments.serviceHint')"
|
|
81
|
-
:count="
|
|
27
|
+
:count="(block.serviceFragmentIds ?? []).length"
|
|
82
28
|
:default-open="props.defaultOpen"
|
|
83
29
|
>
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
icon="i-lucide-plus"
|
|
91
|
-
trailing-icon="i-lucide-chevron-down"
|
|
92
|
-
/>
|
|
93
|
-
</UDropdownMenu>
|
|
94
|
-
</template>
|
|
95
|
-
<div v-if="selectedFragments.length" class="flex flex-wrap gap-1">
|
|
96
|
-
<UBadge
|
|
97
|
-
v-for="f in selectedFragments"
|
|
98
|
-
:key="f.id"
|
|
99
|
-
color="primary"
|
|
100
|
-
variant="subtle"
|
|
101
|
-
size="sm"
|
|
102
|
-
class="cursor-pointer"
|
|
103
|
-
:title="f.summary"
|
|
104
|
-
@click="removeFragment(f.id)"
|
|
105
|
-
>
|
|
106
|
-
{{ f.title }}<UIcon name="i-lucide-x" class="ms-0.5 h-3 w-3" />
|
|
107
|
-
</UBadge>
|
|
108
|
-
</div>
|
|
109
|
-
<div v-else class="text-[11px] text-slate-500">
|
|
110
|
-
{{ t('inspector.fragments.serviceEmpty') }}
|
|
111
|
-
</div>
|
|
30
|
+
<FragmentSelector
|
|
31
|
+
:model-value="block.serviceFragmentIds ?? []"
|
|
32
|
+
:pool="fragments.fragments"
|
|
33
|
+
:empty-text="t('inspector.fragments.serviceEmpty')"
|
|
34
|
+
@update:model-value="setFragments"
|
|
35
|
+
/>
|
|
112
36
|
</InspectorSection>
|
|
113
37
|
</template>
|
|
@@ -1,78 +1,20 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
3
2
|
import type { Block } from '~/types/domain'
|
|
4
3
|
import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
|
|
5
|
-
import
|
|
4
|
+
import FragmentSelector from '~/components/fragments/FragmentSelector.vue'
|
|
6
5
|
|
|
7
6
|
const props = defineProps<{ block: Block }>()
|
|
8
7
|
|
|
9
8
|
const board = useBoardStore()
|
|
10
9
|
const fragments = useFragmentsStore()
|
|
11
|
-
const ui = useUiStore()
|
|
12
|
-
const accounts = useAccountsStore()
|
|
13
10
|
const { t } = useI18n()
|
|
14
11
|
|
|
15
|
-
// The catalog is per-board and invalidated on a workspace switch, so (re)load it when the
|
|
16
|
-
// task inspector mounts — mirrors ServiceFragments; ensureLoaded is a no-op while current.
|
|
17
|
-
onMounted(() => fragments.ensureLoaded())
|
|
18
|
-
|
|
19
12
|
// ---- best-practice prompt fragments ----------------------------------------
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
(props.block.fragmentIds
|
|
25
|
-
(id) => fragments.getFragment(id) ?? { id, title: id, summary: '' },
|
|
26
|
-
),
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
// A trailing group that jumps from "attach a fragment" to authoring/editing the
|
|
30
|
-
// library itself (board tier always; account tier when accounts are enabled).
|
|
31
|
-
// Open to every member — managing fragments is not an admin-only action.
|
|
32
|
-
const manageItems = computed<DropdownMenuItem[]>(() => {
|
|
33
|
-
const items: DropdownMenuItem[] = [
|
|
34
|
-
{
|
|
35
|
-
label: t('inspector.fragments.manageBoard'),
|
|
36
|
-
icon: 'i-lucide-book-marked',
|
|
37
|
-
onSelect: () => ui.openFragmentLibrary(),
|
|
38
|
-
},
|
|
39
|
-
]
|
|
40
|
-
if (accounts.enabled) {
|
|
41
|
-
items.push({
|
|
42
|
-
label: t('inspector.fragments.manageAccount'),
|
|
43
|
-
icon: 'i-lucide-users',
|
|
44
|
-
onSelect: () => ui.openAccountSettings('fragments'),
|
|
45
|
-
})
|
|
46
|
-
}
|
|
47
|
-
return items
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
// Picker menu: fragments suitable for this block's type, not already selected,
|
|
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
|
-
const selected = new Set(props.block.fragmentIds ?? [])
|
|
55
|
-
return [
|
|
56
|
-
...buildFragmentPickerGroups(
|
|
57
|
-
fragments.forBlockType(props.block.type),
|
|
58
|
-
(id) => selected.has(id),
|
|
59
|
-
addFragment,
|
|
60
|
-
),
|
|
61
|
-
manageItems.value,
|
|
62
|
-
]
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
function addFragment(id: string) {
|
|
66
|
-
const list = props.block.fragmentIds ? [...props.block.fragmentIds] : []
|
|
67
|
-
if (!list.includes(id)) list.push(id)
|
|
68
|
-
board.updateBlock(props.block.id, { fragmentIds: list })
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function removeFragment(id: string) {
|
|
72
|
-
if (!props.block.fragmentIds) return
|
|
73
|
-
board.updateBlock(props.block.id, {
|
|
74
|
-
fragmentIds: props.block.fragmentIds.filter((x) => x !== id),
|
|
75
|
-
})
|
|
13
|
+
// The task's OWN selection (seeded from its service at creation, then editable per task). The
|
|
14
|
+
// shared <FragmentSelector> renders the picker; a change persists via updateBlock.
|
|
15
|
+
const fragmentPool = computed(() => fragments.forBlockType(props.block.type))
|
|
16
|
+
function setFragments(ids: string[]) {
|
|
17
|
+
board.updateBlock(props.block.id, { fragmentIds: ids })
|
|
76
18
|
}
|
|
77
19
|
</script>
|
|
78
20
|
|
|
@@ -97,37 +39,13 @@ function removeFragment(id: string) {
|
|
|
97
39
|
|
|
98
40
|
<!-- best practices (prompt fragments) -->
|
|
99
41
|
<div>
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
variant="ghost"
|
|
108
|
-
color="neutral"
|
|
109
|
-
icon="i-lucide-plus"
|
|
110
|
-
trailing-icon="i-lucide-chevron-down"
|
|
111
|
-
/>
|
|
112
|
-
</UDropdownMenu>
|
|
113
|
-
</div>
|
|
114
|
-
<div v-if="selectedFragments.length" class="mb-1 flex flex-wrap gap-1">
|
|
115
|
-
<UBadge
|
|
116
|
-
v-for="f in selectedFragments"
|
|
117
|
-
:key="f.id"
|
|
118
|
-
color="primary"
|
|
119
|
-
variant="subtle"
|
|
120
|
-
size="sm"
|
|
121
|
-
class="cursor-pointer"
|
|
122
|
-
:title="f.summary"
|
|
123
|
-
@click="removeFragment(f.id)"
|
|
124
|
-
>
|
|
125
|
-
{{ f.title }}<UIcon name="i-lucide-x" class="ms-0.5 h-3 w-3" />
|
|
126
|
-
</UBadge>
|
|
127
|
-
</div>
|
|
128
|
-
<div v-else class="text-[11px] text-slate-500">
|
|
129
|
-
{{ t('inspector.structure.bestPracticesEmpty') }}
|
|
130
|
-
</div>
|
|
42
|
+
<FragmentSelector
|
|
43
|
+
:model-value="block.fragmentIds ?? []"
|
|
44
|
+
:pool="fragmentPool"
|
|
45
|
+
:label="t('inspector.structure.bestPractices')"
|
|
46
|
+
:empty-text="t('inspector.structure.bestPracticesEmpty')"
|
|
47
|
+
@update:model-value="setFragments"
|
|
48
|
+
/>
|
|
131
49
|
<p class="mt-1 text-[11px] leading-snug text-slate-500">
|
|
132
50
|
{{ t('inspector.structure.bestPracticesHint') }}
|
|
133
51
|
</p>
|
|
@@ -17,7 +17,9 @@ import type {
|
|
|
17
17
|
PrReviewResolution,
|
|
18
18
|
PrReviewSeverity,
|
|
19
19
|
PrReviewStepState,
|
|
20
|
+
StepSubtasks,
|
|
20
21
|
} from '~/types/execution'
|
|
22
|
+
import { subtaskIconClass } from '~/utils/pipelineRender'
|
|
21
23
|
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
22
24
|
|
|
23
25
|
const execution = useExecutionStore()
|
|
@@ -44,6 +46,18 @@ const step = computed(() => {
|
|
|
44
46
|
const state = computed<PrReviewStepState | null>(() => step.value?.prReview ?? null)
|
|
45
47
|
const status = computed(() => state.value?.status ?? null)
|
|
46
48
|
const awaiting = computed(() => status.value === 'awaiting_selection')
|
|
49
|
+
// The reviewer's live todo list while it works, streamed onto the step. Its entries are the
|
|
50
|
+
// cohesive slices/chunks the agent grouped the diff into (plus a final "aggregate" step), so it
|
|
51
|
+
// surfaces slices-reviewed-so-far progress during the `reviewing` phase — richer than a spinner.
|
|
52
|
+
const subtasks = computed<StepSubtasks | null>(() => step.value?.subtasks ?? null)
|
|
53
|
+
const hasProgress = computed(() => (subtasks.value?.total ?? 0) > 0)
|
|
54
|
+
|
|
55
|
+
/** Icon per todo-item status (matches the pipeline timeline's live subtask breakdown). */
|
|
56
|
+
const ITEM_ICON: Record<string, string> = {
|
|
57
|
+
completed: 'i-lucide-check-circle-2',
|
|
58
|
+
in_progress: 'i-lucide-loader-circle',
|
|
59
|
+
pending: 'i-lucide-circle',
|
|
60
|
+
}
|
|
47
61
|
// A resolution is executing (the Fixer is committing, or comments are being posted) — show a
|
|
48
62
|
// working state between the human's choice and the run advancing/the stream echoing `done`.
|
|
49
63
|
const working = computed(() => status.value === 'fixing' || status.value === 'posting')
|
|
@@ -145,14 +159,82 @@ async function onResolve(action: PrReviewResolution): Promise<void> {
|
|
|
145
159
|
</template>
|
|
146
160
|
|
|
147
161
|
<div class="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
|
148
|
-
<!-- Reviewing: the read-only reviewer is still working.
|
|
162
|
+
<!-- Reviewing: the read-only reviewer is still working. Once it starts maintaining its
|
|
163
|
+
per-slice todo list, surface the live chunk progress (slices reviewed / total + the
|
|
164
|
+
breakdown) instead of a bare spinner. -->
|
|
149
165
|
<div
|
|
150
166
|
v-if="status === 'reviewing'"
|
|
151
|
-
|
|
167
|
+
data-testid="pr-review-reviewing"
|
|
168
|
+
class="flex h-full flex-col"
|
|
152
169
|
>
|
|
153
|
-
|
|
154
|
-
<
|
|
155
|
-
|
|
170
|
+
<!-- Live chunk progress once the reviewer has planned its slices. -->
|
|
171
|
+
<div v-if="hasProgress" class="py-2">
|
|
172
|
+
<div class="mb-1 flex items-center gap-2 text-sm text-slate-200">
|
|
173
|
+
<UIcon
|
|
174
|
+
name="i-lucide-loader-circle"
|
|
175
|
+
class="h-4 w-4 shrink-0 animate-spin text-indigo-300"
|
|
176
|
+
/>
|
|
177
|
+
<span>{{ t('prReview.reviewing.title') }}</span>
|
|
178
|
+
</div>
|
|
179
|
+
<p class="mb-3 text-[11px] text-slate-500">{{ t('prReview.reviewing.hint') }}</p>
|
|
180
|
+
|
|
181
|
+
<div class="flex items-center justify-between text-[11px] text-slate-400">
|
|
182
|
+
<span data-testid="pr-review-chunk-count">
|
|
183
|
+
{{
|
|
184
|
+
t('prReview.reviewing.chunks', {
|
|
185
|
+
completed: subtasks!.completed,
|
|
186
|
+
total: subtasks!.total,
|
|
187
|
+
})
|
|
188
|
+
}}
|
|
189
|
+
</span>
|
|
190
|
+
<span v-if="subtasks!.inProgress > 0" class="text-indigo-300">
|
|
191
|
+
{{ t('prReview.reviewing.inProgress', { count: subtasks!.inProgress }) }}
|
|
192
|
+
</span>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="mt-1 h-1.5 overflow-hidden rounded-full bg-slate-700/60">
|
|
195
|
+
<div
|
|
196
|
+
class="h-full rounded-full bg-indigo-400 transition-all duration-500"
|
|
197
|
+
:style="{ width: `${(subtasks!.completed / subtasks!.total) * 100}%` }"
|
|
198
|
+
/>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<!-- The slice/todo breakdown the agent is working through. -->
|
|
202
|
+
<ul
|
|
203
|
+
v-if="subtasks!.items?.length"
|
|
204
|
+
class="mt-3 space-y-1.5"
|
|
205
|
+
data-testid="pr-review-chunks"
|
|
206
|
+
>
|
|
207
|
+
<li
|
|
208
|
+
v-for="(item, i) in subtasks!.items"
|
|
209
|
+
:key="i"
|
|
210
|
+
class="flex items-start gap-1.5 text-[12px]"
|
|
211
|
+
:class="
|
|
212
|
+
item.status === 'completed'
|
|
213
|
+
? 'text-slate-500 line-through'
|
|
214
|
+
: item.status === 'in_progress'
|
|
215
|
+
? 'text-slate-100'
|
|
216
|
+
: 'text-slate-400'
|
|
217
|
+
"
|
|
218
|
+
>
|
|
219
|
+
<UIcon
|
|
220
|
+
:name="ITEM_ICON[item.status]"
|
|
221
|
+
class="mt-0.5 h-3.5 w-3.5 shrink-0"
|
|
222
|
+
:class="subtaskIconClass(item.status, false)"
|
|
223
|
+
/>
|
|
224
|
+
<span>{{ item.label }}</span>
|
|
225
|
+
</li>
|
|
226
|
+
</ul>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<!-- Before the reviewer has planned its slices: the cold-start spinner. -->
|
|
230
|
+
<div
|
|
231
|
+
v-else
|
|
232
|
+
class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
|
|
233
|
+
>
|
|
234
|
+
<UIcon name="i-lucide-loader-circle" class="h-8 w-8 animate-spin opacity-60" />
|
|
235
|
+
<p class="text-sm">{{ t('prReview.reviewing.title') }}</p>
|
|
236
|
+
<p class="max-w-sm text-[11px] text-slate-500">{{ t('prReview.reviewing.hint') }}</p>
|
|
237
|
+
</div>
|
|
156
238
|
</div>
|
|
157
239
|
|
|
158
240
|
<!-- A resolution is executing: the Fixer is committing / comments are being posted. -->
|
package/app/stores/board.ts
CHANGED
|
@@ -241,7 +241,10 @@ export const useBoardStore = defineStore('board', () => {
|
|
|
241
241
|
...(options?.modelPresetId ? { modelPresetId: options.modelPresetId } : {}),
|
|
242
242
|
...(options?.pipelineId ? { pipelineId: options.pipelineId } : {}),
|
|
243
243
|
...(options?.agentConfig ? { agentConfig: options.agentConfig } : {}),
|
|
244
|
-
|
|
244
|
+
// Forward the selection when the caller provides one (the create form always does, even
|
|
245
|
+
// when empty — an explicit clear the backend must honour rather than re-seed); omit only
|
|
246
|
+
// when a caller doesn't manage fragments at all (then the backend seeds from the service).
|
|
247
|
+
...(options?.fragmentIds !== undefined ? { fragmentIds: options.fragmentIds } : {}),
|
|
245
248
|
...(options?.technical ? { technical: true } : {}),
|
|
246
249
|
})
|
|
247
250
|
upsert(block)
|
package/i18n/locales/de.json
CHANGED
|
@@ -4936,7 +4936,9 @@
|
|
|
4936
4936
|
"line": "Zeile {line}",
|
|
4937
4937
|
"reviewing": {
|
|
4938
4938
|
"title": "Pull Request wird geprüft…",
|
|
4939
|
-
"hint": "Der Diff wird in zusammenhängende Teile zerlegt und einzeln geprüft."
|
|
4939
|
+
"hint": "Der Diff wird in zusammenhängende Teile zerlegt und einzeln geprüft.",
|
|
4940
|
+
"chunks": "{completed} von {total} Abschnitten geprüft",
|
|
4941
|
+
"inProgress": "{count} in Bearbeitung"
|
|
4940
4942
|
},
|
|
4941
4943
|
"fixing": {
|
|
4942
4944
|
"title": "Ausgewählte Befunde werden behoben…",
|
package/i18n/locales/en.json
CHANGED
|
@@ -5065,7 +5065,9 @@
|
|
|
5065
5065
|
"line": "line {line}",
|
|
5066
5066
|
"reviewing": {
|
|
5067
5067
|
"title": "Reviewing the pull request…",
|
|
5068
|
-
"hint": "Slicing the diff into cohesive chunks and reviewing each one."
|
|
5068
|
+
"hint": "Slicing the diff into cohesive chunks and reviewing each one.",
|
|
5069
|
+
"chunks": "{completed} of {total} chunks reviewed",
|
|
5070
|
+
"inProgress": "{count} in progress"
|
|
5069
5071
|
},
|
|
5070
5072
|
"fixing": {
|
|
5071
5073
|
"title": "Fixing the selected findings…",
|
package/i18n/locales/es.json
CHANGED
|
@@ -4924,7 +4924,9 @@
|
|
|
4924
4924
|
"line": "línea {line}",
|
|
4925
4925
|
"reviewing": {
|
|
4926
4926
|
"title": "Revisando el pull request…",
|
|
4927
|
-
"hint": "Dividiendo el diff en bloques coherentes y revisando cada uno."
|
|
4927
|
+
"hint": "Dividiendo el diff en bloques coherentes y revisando cada uno.",
|
|
4928
|
+
"chunks": "{completed} de {total} fragmentos revisados",
|
|
4929
|
+
"inProgress": "{count} en curso"
|
|
4928
4930
|
},
|
|
4929
4931
|
"fixing": {
|
|
4930
4932
|
"title": "Corrigiendo los hallazgos seleccionados…",
|
package/i18n/locales/fr.json
CHANGED
|
@@ -4924,7 +4924,9 @@
|
|
|
4924
4924
|
"line": "ligne {line}",
|
|
4925
4925
|
"reviewing": {
|
|
4926
4926
|
"title": "Revue de la pull request…",
|
|
4927
|
-
"hint": "Découpage du diff en blocs cohérents et revue de chacun."
|
|
4927
|
+
"hint": "Découpage du diff en blocs cohérents et revue de chacun.",
|
|
4928
|
+
"chunks": "{completed} sur {total} sections examinées",
|
|
4929
|
+
"inProgress": "{count} en cours"
|
|
4928
4930
|
},
|
|
4929
4931
|
"fixing": {
|
|
4930
4932
|
"title": "Correction des points sélectionnés…",
|
package/i18n/locales/he.json
CHANGED
|
@@ -4935,7 +4935,9 @@
|
|
|
4935
4935
|
"line": "שורה {line}",
|
|
4936
4936
|
"reviewing": {
|
|
4937
4937
|
"title": "בודק את בקשת המשיכה…",
|
|
4938
|
-
"hint": "מחלק את ההבדלים לקטעים לכידים ובודק כל אחד."
|
|
4938
|
+
"hint": "מחלק את ההבדלים לקטעים לכידים ובודק כל אחד.",
|
|
4939
|
+
"chunks": "{completed} מתוך {total} מקטעים נבדקו",
|
|
4940
|
+
"inProgress": "{count} בתהליך"
|
|
4939
4941
|
},
|
|
4940
4942
|
"fixing": {
|
|
4941
4943
|
"title": "מתקן את הממצאים שנבחרו…",
|
package/i18n/locales/it.json
CHANGED
|
@@ -4936,7 +4936,9 @@
|
|
|
4936
4936
|
"line": "riga {line}",
|
|
4937
4937
|
"reviewing": {
|
|
4938
4938
|
"title": "Revisione della pull request…",
|
|
4939
|
-
"hint": "Suddivisione del diff in blocchi coerenti e revisione di ciascuno."
|
|
4939
|
+
"hint": "Suddivisione del diff in blocchi coerenti e revisione di ciascuno.",
|
|
4940
|
+
"chunks": "{completed} di {total} sezioni esaminate",
|
|
4941
|
+
"inProgress": "{count} in corso"
|
|
4940
4942
|
},
|
|
4941
4943
|
"fixing": {
|
|
4942
4944
|
"title": "Correzione dei rilievi selezionati…",
|
package/i18n/locales/ja.json
CHANGED
|
@@ -4936,7 +4936,9 @@
|
|
|
4936
4936
|
"line": "{line}行目",
|
|
4937
4937
|
"reviewing": {
|
|
4938
4938
|
"title": "プルリクエストをレビュー中…",
|
|
4939
|
-
"hint": "差分をまとまりのある単位に分割し、各単位をレビューしています。"
|
|
4939
|
+
"hint": "差分をまとまりのある単位に分割し、各単位をレビューしています。",
|
|
4940
|
+
"chunks": "{total} 個中 {completed} 個のチャンクをレビュー済み",
|
|
4941
|
+
"inProgress": "{count} 件処理中"
|
|
4940
4942
|
},
|
|
4941
4943
|
"fixing": {
|
|
4942
4944
|
"title": "選択した指摘を修正中…",
|
package/i18n/locales/pl.json
CHANGED
|
@@ -4924,7 +4924,9 @@
|
|
|
4924
4924
|
"line": "wiersz {line}",
|
|
4925
4925
|
"reviewing": {
|
|
4926
4926
|
"title": "Przeglądanie pull requesta…",
|
|
4927
|
-
"hint": "Dzielenie zmian na spójne części i przeglądanie każdej z nich."
|
|
4927
|
+
"hint": "Dzielenie zmian na spójne części i przeglądanie każdej z nich.",
|
|
4928
|
+
"chunks": "Sprawdzono {completed} z {total} fragmentów",
|
|
4929
|
+
"inProgress": "{count} w toku"
|
|
4928
4930
|
},
|
|
4929
4931
|
"fixing": {
|
|
4930
4932
|
"title": "Naprawianie wybranych uwag…",
|
package/i18n/locales/tr.json
CHANGED
|
@@ -4936,7 +4936,9 @@
|
|
|
4936
4936
|
"line": "satır {line}",
|
|
4937
4937
|
"reviewing": {
|
|
4938
4938
|
"title": "Pull request inceleniyor…",
|
|
4939
|
-
"hint": "Fark tutarlı parçalara bölünüp her biri inceleniyor."
|
|
4939
|
+
"hint": "Fark tutarlı parçalara bölünüp her biri inceleniyor.",
|
|
4940
|
+
"chunks": "{total} bölümden {completed} tanesi incelendi",
|
|
4941
|
+
"inProgress": "{count} devam ediyor"
|
|
4940
4942
|
},
|
|
4941
4943
|
"fixing": {
|
|
4942
4944
|
"title": "Seçili bulgular düzeltiliyor…",
|
package/i18n/locales/uk.json
CHANGED
|
@@ -4924,7 +4924,9 @@
|
|
|
4924
4924
|
"line": "рядок {line}",
|
|
4925
4925
|
"reviewing": {
|
|
4926
4926
|
"title": "Перевірка pull request…",
|
|
4927
|
-
"hint": "Поділ змін на цілісні частини та огляд кожної з них."
|
|
4927
|
+
"hint": "Поділ змін на цілісні частини та огляд кожної з них.",
|
|
4928
|
+
"chunks": "Перевірено {completed} з {total} фрагментів",
|
|
4929
|
+
"inProgress": "{count} у процесі"
|
|
4928
4930
|
},
|
|
4929
4931
|
"fixing": {
|
|
4930
4932
|
"title": "Виправлення вибраних зауважень…",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.144.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.152.
|
|
43
|
+
"@cat-factory/contracts": "0.152.1"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@toad-contracts/testing": "0.3.2",
|