@cat-factory/app 0.148.1 → 0.150.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.
- package/app/components/fragments/FragmentLibraryManager.vue +188 -18
- package/app/components/panels/AgentStepDetail.vue +11 -0
- package/app/components/panels/StepEffortReport.vue +86 -0
- package/app/components/panels/StepFragmentAdherence.vue +85 -0
- package/app/components/prReview/PrReviewWindow.vue +8 -0
- package/app/components/providers/ApiKeysSection.vue +47 -7
- package/app/components/providers/VendorCredentialsModal.vue +56 -7
- package/app/composables/api/fragments.ts +17 -0
- package/app/composables/api/models.ts +22 -0
- package/app/stores/apiKeys.ts +30 -1
- package/app/stores/fragmentLibrary.ts +16 -0
- package/app/stores/vendorCredentials.ts +32 -2
- package/app/types/execution.ts +3 -0
- package/app/types/fragments.ts +2 -0
- package/app/types/models.ts +2 -0
- package/i18n/locales/de.json +37 -8
- package/i18n/locales/en.json +37 -8
- package/i18n/locales/es.json +37 -8
- package/i18n/locales/fr.json +37 -8
- package/i18n/locales/he.json +37 -8
- package/i18n/locales/it.json +37 -8
- package/i18n/locales/ja.json +37 -8
- package/i18n/locales/pl.json +37 -8
- package/i18n/locales/tr.json +37 -8
- package/i18n/locales/uk.json +37 -8
- package/package.json +2 -2
|
@@ -135,6 +135,80 @@ const draftValid = computed(
|
|
|
135
135
|
() => draft.value.title.trim() && draft.value.summary.trim() && draft.value.body.trim(),
|
|
136
136
|
)
|
|
137
137
|
|
|
138
|
+
// ---- auto-generate a title from the fragment's content (inline LLM call) ---
|
|
139
|
+
// Shared by the create form and the inline editor; keyed so only the button that triggered it
|
|
140
|
+
// spins. Generation needs a body (the title is derived from it); the summary rides along.
|
|
141
|
+
const generatingTitleFor = ref<string | null>(null)
|
|
142
|
+
async function autofillTitle(
|
|
143
|
+
key: string,
|
|
144
|
+
get: () => { body: string; summary?: string },
|
|
145
|
+
set: (title: string) => void,
|
|
146
|
+
) {
|
|
147
|
+
const { body, summary } = get()
|
|
148
|
+
if (!body.trim() || generatingTitleFor.value) return
|
|
149
|
+
generatingTitleFor.value = key
|
|
150
|
+
try {
|
|
151
|
+
const title = await library.generateTitle({
|
|
152
|
+
body: body.trim(),
|
|
153
|
+
summary: summary?.trim() || undefined,
|
|
154
|
+
})
|
|
155
|
+
set(title)
|
|
156
|
+
} catch (e) {
|
|
157
|
+
notifyError(t('fragments.authored.titleGenFailed'), e)
|
|
158
|
+
} finally {
|
|
159
|
+
generatingTitleFor.value = null
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---- edit an existing hand-authored fragment (title / summary / body / tags) ---
|
|
164
|
+
const editDraft = ref<{
|
|
165
|
+
id: string
|
|
166
|
+
title: string
|
|
167
|
+
summary: string
|
|
168
|
+
body: string
|
|
169
|
+
tags: string
|
|
170
|
+
} | null>(null)
|
|
171
|
+
function startEdit(f: (typeof library.fragments)[number]) {
|
|
172
|
+
editDraft.value = {
|
|
173
|
+
id: f.id,
|
|
174
|
+
title: f.title,
|
|
175
|
+
summary: f.summary,
|
|
176
|
+
body: f.body,
|
|
177
|
+
tags: (f.tags ?? []).join(', '),
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function cancelEdit() {
|
|
181
|
+
editDraft.value = null
|
|
182
|
+
}
|
|
183
|
+
const editValid = computed(
|
|
184
|
+
() =>
|
|
185
|
+
!!editDraft.value &&
|
|
186
|
+
!!editDraft.value.title.trim() &&
|
|
187
|
+
!!editDraft.value.summary.trim() &&
|
|
188
|
+
!!editDraft.value.body.trim(),
|
|
189
|
+
)
|
|
190
|
+
async function saveEdit() {
|
|
191
|
+
const d = editDraft.value
|
|
192
|
+
if (!d || !editValid.value) return
|
|
193
|
+
await withRow(`edit:${d.id}`, async () => {
|
|
194
|
+
try {
|
|
195
|
+
await library.update(d.id, {
|
|
196
|
+
title: d.title.trim(),
|
|
197
|
+
summary: d.summary.trim(),
|
|
198
|
+
body: d.body.trim(),
|
|
199
|
+
tags: d.tags
|
|
200
|
+
.split(',')
|
|
201
|
+
.map((t) => t.trim())
|
|
202
|
+
.filter(Boolean),
|
|
203
|
+
})
|
|
204
|
+
editDraft.value = null
|
|
205
|
+
toast.add({ title: t('fragments.toast.updated'), icon: 'i-lucide-check' })
|
|
206
|
+
} catch (e) {
|
|
207
|
+
notifyError(t('fragments.toast.updateFailed'), e)
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
138
212
|
async function createFragment() {
|
|
139
213
|
if (!draftValid.value) return
|
|
140
214
|
creating.value = true
|
|
@@ -522,26 +596,96 @@ async function unlinkSource(id: string) {
|
|
|
522
596
|
<div
|
|
523
597
|
v-for="f in library.fragments"
|
|
524
598
|
:key="f.id"
|
|
525
|
-
class="
|
|
599
|
+
class="rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
526
600
|
>
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
601
|
+
<!-- Inline editor (hand-authored fragments): title / summary / body / tags, with the
|
|
602
|
+
same auto-generate-title button as the create form. -->
|
|
603
|
+
<div v-if="editDraft && editDraft.id === f.id" class="flex flex-col gap-2">
|
|
604
|
+
<div class="flex gap-2">
|
|
605
|
+
<UInput
|
|
606
|
+
v-model="editDraft.title"
|
|
607
|
+
:placeholder="t('fragments.authored.titlePlaceholder')"
|
|
608
|
+
class="flex-1"
|
|
609
|
+
/>
|
|
610
|
+
<UButton
|
|
611
|
+
icon="i-lucide-wand-2"
|
|
612
|
+
size="sm"
|
|
613
|
+
variant="outline"
|
|
614
|
+
:disabled="!editDraft.body.trim()"
|
|
615
|
+
:loading="generatingTitleFor === `edit:${f.id}`"
|
|
616
|
+
:title="t('fragments.authored.generateTitleHint')"
|
|
617
|
+
@click="
|
|
618
|
+
autofillTitle(
|
|
619
|
+
`edit:${f.id}`,
|
|
620
|
+
() => ({ body: editDraft!.body, summary: editDraft!.summary }),
|
|
621
|
+
(title) => {
|
|
622
|
+
if (editDraft) editDraft.title = title
|
|
623
|
+
},
|
|
624
|
+
)
|
|
625
|
+
"
|
|
626
|
+
>
|
|
627
|
+
{{ t('fragments.authored.generateTitle') }}
|
|
628
|
+
</UButton>
|
|
629
|
+
</div>
|
|
630
|
+
<UInput
|
|
631
|
+
v-model="editDraft.summary"
|
|
632
|
+
:placeholder="t('fragments.authored.summaryPlaceholder')"
|
|
633
|
+
/>
|
|
634
|
+
<UTextarea
|
|
635
|
+
v-model="editDraft.body"
|
|
636
|
+
:placeholder="t('fragments.authored.bodyPlaceholder')"
|
|
637
|
+
:rows="4"
|
|
638
|
+
/>
|
|
639
|
+
<UInput
|
|
640
|
+
v-model="editDraft.tags"
|
|
641
|
+
:placeholder="t('fragments.authored.tagsPlaceholder')"
|
|
642
|
+
/>
|
|
643
|
+
<div class="flex gap-2">
|
|
644
|
+
<UButton
|
|
645
|
+
size="sm"
|
|
646
|
+
:disabled="!editValid"
|
|
647
|
+
:loading="rowBusy(`edit:${f.id}`)"
|
|
648
|
+
@click="saveEdit"
|
|
649
|
+
>
|
|
650
|
+
{{ t('common.save') }}
|
|
651
|
+
</UButton>
|
|
652
|
+
<UButton size="sm" variant="ghost" color="neutral" @click="cancelEdit">
|
|
653
|
+
{{ t('common.cancel') }}
|
|
654
|
+
</UButton>
|
|
655
|
+
</div>
|
|
656
|
+
</div>
|
|
657
|
+
<!-- Row -->
|
|
658
|
+
<div v-else class="flex items-start gap-2">
|
|
659
|
+
<div class="min-w-0">
|
|
660
|
+
<div class="flex items-center gap-2">
|
|
661
|
+
<span class="font-medium text-slate-100">{{ f.title }}</span>
|
|
662
|
+
<UBadge v-if="f.source" size="xs" color="info" variant="subtle">{{
|
|
663
|
+
t('fragments.authored.fromRepo')
|
|
664
|
+
}}</UBadge>
|
|
665
|
+
</div>
|
|
666
|
+
<p class="text-sm text-slate-400">{{ f.summary }}</p>
|
|
667
|
+
</div>
|
|
668
|
+
<div class="ms-auto flex gap-1">
|
|
669
|
+
<!-- Editing a repo-SOURCED fragment locally would be overwritten on the next sync,
|
|
670
|
+
so only hand-authored fragments are editable here. -->
|
|
671
|
+
<UButton
|
|
672
|
+
v-if="!f.source"
|
|
673
|
+
icon="i-lucide-pencil"
|
|
674
|
+
size="xs"
|
|
675
|
+
variant="ghost"
|
|
676
|
+
:title="t('common.edit')"
|
|
677
|
+
@click="startEdit(f)"
|
|
678
|
+
/>
|
|
679
|
+
<UButton
|
|
680
|
+
icon="i-lucide-trash-2"
|
|
681
|
+
size="xs"
|
|
682
|
+
color="error"
|
|
683
|
+
variant="ghost"
|
|
684
|
+
:loading="rowBusy(`remove:${f.id}`)"
|
|
685
|
+
@click="removeFragment(f.id)"
|
|
686
|
+
/>
|
|
533
687
|
</div>
|
|
534
|
-
<p class="text-sm text-slate-400">{{ f.summary }}</p>
|
|
535
688
|
</div>
|
|
536
|
-
<UButton
|
|
537
|
-
icon="i-lucide-trash-2"
|
|
538
|
-
size="xs"
|
|
539
|
-
color="error"
|
|
540
|
-
variant="ghost"
|
|
541
|
-
class="ms-auto"
|
|
542
|
-
:loading="rowBusy(`remove:${f.id}`)"
|
|
543
|
-
@click="removeFragment(f.id)"
|
|
544
|
-
/>
|
|
545
689
|
</div>
|
|
546
690
|
<p v-if="!library.fragments.length" class="text-sm text-slate-500">
|
|
547
691
|
{{
|
|
@@ -554,7 +698,33 @@ async function unlinkSource(id: string) {
|
|
|
554
698
|
<div class="rounded-md border border-slate-800 p-3">
|
|
555
699
|
<p class="mb-2 text-sm font-medium">{{ t('fragments.authored.addTitle') }}</p>
|
|
556
700
|
<div class="flex flex-col gap-2">
|
|
557
|
-
<
|
|
701
|
+
<div class="flex gap-2">
|
|
702
|
+
<UInput
|
|
703
|
+
v-model="draft.title"
|
|
704
|
+
:placeholder="t('fragments.authored.titlePlaceholder')"
|
|
705
|
+
class="flex-1"
|
|
706
|
+
/>
|
|
707
|
+
<UButton
|
|
708
|
+
icon="i-lucide-wand-2"
|
|
709
|
+
size="sm"
|
|
710
|
+
variant="outline"
|
|
711
|
+
:disabled="!draft.body.trim()"
|
|
712
|
+
:loading="generatingTitleFor === 'create'"
|
|
713
|
+
:title="t('fragments.authored.generateTitleHint')"
|
|
714
|
+
data-testid="fragment-generate-title"
|
|
715
|
+
@click="
|
|
716
|
+
autofillTitle(
|
|
717
|
+
'create',
|
|
718
|
+
() => ({ body: draft.body, summary: draft.summary }),
|
|
719
|
+
(title) => {
|
|
720
|
+
draft.title = title
|
|
721
|
+
},
|
|
722
|
+
)
|
|
723
|
+
"
|
|
724
|
+
>
|
|
725
|
+
{{ t('fragments.authored.generateTitle') }}
|
|
726
|
+
</UButton>
|
|
727
|
+
</div>
|
|
558
728
|
<UInput
|
|
559
729
|
v-model="draft.summary"
|
|
560
730
|
:placeholder="t('fragments.authored.summaryPlaceholder')"
|
|
@@ -483,6 +483,17 @@ async function copyOutput() {
|
|
|
483
483
|
it raised and the greenlight verdict; plus the fixer-loop phase -->
|
|
484
484
|
<StepTestReport v-if="testReport" :report="testReport" :phase="testPhase" />
|
|
485
485
|
|
|
486
|
+
<!-- code/PR reviewer's best-practice adherence: per standard, a 1..10 rating of how
|
|
487
|
+
well the change adheres + the issues it surfaced. Only on a review step. -->
|
|
488
|
+
<StepFragmentAdherence
|
|
489
|
+
v-if="step.fragmentAdherence?.length"
|
|
490
|
+
:items="step.fragmentAdherence"
|
|
491
|
+
/>
|
|
492
|
+
|
|
493
|
+
<!-- container agent's effort self-assessment (how hard it was, what reduced its
|
|
494
|
+
effectiveness, key obstacles). Only when the agent reported one. -->
|
|
495
|
+
<StepEffortReport v-if="step.effortReport" :report="step.effortReport" />
|
|
496
|
+
|
|
486
497
|
<!-- edit-then-approve: a direct editor over the raw conclusions; the
|
|
487
498
|
edits become the approved proposal that flows to the next step -->
|
|
488
499
|
<section v-if="editing" class="scroll-mt-4">
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The container agent's effort self-assessment, surfaced in run details: how hard the work was
|
|
3
|
+
// (1..10), what reduced its effectiveness, and the key obstacles it hit. Populated by the harness
|
|
4
|
+
// from the agent's sentinel file and recorded on the step (`step.effortReport`). Rendered only when
|
|
5
|
+
// present, so a run on an older harness image (or an agent that wrote none) shows nothing.
|
|
6
|
+
import type { AgentEffortReport } from '~/types/execution'
|
|
7
|
+
|
|
8
|
+
const props = defineProps<{ report: AgentEffortReport }>()
|
|
9
|
+
const { t } = useI18n()
|
|
10
|
+
|
|
11
|
+
// Clamp for the bar width; the schema already bounds 1..10 but be defensive against a stray value.
|
|
12
|
+
const difficultyPct = computed(() =>
|
|
13
|
+
Math.min(100, Math.max(0, (props.report.difficulty / 10) * 100)),
|
|
14
|
+
)
|
|
15
|
+
// Colour the difficulty by band: easy (emerald) → moderate (amber) → hard (rose).
|
|
16
|
+
const difficultyClass = computed(() =>
|
|
17
|
+
props.report.difficulty >= 8
|
|
18
|
+
? 'bg-rose-400'
|
|
19
|
+
: props.report.difficulty >= 5
|
|
20
|
+
? 'bg-amber-400'
|
|
21
|
+
: 'bg-emerald-400',
|
|
22
|
+
)
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<section
|
|
27
|
+
data-testid="step-effort-report"
|
|
28
|
+
class="scroll-mt-4 rounded-xl border border-slate-800 bg-slate-900/50 p-4"
|
|
29
|
+
>
|
|
30
|
+
<div
|
|
31
|
+
class="mb-2 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-400"
|
|
32
|
+
>
|
|
33
|
+
<UIcon name="i-lucide-gauge" class="h-3.5 w-3.5" />
|
|
34
|
+
<span>{{ t('panels.stepDetail.effort.heading') }}</span>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="flex items-center gap-2">
|
|
38
|
+
<span class="text-[12px] text-slate-300">{{ t('panels.stepDetail.effort.difficulty') }}</span>
|
|
39
|
+
<div class="h-1.5 flex-1 overflow-hidden rounded-full bg-slate-700/60">
|
|
40
|
+
<div
|
|
41
|
+
class="h-full rounded-full"
|
|
42
|
+
:class="difficultyClass"
|
|
43
|
+
:style="{ width: `${difficultyPct}%` }"
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
<span data-testid="step-effort-difficulty" class="text-[12px] font-medium text-slate-200">
|
|
47
|
+
{{ t('panels.stepDetail.effort.outOfTen', { value: report.difficulty }) }}
|
|
48
|
+
</span>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<p
|
|
52
|
+
v-if="report.summary"
|
|
53
|
+
class="mt-2 whitespace-pre-wrap text-[13px] leading-relaxed text-slate-300"
|
|
54
|
+
>
|
|
55
|
+
{{ report.summary }}
|
|
56
|
+
</p>
|
|
57
|
+
|
|
58
|
+
<div v-if="report.reducedEffectiveness" class="mt-3">
|
|
59
|
+
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
60
|
+
{{ t('panels.stepDetail.effort.reduced') }}
|
|
61
|
+
</p>
|
|
62
|
+
<p class="mt-0.5 whitespace-pre-wrap text-[12px] text-slate-300">
|
|
63
|
+
{{ report.reducedEffectiveness }}
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div v-if="report.obstacles?.length" class="mt-3">
|
|
68
|
+
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
69
|
+
{{ t('panels.stepDetail.effort.obstacles') }}
|
|
70
|
+
</p>
|
|
71
|
+
<ul class="mt-0.5 space-y-1">
|
|
72
|
+
<li
|
|
73
|
+
v-for="(obstacle, i) in report.obstacles"
|
|
74
|
+
:key="i"
|
|
75
|
+
class="flex items-start gap-1.5 text-[12px] text-slate-300"
|
|
76
|
+
>
|
|
77
|
+
<UIcon
|
|
78
|
+
name="i-lucide-alert-triangle"
|
|
79
|
+
class="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-400/80"
|
|
80
|
+
/>
|
|
81
|
+
<span>{{ obstacle }}</span>
|
|
82
|
+
</li>
|
|
83
|
+
</ul>
|
|
84
|
+
</div>
|
|
85
|
+
</section>
|
|
86
|
+
</template>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// A code/PR review step's best-practice adherence report, surfaced in run details: for each
|
|
3
|
+
// best-practice standard (prompt fragment) folded into the reviewer's prompt, how well the reviewed
|
|
4
|
+
// change adheres (1..10) and the issues that standard surfaced. Recorded on the step
|
|
5
|
+
// (`step.fragmentAdherence`) by the engine from the review agent's output. Rendered only when the
|
|
6
|
+
// reviewer reported at least one standard; when none were reachable the reviewer says so in its
|
|
7
|
+
// summary instead (so there is nothing to show here).
|
|
8
|
+
import type { FragmentAdherence } from '~/types/execution'
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{ items: FragmentAdherence }>()
|
|
11
|
+
const { t } = useI18n()
|
|
12
|
+
|
|
13
|
+
/** Rating band → bar colour: poor adherence (rose) → partial (amber) → strong (emerald). */
|
|
14
|
+
function ratingClass(rating: number): string {
|
|
15
|
+
return rating >= 8 ? 'bg-emerald-400' : rating >= 5 ? 'bg-amber-400' : 'bg-rose-400'
|
|
16
|
+
}
|
|
17
|
+
function ratingPct(rating: number): number {
|
|
18
|
+
return Math.min(100, Math.max(0, (rating / 10) * 100))
|
|
19
|
+
}
|
|
20
|
+
/** The label the reviewer was asked to cite the standard by: its title, else its id. */
|
|
21
|
+
function label(item: FragmentAdherence[number]): string {
|
|
22
|
+
return item.title?.trim() || item.fragmentId || t('panels.stepDetail.adherence.unnamed')
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<section
|
|
28
|
+
v-if="items.length"
|
|
29
|
+
data-testid="step-fragment-adherence"
|
|
30
|
+
class="scroll-mt-4 rounded-xl border border-slate-800 bg-slate-900/50 p-4"
|
|
31
|
+
>
|
|
32
|
+
<div
|
|
33
|
+
class="mb-2 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-400"
|
|
34
|
+
>
|
|
35
|
+
<UIcon name="i-lucide-clipboard-check" class="h-3.5 w-3.5" />
|
|
36
|
+
<span>{{ t('panels.stepDetail.adherence.heading') }}</span>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="space-y-2.5">
|
|
40
|
+
<article
|
|
41
|
+
v-for="(item, i) in items"
|
|
42
|
+
:key="item.fragmentId ?? i"
|
|
43
|
+
data-testid="step-fragment-adherence-item"
|
|
44
|
+
class="rounded-lg border border-slate-800 bg-slate-900/60 px-3 py-2"
|
|
45
|
+
>
|
|
46
|
+
<div class="flex items-center gap-2">
|
|
47
|
+
<span class="min-w-0 flex-1 truncate text-[13px] font-medium text-slate-100">{{
|
|
48
|
+
label(item)
|
|
49
|
+
}}</span>
|
|
50
|
+
<div class="h-1.5 w-16 shrink-0 overflow-hidden rounded-full bg-slate-700/60">
|
|
51
|
+
<div
|
|
52
|
+
class="h-full rounded-full"
|
|
53
|
+
:class="ratingClass(item.rating)"
|
|
54
|
+
:style="{ width: `${ratingPct(item.rating)}%` }"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
<span class="shrink-0 text-[12px] font-medium text-slate-200">
|
|
58
|
+
{{ t('panels.stepDetail.adherence.outOfTen', { value: item.rating }) }}
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
<p
|
|
62
|
+
v-if="item.assessment"
|
|
63
|
+
class="mt-1 whitespace-pre-wrap text-[12px] leading-relaxed text-slate-300"
|
|
64
|
+
>
|
|
65
|
+
{{ item.assessment }}
|
|
66
|
+
</p>
|
|
67
|
+
<div v-if="item.relatedFindings.length" class="mt-1.5">
|
|
68
|
+
<p class="text-[10px] font-semibold uppercase tracking-wide text-slate-500">
|
|
69
|
+
{{ t('panels.stepDetail.adherence.relatedFindings') }}
|
|
70
|
+
</p>
|
|
71
|
+
<ul class="mt-0.5 space-y-0.5">
|
|
72
|
+
<li
|
|
73
|
+
v-for="(finding, fi) in item.relatedFindings"
|
|
74
|
+
:key="fi"
|
|
75
|
+
class="flex items-start gap-1.5 text-[11px] text-slate-400"
|
|
76
|
+
>
|
|
77
|
+
<UIcon name="i-lucide-dot" class="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
78
|
+
<span>{{ finding }}</span>
|
|
79
|
+
</li>
|
|
80
|
+
</ul>
|
|
81
|
+
</div>
|
|
82
|
+
</article>
|
|
83
|
+
</div>
|
|
84
|
+
</section>
|
|
85
|
+
</template>
|
|
@@ -466,6 +466,14 @@ async function onDismiss(id: string): Promise<void> {
|
|
|
466
466
|
{{ state.summary }}
|
|
467
467
|
</p>
|
|
468
468
|
|
|
469
|
+
<!-- Best-practice adherence: per standard folded into the reviewer's prompt, a 1..10
|
|
470
|
+
rating of how well the PR adheres + the issues that standard surfaced. -->
|
|
471
|
+
<StepFragmentAdherence
|
|
472
|
+
v-if="step?.fragmentAdherence?.length"
|
|
473
|
+
:items="step.fragmentAdherence"
|
|
474
|
+
class="mb-3"
|
|
475
|
+
/>
|
|
476
|
+
|
|
469
477
|
<!-- A clean PR / resolved review with no findings. -->
|
|
470
478
|
<div
|
|
471
479
|
v-if="findings.length === 0"
|
|
@@ -210,6 +210,23 @@ async function add() {
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
/** Route an update to the scope the key lives in (account / workspace / user). */
|
|
214
|
+
async function updateKey(k: ApiKey, patch: { enabled?: boolean; isDefault?: boolean }) {
|
|
215
|
+
try {
|
|
216
|
+
if (k.scope === 'account') await keys.updateAccountKey(k.id, patch)
|
|
217
|
+
else if (k.scope === 'workspace') await keys.updateWorkspaceKey(k.id, patch)
|
|
218
|
+
else await keys.updateUserKey(k.id, patch)
|
|
219
|
+
// Enabling/disabling changes provider selectability in the picker — refresh it.
|
|
220
|
+
if (workspace.workspaceId) await models.refresh(workspace.workspaceId)
|
|
221
|
+
} catch (e) {
|
|
222
|
+
toast.add({
|
|
223
|
+
title: t('providers.apiKeys.toast.updateFailed'),
|
|
224
|
+
description: e instanceof Error ? e.message : String(e),
|
|
225
|
+
color: 'error',
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
213
230
|
async function remove(k: ApiKey) {
|
|
214
231
|
const noun = t('providers.apiKeys.keyNoun')
|
|
215
232
|
if (!(await confirmAction('remove', noun))) return
|
|
@@ -352,10 +369,14 @@ async function remove(k: ApiKey) {
|
|
|
352
369
|
v-for="k in connected"
|
|
353
370
|
:key="k.id"
|
|
354
371
|
class="flex items-center justify-between rounded-md border border-slate-700 bg-slate-900/50 px-3 py-2 text-sm"
|
|
372
|
+
:class="{ 'opacity-55': !k.enabled }"
|
|
355
373
|
>
|
|
356
374
|
<div>
|
|
357
375
|
<span class="font-medium text-slate-200">{{ k.label }}</span>
|
|
358
376
|
<span class="ms-2 text-xs text-slate-500">{{ providerLabel(k.provider) }}</span>
|
|
377
|
+
<UBadge v-if="!k.enabled" color="neutral" variant="subtle" size="sm" class="ms-2">
|
|
378
|
+
{{ t('providers.apiKeys.disabledBadge') }}
|
|
379
|
+
</UBadge>
|
|
359
380
|
<div class="text-[11px] tabular-nums text-slate-500">
|
|
360
381
|
{{
|
|
361
382
|
t(
|
|
@@ -366,13 +387,32 @@ async function remove(k: ApiKey) {
|
|
|
366
387
|
}}
|
|
367
388
|
</div>
|
|
368
389
|
</div>
|
|
369
|
-
<
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
390
|
+
<div class="flex items-center gap-2">
|
|
391
|
+
<UButton
|
|
392
|
+
:icon="k.isDefault ? 'i-lucide-star' : 'i-lucide-star-off'"
|
|
393
|
+
:color="k.isDefault ? 'primary' : 'neutral'"
|
|
394
|
+
:variant="k.isDefault ? 'subtle' : 'ghost'"
|
|
395
|
+
size="xs"
|
|
396
|
+
@click="updateKey(k, { isDefault: !k.isDefault })"
|
|
397
|
+
>
|
|
398
|
+
{{
|
|
399
|
+
k.isDefault ? t('providers.apiKeys.defaultBadge') : t('providers.apiKeys.pinDefault')
|
|
400
|
+
}}
|
|
401
|
+
</UButton>
|
|
402
|
+
<USwitch
|
|
403
|
+
:model-value="k.enabled"
|
|
404
|
+
size="sm"
|
|
405
|
+
:aria-label="t('providers.apiKeys.enableToggle')"
|
|
406
|
+
@update:model-value="(v: boolean) => updateKey(k, { enabled: v })"
|
|
407
|
+
/>
|
|
408
|
+
<UButton
|
|
409
|
+
icon="i-lucide-trash-2"
|
|
410
|
+
color="error"
|
|
411
|
+
variant="ghost"
|
|
412
|
+
size="xs"
|
|
413
|
+
@click="remove(k)"
|
|
414
|
+
/>
|
|
415
|
+
</div>
|
|
376
416
|
</div>
|
|
377
417
|
</div>
|
|
378
418
|
</div>
|
|
@@ -141,6 +141,30 @@ async function add() {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
async function toggleEnabled(cred: { id: string }, enabled: boolean) {
|
|
145
|
+
try {
|
|
146
|
+
await creds.update(cred.id, { enabled })
|
|
147
|
+
} catch (e) {
|
|
148
|
+
toast.add({
|
|
149
|
+
title: t('providers.vendorCredentials.toast.updateFailed'),
|
|
150
|
+
description: e instanceof Error ? e.message : String(e),
|
|
151
|
+
color: 'error',
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function toggleDefault(cred: { id: string; isDefault: boolean }) {
|
|
157
|
+
try {
|
|
158
|
+
await creds.update(cred.id, { isDefault: !cred.isDefault })
|
|
159
|
+
} catch (e) {
|
|
160
|
+
toast.add({
|
|
161
|
+
title: t('providers.vendorCredentials.toast.updateFailed'),
|
|
162
|
+
description: e instanceof Error ? e.message : String(e),
|
|
163
|
+
color: 'error',
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
144
168
|
async function remove(cred: { id: string; label: string }) {
|
|
145
169
|
const ok = await confirm({
|
|
146
170
|
title: t('providers.vendorCredentials.confirmRemove.title'),
|
|
@@ -245,10 +269,14 @@ function vendorLabel(v: SubscriptionVendor): string {
|
|
|
245
269
|
v-for="c in creds.credentials"
|
|
246
270
|
:key="c.id"
|
|
247
271
|
class="flex items-center justify-between rounded-md border border-slate-700 bg-slate-900/50 px-3 py-2 text-sm"
|
|
272
|
+
:class="{ 'opacity-55': !c.enabled }"
|
|
248
273
|
>
|
|
249
274
|
<div>
|
|
250
275
|
<span class="font-medium text-slate-200">{{ c.label }}</span>
|
|
251
276
|
<span class="ms-2 text-xs text-slate-500">{{ vendorLabel(c.vendor) }}</span>
|
|
277
|
+
<UBadge v-if="!c.enabled" color="neutral" variant="subtle" size="sm" class="ms-2">
|
|
278
|
+
{{ t('providers.vendorCredentials.disabledBadge') }}
|
|
279
|
+
</UBadge>
|
|
252
280
|
<div class="text-[11px] tabular-nums text-slate-500">
|
|
253
281
|
{{
|
|
254
282
|
t(
|
|
@@ -262,13 +290,34 @@ function vendorLabel(v: SubscriptionVendor): string {
|
|
|
262
290
|
}}
|
|
263
291
|
</div>
|
|
264
292
|
</div>
|
|
265
|
-
<
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
293
|
+
<div class="flex items-center gap-2">
|
|
294
|
+
<UButton
|
|
295
|
+
:icon="c.isDefault ? 'i-lucide-star' : 'i-lucide-star-off'"
|
|
296
|
+
:color="c.isDefault ? 'primary' : 'neutral'"
|
|
297
|
+
:variant="c.isDefault ? 'subtle' : 'ghost'"
|
|
298
|
+
size="xs"
|
|
299
|
+
@click="toggleDefault(c)"
|
|
300
|
+
>
|
|
301
|
+
{{
|
|
302
|
+
c.isDefault
|
|
303
|
+
? t('providers.vendorCredentials.defaultBadge')
|
|
304
|
+
: t('providers.vendorCredentials.pinDefault')
|
|
305
|
+
}}
|
|
306
|
+
</UButton>
|
|
307
|
+
<USwitch
|
|
308
|
+
:model-value="c.enabled"
|
|
309
|
+
size="sm"
|
|
310
|
+
:aria-label="t('providers.vendorCredentials.enableToggle')"
|
|
311
|
+
@update:model-value="(v: boolean) => toggleEnabled(c, v)"
|
|
312
|
+
/>
|
|
313
|
+
<UButton
|
|
314
|
+
icon="i-lucide-trash-2"
|
|
315
|
+
color="error"
|
|
316
|
+
variant="ghost"
|
|
317
|
+
size="xs"
|
|
318
|
+
@click="remove(c)"
|
|
319
|
+
/>
|
|
320
|
+
</div>
|
|
272
321
|
</div>
|
|
273
322
|
</div>
|
|
274
323
|
</div>
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
createPromptFragmentContract,
|
|
4
4
|
deletePromptFragmentContract,
|
|
5
5
|
fragmentSourceStatusContract,
|
|
6
|
+
generatePromptFragmentTitleContract,
|
|
6
7
|
linkFragmentSourceContract,
|
|
7
8
|
listFragmentCatalogContract,
|
|
8
9
|
listFragmentSourcesContract,
|
|
@@ -17,6 +18,7 @@ import type {
|
|
|
17
18
|
CreateDocumentFragmentInput,
|
|
18
19
|
CreatePromptFragmentInput,
|
|
19
20
|
FragmentOwnerKind,
|
|
21
|
+
GenerateFragmentTitleInput,
|
|
20
22
|
LinkFragmentSourceInput,
|
|
21
23
|
UpdatePromptFragmentInput,
|
|
22
24
|
} from '~/types/domain'
|
|
@@ -58,6 +60,21 @@ export function fragmentsApi({ send, ws, scope }: ApiContext) {
|
|
|
58
60
|
pathParams: { fragmentId },
|
|
59
61
|
}),
|
|
60
62
|
|
|
63
|
+
// Auto-generate a title for a hand-authored fragment from its content (an inline LLM call).
|
|
64
|
+
// At the account scope the backend resolves the model against `viaWorkspaceId`'s scope;
|
|
65
|
+
// ignored at the workspace scope (which uses the addressed workspace).
|
|
66
|
+
generateFragmentTitle: (
|
|
67
|
+
kind: FragmentOwnerKind,
|
|
68
|
+
id: string,
|
|
69
|
+
body: GenerateFragmentTitleInput,
|
|
70
|
+
viaWorkspaceId?: string,
|
|
71
|
+
) =>
|
|
72
|
+
send(generatePromptFragmentTitleContract, {
|
|
73
|
+
pathPrefix: scope(kind, id),
|
|
74
|
+
queryParams: { viaWorkspaceId },
|
|
75
|
+
body,
|
|
76
|
+
}),
|
|
77
|
+
|
|
61
78
|
// Link an external document (Confluence/Notion/GitHub) as a living fragment.
|
|
62
79
|
createDocumentFragment: (
|
|
63
80
|
kind: FragmentOwnerKind,
|