@cat-factory/app 0.184.0 → 0.186.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/README.md +4 -1
- package/app/assets/css/main.css +2 -0
- package/app/assets/css/prose.css +135 -0
- package/app/components/board/AddTaskModal.vue +31 -1
- package/app/components/brainstorm/BrainstormWindow.vue +4 -37
- package/app/components/clarity/ClarityReviewWindow.vue +4 -37
- package/app/components/initiative/InitiativePlanReview.vue +351 -0
- package/app/components/initiative/InitiativeTrackerWindow.vue +12 -126
- package/app/components/panels/AgentStepDetail.vue +19 -122
- package/app/components/panels/inspector/ServiceValidationConfig.vue +42 -4
- package/app/components/panels/inspector/TaskReviewTarget.vue +69 -0
- package/app/components/requirements/RequirementsReviewWindow.vue +4 -37
- package/app/composables/useProseComments.ts +126 -0
- package/app/composables/useStepApproval.ts +29 -85
- package/app/composables/useStepProse.spec.ts +67 -0
- package/app/composables/useStepProse.ts +22 -10
- package/app/modular/nav-contributions.spec.ts +3 -13
- package/app/modular/panels/inspector.logic.spec.ts +7 -0
- package/app/modular/panels/inspector.logic.ts +5 -0
- package/app/modular/panels/inspector.ts +2 -0
- package/app/stores/execution.ts +9 -0
- package/app/stores/validationChecks.ts +8 -4
- package/app/utils/initiative.spec.ts +24 -0
- package/i18n/locales/de.json +17 -3
- package/i18n/locales/en.json +18 -3
- package/i18n/locales/es.json +17 -3
- package/i18n/locales/fr.json +17 -3
- package/i18n/locales/he.json +17 -3
- package/i18n/locales/it.json +17 -3
- package/i18n/locales/ja.json +17 -3
- package/i18n/locales/pl.json +17 -3
- package/i18n/locales/tr.json +17 -3
- package/i18n/locales/uk.json +17 -3
- package/package.json +2 -2
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// step's park routes (its archetype declares this result view), so the approve /
|
|
13
13
|
// request-changes rail has to live here or the gate has no resolving surface at all —
|
|
14
14
|
// which is exactly how an approved-only-over-REST plan gate shipped.
|
|
15
|
-
import { computed, reactive, ref
|
|
15
|
+
import { computed, reactive, ref } from 'vue'
|
|
16
16
|
import type { InitiativeFollowUp, InitiativeItem } from '~/types/domain'
|
|
17
17
|
import { useInitiativePlanning } from '~/composables/useInitiativePlanning'
|
|
18
18
|
import {
|
|
@@ -26,10 +26,10 @@ import {
|
|
|
26
26
|
} from '~/utils/initiative'
|
|
27
27
|
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
28
28
|
import StepRunMeta from '~/components/panels/StepRunMeta.vue'
|
|
29
|
+
import InitiativePlanReview from '~/components/initiative/InitiativePlanReview.vue'
|
|
29
30
|
|
|
30
31
|
const board = useBoardStore()
|
|
31
32
|
const initiatives = useInitiativesStore()
|
|
32
|
-
const execution = useExecutionStore()
|
|
33
33
|
const access = useWorkspaceAccess()
|
|
34
34
|
const { t } = useI18n()
|
|
35
35
|
const toast = useToast()
|
|
@@ -106,52 +106,6 @@ async function checkpointControl(action: 'resume' | 'cancel') {
|
|
|
106
106
|
// human parked on the gate actually uses. So the rail appears on every route into the window.
|
|
107
107
|
const { planApproval } = useInitiativePlanning(() => blockId.value ?? '')
|
|
108
108
|
|
|
109
|
-
/** Draft feedback for "request changes" (the planner re-runs with it), and the rail's in-flight
|
|
110
|
-
* state. Reset when a different initiative opens so a draft can't follow the window. */
|
|
111
|
-
const planFeedback = ref('')
|
|
112
|
-
const requestingChanges = ref(false)
|
|
113
|
-
const resolvingPlan = ref(false)
|
|
114
|
-
watch(blockId, () => {
|
|
115
|
-
planFeedback.value = ''
|
|
116
|
-
requestingChanges.value = false
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
const canRequestChanges = computed(() => planFeedback.value.trim().length > 0)
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Accept the drafted plan: the run advances to the committer, which persists the initiative and
|
|
123
|
-
* arms the execution loop. The window deliberately stays OPEN — the rail disappears with the
|
|
124
|
-
* approval (live), and the tracker is where the plan then starts executing.
|
|
125
|
-
*/
|
|
126
|
-
async function approvePlan() {
|
|
127
|
-
const parked = planApproval.value
|
|
128
|
-
if (!parked || resolvingPlan.value) return
|
|
129
|
-
resolvingPlan.value = true
|
|
130
|
-
try {
|
|
131
|
-
await execution.approveStep(parked.instanceId, parked.approval.id)
|
|
132
|
-
} finally {
|
|
133
|
-
resolvingPlan.value = false
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Send the plan back to the planner with what to change; it re-plans and parks again. */
|
|
138
|
-
async function submitPlanChanges() {
|
|
139
|
-
const parked = planApproval.value
|
|
140
|
-
if (!parked || resolvingPlan.value || !canRequestChanges.value) return
|
|
141
|
-
resolvingPlan.value = true
|
|
142
|
-
try {
|
|
143
|
-
const ok = await execution.requestStepChanges(parked.instanceId, parked.approval.id, {
|
|
144
|
-
feedback: planFeedback.value.trim(),
|
|
145
|
-
})
|
|
146
|
-
if (ok) {
|
|
147
|
-
planFeedback.value = ''
|
|
148
|
-
requestingChanges.value = false
|
|
149
|
-
}
|
|
150
|
-
} finally {
|
|
151
|
-
resolvingPlan.value = false
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
109
|
const policyRules = computed(() => initiative.value?.policy?.rules ?? [])
|
|
156
110
|
function ruleAxes(rule: { minComplexity?: number; minRisk?: number; minImpact?: number }): string {
|
|
157
111
|
const axes = [
|
|
@@ -294,85 +248,17 @@ async function savePolicy() {
|
|
|
294
248
|
</div>
|
|
295
249
|
|
|
296
250
|
<template v-else>
|
|
297
|
-
<!-- The planner's human gate
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
<
|
|
251
|
+
<!-- The planner's human gate. This window is where the park ROUTES (the planner's
|
|
252
|
+
archetype declares this result view), so it is the only surface that can resolve
|
|
253
|
+
it — and the plan it judges is rendered as a navigable document with per-block
|
|
254
|
+
commenting, the same tools the step reader gives the architect's prose. -->
|
|
255
|
+
<InitiativePlanReview
|
|
302
256
|
v-if="planApproval"
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
name="i-lucide-clipboard-check"
|
|
309
|
-
class="mt-0.5 h-4 w-4 shrink-0 text-amber-300"
|
|
310
|
-
/>
|
|
311
|
-
<div class="min-w-0 flex-1">
|
|
312
|
-
<h3 class="text-[13px] font-semibold text-amber-200">
|
|
313
|
-
{{ t('initiative.planReview.title') }}
|
|
314
|
-
</h3>
|
|
315
|
-
<p class="mt-0.5 text-[12px] leading-relaxed text-amber-100/80">
|
|
316
|
-
{{ t('initiative.planReview.body') }}
|
|
317
|
-
</p>
|
|
318
|
-
<div v-if="!requestingChanges" class="mt-2.5 flex flex-wrap gap-2">
|
|
319
|
-
<button
|
|
320
|
-
class="rounded bg-indigo-600 px-2.5 py-1 text-[11px] font-medium text-white hover:bg-indigo-500 disabled:opacity-50"
|
|
321
|
-
:disabled="resolvingPlan || !access.canExecuteRuns.value"
|
|
322
|
-
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
323
|
-
data-testid="initiative-plan-approve"
|
|
324
|
-
@click="approvePlan"
|
|
325
|
-
>
|
|
326
|
-
{{ t('initiative.planReview.approve') }}
|
|
327
|
-
</button>
|
|
328
|
-
<button
|
|
329
|
-
class="rounded border border-amber-400/50 px-2.5 py-1 text-[11px] font-medium text-amber-200 hover:bg-amber-500/10 disabled:opacity-50"
|
|
330
|
-
:disabled="resolvingPlan || !access.canExecuteRuns.value"
|
|
331
|
-
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
332
|
-
data-testid="initiative-plan-request-changes"
|
|
333
|
-
@click="requestingChanges = true"
|
|
334
|
-
>
|
|
335
|
-
{{ t('initiative.planReview.requestChanges') }}
|
|
336
|
-
</button>
|
|
337
|
-
</div>
|
|
338
|
-
<!-- Request-changes composer: the feedback is what the planner re-plans FROM,
|
|
339
|
-
so it is required — an empty send would re-run the planner with nothing
|
|
340
|
-
to act on and park again on the same plan. -->
|
|
341
|
-
<div v-else class="mt-2.5">
|
|
342
|
-
<UTextarea
|
|
343
|
-
v-model="planFeedback"
|
|
344
|
-
:rows="3"
|
|
345
|
-
autoresize
|
|
346
|
-
size="sm"
|
|
347
|
-
class="w-full"
|
|
348
|
-
data-testid="initiative-plan-feedback"
|
|
349
|
-
:placeholder="t('initiative.planReview.feedbackPlaceholder')"
|
|
350
|
-
/>
|
|
351
|
-
<div class="mt-2 flex flex-wrap gap-2">
|
|
352
|
-
<button
|
|
353
|
-
class="rounded bg-amber-500 px-2.5 py-1 text-[11px] font-medium text-slate-950 hover:bg-amber-400 disabled:opacity-50"
|
|
354
|
-
:disabled="
|
|
355
|
-
resolvingPlan || !canRequestChanges || !access.canExecuteRuns.value
|
|
356
|
-
"
|
|
357
|
-
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
358
|
-
data-testid="initiative-plan-send-back"
|
|
359
|
-
@click="submitPlanChanges"
|
|
360
|
-
>
|
|
361
|
-
{{ t('initiative.planReview.sendBack') }}
|
|
362
|
-
</button>
|
|
363
|
-
<button
|
|
364
|
-
class="rounded border border-slate-600 px-2.5 py-1 text-[11px] font-medium text-slate-300 hover:bg-slate-800 disabled:opacity-50"
|
|
365
|
-
:disabled="resolvingPlan"
|
|
366
|
-
data-testid="initiative-plan-cancel-changes"
|
|
367
|
-
@click="requestingChanges = false"
|
|
368
|
-
>
|
|
369
|
-
{{ t('common.cancel') }}
|
|
370
|
-
</button>
|
|
371
|
-
</div>
|
|
372
|
-
</div>
|
|
373
|
-
</div>
|
|
374
|
-
</div>
|
|
375
|
-
</section>
|
|
257
|
+
:approval="planApproval.approval"
|
|
258
|
+
:instance-id="planApproval.instanceId"
|
|
259
|
+
:can-execute="access.canExecuteRuns.value"
|
|
260
|
+
:output-is-rendered="planApproval.outputIsRendered"
|
|
261
|
+
/>
|
|
376
262
|
|
|
377
263
|
<!-- Paused at a phase checkpoint (D2): a completed checkpoint phase is awaiting
|
|
378
264
|
review before the next phase spawns. Read the phase's artifacts/PRs below,
|
|
@@ -148,6 +148,14 @@ const {
|
|
|
148
148
|
|
|
149
149
|
const approvalPending = computed(() => step.value?.approval?.status === 'pending')
|
|
150
150
|
const approvalId = computed(() => step.value?.approval?.id ?? null)
|
|
151
|
+
// Whether "approve with corrections" applies. A step whose output is a deterministic
|
|
152
|
+
// RENDERING of an artifact it already produced (the initiative plan, the spec doc, the
|
|
153
|
+
// blueprint tree — `step.outputIsRendered`, set by the engine's `reviewableArtifactOutput`
|
|
154
|
+
// seam) has no editable proposal: the artifact was ingested into domain state before the gate
|
|
155
|
+
// was raised, so an edit typed over the rendering would never reach it. The backend refuses
|
|
156
|
+
// such an edit; hiding the affordance is what stops a reviewer typing corrections into a
|
|
157
|
+
// dead end. Their route is "request changes", which re-runs the producer with the correction.
|
|
158
|
+
const proposalEditable = computed(() => step.value?.outputIsRendered !== true)
|
|
151
159
|
// A companion step parked at its automatic-rework cap: instead of the generic
|
|
152
160
|
// approve/request-changes/reject rail, it shows the shared iteration-cap prompt
|
|
153
161
|
// (one more round / proceed / stop & reset), resolved through its own endpoint.
|
|
@@ -261,6 +269,7 @@ async function copyOutput() {
|
|
|
261
269
|
<!-- ToC sidebar (only meaningful when there are prose headings) -->
|
|
262
270
|
<aside
|
|
263
271
|
v-if="outline.hasToc"
|
|
272
|
+
data-testid="step-detail-toc"
|
|
264
273
|
class="hidden w-72 shrink-0 flex-col border-e border-slate-800 bg-slate-900/60 md:flex"
|
|
265
274
|
>
|
|
266
275
|
<div class="border-b border-slate-800 px-4 py-3">
|
|
@@ -646,6 +655,7 @@ async function copyOutput() {
|
|
|
646
655
|
<!-- composer for the block the human just clicked -->
|
|
647
656
|
<div
|
|
648
657
|
v-if="draftTarget"
|
|
658
|
+
data-testid="step-review-composer"
|
|
649
659
|
class="rounded-lg border border-indigo-500/40 bg-indigo-500/5 p-3"
|
|
650
660
|
>
|
|
651
661
|
<div class="mb-1 text-[10px] uppercase tracking-wide text-indigo-300">
|
|
@@ -656,6 +666,7 @@ async function copyOutput() {
|
|
|
656
666
|
>{{ draftTarget.quotedSource }}</pre>
|
|
657
667
|
<UTextarea
|
|
658
668
|
v-model="draftBody"
|
|
669
|
+
data-testid="step-review-comment-body"
|
|
659
670
|
:rows="3"
|
|
660
671
|
autoresize
|
|
661
672
|
size="sm"
|
|
@@ -669,6 +680,7 @@ async function copyOutput() {
|
|
|
669
680
|
<UButton
|
|
670
681
|
color="primary"
|
|
671
682
|
size="xs"
|
|
683
|
+
data-testid="step-review-comment-add"
|
|
672
684
|
:disabled="!draftBody.trim()"
|
|
673
685
|
@click="addDraftComment"
|
|
674
686
|
>
|
|
@@ -681,6 +693,7 @@ async function copyOutput() {
|
|
|
681
693
|
<div
|
|
682
694
|
v-for="(c, idx) in reviewComments"
|
|
683
695
|
:key="idx"
|
|
696
|
+
data-testid="step-review-comment"
|
|
684
697
|
class="rounded-lg border border-slate-800 bg-slate-900/50 p-3"
|
|
685
698
|
>
|
|
686
699
|
<div class="mb-1 flex items-start justify-between gap-2">
|
|
@@ -709,6 +722,7 @@ async function copyOutput() {
|
|
|
709
722
|
</label>
|
|
710
723
|
<UTextarea
|
|
711
724
|
v-model="feedback"
|
|
725
|
+
data-testid="step-review-feedback"
|
|
712
726
|
:rows="3"
|
|
713
727
|
autoresize
|
|
714
728
|
size="sm"
|
|
@@ -763,6 +777,7 @@ async function copyOutput() {
|
|
|
763
777
|
{{ t('panels.stepDetail.approveAndProceed') }}
|
|
764
778
|
</UButton>
|
|
765
779
|
<UButton
|
|
780
|
+
v-if="proposalEditable"
|
|
766
781
|
color="primary"
|
|
767
782
|
variant="soft"
|
|
768
783
|
size="sm"
|
|
@@ -773,6 +788,9 @@ async function copyOutput() {
|
|
|
773
788
|
>
|
|
774
789
|
{{ t('panels.stepDetail.approveWithCorrections') }}
|
|
775
790
|
</UButton>
|
|
791
|
+
<p v-else class="text-[10px] text-slate-500" data-testid="step-rendered-output-note">
|
|
792
|
+
{{ t('panels.stepDetail.renderedOutputNote') }}
|
|
793
|
+
</p>
|
|
776
794
|
|
|
777
795
|
<!-- destructive: a two-step inline confirm instead of a native dialog -->
|
|
778
796
|
<div
|
|
@@ -812,6 +830,7 @@ async function copyOutput() {
|
|
|
812
830
|
size="sm"
|
|
813
831
|
icon="i-lucide-rotate-ccw"
|
|
814
832
|
class="flex-1"
|
|
833
|
+
data-testid="step-request-changes"
|
|
815
834
|
:disabled="!canRequestChanges"
|
|
816
835
|
:loading="submitting"
|
|
817
836
|
@click="requestChanges"
|
|
@@ -849,126 +868,4 @@ async function copyOutput() {
|
|
|
849
868
|
.reader-fade-leave-to {
|
|
850
869
|
opacity: 0;
|
|
851
870
|
}
|
|
852
|
-
|
|
853
|
-
/* Approval mode: each source-mapped block becomes a comment target — a hover
|
|
854
|
-
highlight + a "+" gutter affordance, GitHub-review style. */
|
|
855
|
-
.reader-prose.review-mode :deep([data-src-start]) {
|
|
856
|
-
position: relative;
|
|
857
|
-
cursor: pointer;
|
|
858
|
-
border-radius: 0.375rem;
|
|
859
|
-
transition: background 0.12s ease;
|
|
860
|
-
}
|
|
861
|
-
.reader-prose.review-mode :deep([data-src-start]:hover) {
|
|
862
|
-
background: rgb(99 102 241 / 0.08);
|
|
863
|
-
box-shadow: inset 2px 0 0 rgb(99 102 241 / 0.5);
|
|
864
|
-
}
|
|
865
|
-
.reader-prose.review-mode :deep([data-src-start])::before {
|
|
866
|
-
content: '+';
|
|
867
|
-
position: absolute;
|
|
868
|
-
left: -1.4rem;
|
|
869
|
-
top: 0.1rem;
|
|
870
|
-
display: none;
|
|
871
|
-
height: 1.1rem;
|
|
872
|
-
width: 1.1rem;
|
|
873
|
-
align-items: center;
|
|
874
|
-
justify-content: center;
|
|
875
|
-
border-radius: 0.25rem;
|
|
876
|
-
background: rgb(99 102 241);
|
|
877
|
-
color: white;
|
|
878
|
-
font-size: 0.8rem;
|
|
879
|
-
line-height: 1;
|
|
880
|
-
}
|
|
881
|
-
.reader-prose.review-mode :deep([data-src-start]:hover)::before {
|
|
882
|
-
display: flex;
|
|
883
|
-
}
|
|
884
|
-
/* Persistent markers: amber for a block that already has a comment, indigo for
|
|
885
|
-
the block whose composer is currently open. */
|
|
886
|
-
.reader-prose :deep(.cf-commented) {
|
|
887
|
-
background: rgb(234 179 8 / 0.1);
|
|
888
|
-
box-shadow: inset 2px 0 0 rgb(234 179 8 / 0.6);
|
|
889
|
-
}
|
|
890
|
-
.reader-prose :deep(.cf-selected) {
|
|
891
|
-
background: rgb(99 102 241 / 0.12);
|
|
892
|
-
box-shadow: inset 2px 0 0 rgb(99 102 241 / 0.8);
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
/* Styling for the markdown HTML injected via v-html (out of scoped reach without
|
|
896
|
-
:deep), kept close to the inspector's existing prose styling. */
|
|
897
|
-
.reader-prose :deep(p) {
|
|
898
|
-
margin: 0.5rem 0;
|
|
899
|
-
}
|
|
900
|
-
.reader-prose :deep(ul),
|
|
901
|
-
.reader-prose :deep(ol) {
|
|
902
|
-
margin: 0.5rem 0;
|
|
903
|
-
padding-left: 1.25rem;
|
|
904
|
-
}
|
|
905
|
-
.reader-prose :deep(ul) {
|
|
906
|
-
list-style: disc;
|
|
907
|
-
}
|
|
908
|
-
.reader-prose :deep(ol) {
|
|
909
|
-
list-style: decimal;
|
|
910
|
-
}
|
|
911
|
-
.reader-prose :deep(li) {
|
|
912
|
-
margin: 0.2rem 0;
|
|
913
|
-
}
|
|
914
|
-
.reader-prose :deep(strong) {
|
|
915
|
-
font-weight: 600;
|
|
916
|
-
color: rgb(226 232 240);
|
|
917
|
-
}
|
|
918
|
-
.reader-prose :deep(em) {
|
|
919
|
-
font-style: italic;
|
|
920
|
-
}
|
|
921
|
-
.reader-prose :deep(code) {
|
|
922
|
-
border-radius: 0.25rem;
|
|
923
|
-
background: rgb(30 41 59 / 0.8);
|
|
924
|
-
padding: 0.1rem 0.3rem;
|
|
925
|
-
font-family: ui-monospace, monospace;
|
|
926
|
-
font-size: 0.85em;
|
|
927
|
-
color: rgb(199 210 254);
|
|
928
|
-
}
|
|
929
|
-
.reader-prose :deep(pre) {
|
|
930
|
-
margin: 0.6rem 0;
|
|
931
|
-
overflow: auto;
|
|
932
|
-
border-radius: 0.5rem;
|
|
933
|
-
background: rgb(2 6 23 / 0.6);
|
|
934
|
-
padding: 0.75rem 0.9rem;
|
|
935
|
-
}
|
|
936
|
-
.reader-prose :deep(pre code) {
|
|
937
|
-
background: transparent;
|
|
938
|
-
padding: 0;
|
|
939
|
-
color: rgb(203 213 225);
|
|
940
|
-
}
|
|
941
|
-
.reader-prose :deep(blockquote) {
|
|
942
|
-
margin: 0.6rem 0;
|
|
943
|
-
border-left: 3px solid rgb(99 102 241 / 0.5);
|
|
944
|
-
padding-left: 0.75rem;
|
|
945
|
-
color: rgb(148 163 184);
|
|
946
|
-
}
|
|
947
|
-
.reader-prose :deep(table) {
|
|
948
|
-
margin: 0.6rem 0;
|
|
949
|
-
border-collapse: collapse;
|
|
950
|
-
font-size: 0.95em;
|
|
951
|
-
}
|
|
952
|
-
.reader-prose :deep(th),
|
|
953
|
-
.reader-prose :deep(td) {
|
|
954
|
-
border: 1px solid rgb(51 65 85);
|
|
955
|
-
padding: 0.3rem 0.6rem;
|
|
956
|
-
}
|
|
957
|
-
.reader-prose :deep(th) {
|
|
958
|
-
background: rgb(30 41 59 / 0.6);
|
|
959
|
-
font-weight: 600;
|
|
960
|
-
}
|
|
961
|
-
.reader-prose :deep(hr) {
|
|
962
|
-
margin: 1rem 0;
|
|
963
|
-
border: none;
|
|
964
|
-
border-top: 1px solid rgb(51 65 85);
|
|
965
|
-
}
|
|
966
|
-
.reader-prose :deep(h1),
|
|
967
|
-
.reader-prose :deep(h2),
|
|
968
|
-
.reader-prose :deep(h3),
|
|
969
|
-
.reader-prose :deep(h4) {
|
|
970
|
-
margin: 0.6rem 0 0.3rem;
|
|
971
|
-
font-weight: 600;
|
|
972
|
-
color: rgb(226 232 240);
|
|
973
|
-
}
|
|
974
871
|
</style>
|
|
@@ -27,9 +27,15 @@ const busy = ref(false)
|
|
|
27
27
|
const detecting = ref(false)
|
|
28
28
|
const rows = ref<ValidationCheck[]>([])
|
|
29
29
|
const maxAttempts = ref(VALIDATION_DEFAULT_MAX_ATTEMPTS)
|
|
30
|
+
// DEPENDENCY PREPOPULATION: the install run BEFORE the agent's first turn, so it reads a tree
|
|
31
|
+
// whose dependencies are present. Edited beside the checks because it comes from the same
|
|
32
|
+
// per-service row, but it is independent of them — a service may declare only this.
|
|
33
|
+
const dependencyInstall = ref('')
|
|
30
34
|
|
|
31
35
|
const saved = computed(() => store.forBlock(props.block.id))
|
|
32
|
-
const configured = computed(
|
|
36
|
+
const configured = computed(
|
|
37
|
+
() => saved.value.checks.length > 0 || Boolean(saved.value.dependencyInstall),
|
|
38
|
+
)
|
|
33
39
|
const canAdd = computed(() => rows.value.length < VALIDATION_MAX_CHECKS)
|
|
34
40
|
/** A row is only submittable once it has a command; the label falls back to the command. */
|
|
35
41
|
const submittable = computed(() =>
|
|
@@ -46,6 +52,7 @@ watch(
|
|
|
46
52
|
(config) => {
|
|
47
53
|
rows.value = config.checks.map((c) => ({ ...c }))
|
|
48
54
|
maxAttempts.value = config.maxAttempts
|
|
55
|
+
dependencyInstall.value = config.dependencyInstall ?? ''
|
|
49
56
|
},
|
|
50
57
|
{ immediate: true },
|
|
51
58
|
)
|
|
@@ -114,7 +121,13 @@ async function detect() {
|
|
|
114
121
|
}
|
|
115
122
|
const merged = mergeDetectedChecks(rows.value, result.checks, VALIDATION_MAX_CHECKS)
|
|
116
123
|
rows.value = merged.rows
|
|
117
|
-
|
|
124
|
+
// Fill the install only when the operator has not written one. Detection is assistive, and
|
|
125
|
+
// overwriting a hand-tuned install (a workspace filter, an offline flag) with the generic
|
|
126
|
+
// guess is the same failure `mergeDetectedChecks` refuses to make on the check rows.
|
|
127
|
+
const suggestedInstall = result.dependencyInstall?.trim() ?? ''
|
|
128
|
+
const filledInstall = suggestedInstall !== '' && dependencyInstall.value.trim() === ''
|
|
129
|
+
if (filledInstall) dependencyInstall.value = suggestedInstall
|
|
130
|
+
if (merged.added === 0 && !filledInstall) {
|
|
118
131
|
toast.add({
|
|
119
132
|
title: t('inspector.validationChecks.detect.nothingNew'),
|
|
120
133
|
description:
|
|
@@ -128,7 +141,13 @@ async function detect() {
|
|
|
128
141
|
}
|
|
129
142
|
const names = result.ecosystems.map(ecosystemLabel).join(', ')
|
|
130
143
|
toast.add({
|
|
131
|
-
|
|
144
|
+
// An install-only detection fills nothing but the install field, and reporting it as
|
|
145
|
+
// "0 checks added" would read as a failed press on the one repo shape prepopulation is
|
|
146
|
+
// most for (dependencies to install, nothing declared to verify).
|
|
147
|
+
title:
|
|
148
|
+
merged.added === 0
|
|
149
|
+
? t('inspector.validationChecks.detect.installOnly')
|
|
150
|
+
: t('inspector.validationChecks.detect.added', { count: merged.added }, merged.added),
|
|
132
151
|
// Name what was recognised AND what was left out: a cap that silently swallowed a
|
|
133
152
|
// suggestion reads as "that is everything your repo has".
|
|
134
153
|
description: [
|
|
@@ -152,7 +171,12 @@ async function detect() {
|
|
|
152
171
|
async function save() {
|
|
153
172
|
busy.value = true
|
|
154
173
|
try {
|
|
155
|
-
await store.save(
|
|
174
|
+
await store.save(
|
|
175
|
+
props.block.id,
|
|
176
|
+
submittable.value,
|
|
177
|
+
maxAttempts.value,
|
|
178
|
+
dependencyInstall.value.trim() || undefined,
|
|
179
|
+
)
|
|
156
180
|
toast.add({
|
|
157
181
|
title: t('inspector.validationChecks.savedToast'),
|
|
158
182
|
icon: 'i-lucide-check',
|
|
@@ -172,6 +196,7 @@ async function clear() {
|
|
|
172
196
|
try {
|
|
173
197
|
await store.remove(props.block.id)
|
|
174
198
|
rows.value = []
|
|
199
|
+
dependencyInstall.value = ''
|
|
175
200
|
toastDone('clear', noun)
|
|
176
201
|
} catch (e) {
|
|
177
202
|
notifyError(t('inspector.validationChecks.clearFailed'), e)
|
|
@@ -204,6 +229,19 @@ async function clear() {
|
|
|
204
229
|
</template>
|
|
205
230
|
|
|
206
231
|
<div class="space-y-2">
|
|
232
|
+
<UFormField
|
|
233
|
+
:label="t('inspector.validationChecks.dependencyInstall')"
|
|
234
|
+
:hint="t('inspector.validationChecks.dependencyInstallHint')"
|
|
235
|
+
>
|
|
236
|
+
<UInput
|
|
237
|
+
v-model="dependencyInstall"
|
|
238
|
+
placeholder="pnpm install --frozen-lockfile"
|
|
239
|
+
size="sm"
|
|
240
|
+
class="w-full"
|
|
241
|
+
data-testid="validation-dependency-install"
|
|
242
|
+
/>
|
|
243
|
+
</UFormField>
|
|
244
|
+
|
|
207
245
|
<p class="text-[11px] text-slate-500">
|
|
208
246
|
{{ t('inspector.validationChecks.hint') }}
|
|
209
247
|
</p>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The pull request a `review` task is reviewing — its SUBJECT, not its output. Distinct from the
|
|
3
|
+
// Execution panel's "Pull request", which links the PR a run PRODUCED: a review task produces no
|
|
4
|
+
// PR at all, so without this the one thing identifying the task was buried in the folded
|
|
5
|
+
// description text with nothing to click.
|
|
6
|
+
//
|
|
7
|
+
// Reads the task's own fields, so it is there from creation — before any run — and stays there
|
|
8
|
+
// after one. The backend confirms the PR against the provider at create time and records that
|
|
9
|
+
// provider's own link (`prUrl`), which is what makes a plain read enough here; a task created
|
|
10
|
+
// while no VCS was connected keeps only the number, and then the reference reads as text rather
|
|
11
|
+
// than pretending to be a link.
|
|
12
|
+
import { computed } from 'vue'
|
|
13
|
+
import type { Block } from '~/types/domain'
|
|
14
|
+
import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
|
|
15
|
+
|
|
16
|
+
const props = defineProps<{ block: Block }>()
|
|
17
|
+
const { t } = useI18n()
|
|
18
|
+
|
|
19
|
+
const isReview = computed(() => props.block.taskType === 'review')
|
|
20
|
+
const fields = computed(() => props.block.taskTypeFields ?? null)
|
|
21
|
+
const url = computed(() => fields.value?.prUrl?.trim() || '')
|
|
22
|
+
const focus = computed(() => fields.value?.reviewFocus?.trim() || '')
|
|
23
|
+
|
|
24
|
+
/** `#123` when the number is known, else the raw link — never an empty affordance. */
|
|
25
|
+
const label = computed(() => {
|
|
26
|
+
const number = fields.value?.prNumber
|
|
27
|
+
return number ? t('inspector.reviewTarget.prNumber', { number }) : url.value
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
/** Nothing to show when the task carries no reference at all (nothing to link or name). */
|
|
31
|
+
const hasTarget = computed(() => Boolean(label.value))
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<InspectorSection
|
|
36
|
+
v-if="isReview && hasTarget"
|
|
37
|
+
:title="t('inspector.reviewTarget.title')"
|
|
38
|
+
:hint="t('inspector.reviewTarget.hint')"
|
|
39
|
+
icon="i-lucide-git-pull-request-arrow"
|
|
40
|
+
default-open
|
|
41
|
+
>
|
|
42
|
+
<UButton
|
|
43
|
+
v-if="url"
|
|
44
|
+
:to="url"
|
|
45
|
+
target="_blank"
|
|
46
|
+
rel="noopener"
|
|
47
|
+
external
|
|
48
|
+
color="neutral"
|
|
49
|
+
variant="soft"
|
|
50
|
+
size="sm"
|
|
51
|
+
icon="i-lucide-git-pull-request"
|
|
52
|
+
trailing-icon="i-lucide-external-link"
|
|
53
|
+
block
|
|
54
|
+
data-testid="inspector-review-target-link"
|
|
55
|
+
>
|
|
56
|
+
<span class="w-full truncate text-start" :title="url">{{ label }}</span>
|
|
57
|
+
</UButton>
|
|
58
|
+
<p
|
|
59
|
+
v-else
|
|
60
|
+
class="rounded-lg border border-slate-800 bg-slate-900/40 p-2.5 text-xs text-slate-300"
|
|
61
|
+
data-testid="inspector-review-target-link"
|
|
62
|
+
>
|
|
63
|
+
{{ label }}
|
|
64
|
+
</p>
|
|
65
|
+
<p v-if="focus" class="text-xs leading-relaxed text-slate-500">
|
|
66
|
+
{{ t('inspector.reviewTarget.focus', { focus }) }}
|
|
67
|
+
</p>
|
|
68
|
+
</InspectorSection>
|
|
69
|
+
</template>
|
|
@@ -1228,41 +1228,8 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
1228
1228
|
.pl-5\.5 {
|
|
1229
1229
|
padding-left: 1.375rem;
|
|
1230
1230
|
}
|
|
1231
|
-
/*
|
|
1232
|
-
prose
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
}
|
|
1236
|
-
.reader-prose :deep(ul),
|
|
1237
|
-
.reader-prose :deep(ol) {
|
|
1238
|
-
margin: 0.4rem 0;
|
|
1239
|
-
padding-left: 1.25rem;
|
|
1240
|
-
list-style: revert;
|
|
1241
|
-
}
|
|
1242
|
-
.reader-prose :deep(li) {
|
|
1243
|
-
margin: 0.2rem 0;
|
|
1244
|
-
}
|
|
1245
|
-
.reader-prose :deep(strong) {
|
|
1246
|
-
color: rgb(226 232 240);
|
|
1247
|
-
font-weight: 600;
|
|
1248
|
-
}
|
|
1249
|
-
.reader-prose :deep(code) {
|
|
1250
|
-
border-radius: 0.25rem;
|
|
1251
|
-
background: rgb(2 6 23 / 0.6);
|
|
1252
|
-
padding: 0.05rem 0.3rem;
|
|
1253
|
-
font-size: 0.85em;
|
|
1254
|
-
}
|
|
1255
|
-
.reader-prose :deep(pre) {
|
|
1256
|
-
margin: 0.5rem 0;
|
|
1257
|
-
overflow-x: auto;
|
|
1258
|
-
border-radius: 0.5rem;
|
|
1259
|
-
background: rgb(2 6 23 / 0.6);
|
|
1260
|
-
padding: 0.75rem;
|
|
1261
|
-
}
|
|
1262
|
-
.reader-prose :deep(blockquote) {
|
|
1263
|
-
margin: 0.5rem 0;
|
|
1264
|
-
border-left: 2px solid rgb(51 65 85);
|
|
1265
|
-
padding-left: 0.75rem;
|
|
1266
|
-
color: rgb(148 163 184);
|
|
1267
|
-
}
|
|
1231
|
+
/* The rendered-markdown presentation is the SHARED global `.reader-prose` sheet
|
|
1232
|
+
(`assets/css/prose.css`), not a local copy: this reader shows the same agent-authored
|
|
1233
|
+
markdown the step reader does, and a per-window duplicate is how the review surfaces
|
|
1234
|
+
drift apart one property at a time. */
|
|
1268
1235
|
</style>
|