@cat-factory/app 0.185.0 → 0.187.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/fragments/FragmentLibraryManager.vue +39 -2
- 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/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/utils/initiative.spec.ts +24 -0
- package/i18n/locales/de.json +16 -3
- package/i18n/locales/en.json +17 -3
- package/i18n/locales/es.json +16 -3
- package/i18n/locales/fr.json +16 -3
- package/i18n/locales/he.json +16 -3
- package/i18n/locales/it.json +16 -3
- package/i18n/locales/ja.json +16 -3
- package/i18n/locales/pl.json +16 -3
- package/i18n/locales/tr.json +16 -3
- package/i18n/locales/uk.json +16 -3
- package/package.json +2 -2
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The planner's human gate, reviewed on the PLAN — the tracker window's plan-approval rail.
|
|
3
|
+
//
|
|
4
|
+
// The rail this replaces could approve or send back, but it judged nothing: the plan was a wall
|
|
5
|
+
// of structured sections above it, with no way to navigate a long one and no way to say WHICH
|
|
6
|
+
// part needed changing. So it renders the plan as the document it is — the engine puts a markdown
|
|
7
|
+
// rendering of the INGESTED plan on the gate's proposal (`renderInitiativePlanForReview`, authored
|
|
8
|
+
// by the planner's step resolver, which is the only thing that knows what ingest committed) — and
|
|
9
|
+
// gives it the same three tools the step reader gives the architect's prose: an outline to
|
|
10
|
+
// navigate by, click-to-comment on any block, and overall feedback. Everything here is shared with
|
|
11
|
+
// that reader rather than re-implemented: `useStepProse` for the outline/collapse/scroll-spy,
|
|
12
|
+
// `useProseComments` for the anchoring, and the global `.reader-prose` sheet for the presentation,
|
|
13
|
+
// so the surfaces cannot drift.
|
|
14
|
+
//
|
|
15
|
+
// Deliberately NOT offered here: "approve with corrections". The plan was ingested into the
|
|
16
|
+
// `initiatives` entity before this gate was raised, so the document is a VIEW of committed state
|
|
17
|
+
// — an edit typed over it would reach nothing, and the engine refuses it outright
|
|
18
|
+
// (`outputIsRendered` → 422). Requesting changes is the route for a correction, which is why a
|
|
19
|
+
// comment is worth anchoring: it quotes the planner's own text back to it on the re-plan.
|
|
20
|
+
import { computed, ref, watch } from 'vue'
|
|
21
|
+
import type { StepApproval } from '~/types/execution'
|
|
22
|
+
import { useStepProse } from '~/composables/useStepProse'
|
|
23
|
+
import { useProseComments } from '~/composables/useProseComments'
|
|
24
|
+
|
|
25
|
+
const props = defineProps<{
|
|
26
|
+
/** The parked gate: its `proposal` is the rendered plan document under review. */
|
|
27
|
+
approval: StepApproval
|
|
28
|
+
/** The run the gate belongs to, for the approve / request-changes commands. */
|
|
29
|
+
instanceId: string
|
|
30
|
+
/** Whether the viewer may resolve runs at all (RBAC); false renders the actions disabled. */
|
|
31
|
+
canExecute: boolean
|
|
32
|
+
/**
|
|
33
|
+
* Whether the proposal IS the plan rendering (the step's `outputIsRendered`). Read rather
|
|
34
|
+
* than inferred from the proposal being non-empty: a run planned before the engine rendered
|
|
35
|
+
* plans parks on the planner's transcript summary, which is a perfectly non-empty string —
|
|
36
|
+
* so an emptiness check would show that one sentence under a table of contents as if it were
|
|
37
|
+
* the plan, which is the failure this whole surface exists to end.
|
|
38
|
+
*/
|
|
39
|
+
outputIsRendered: boolean
|
|
40
|
+
}>()
|
|
41
|
+
|
|
42
|
+
const execution = useExecutionStore()
|
|
43
|
+
const { t } = useI18n()
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The plan document under review — the gate's proposal, but ONLY once the step says that
|
|
47
|
+
* proposal is the plan rendering. Not named `document`: that shadows the global in a template
|
|
48
|
+
* expression. Anything else (a run planned before the engine rendered plans, which parks on the
|
|
49
|
+
* planner's transcript summary) reads as no document, and the `noDocument` notice says so rather
|
|
50
|
+
* than dressing a stray sentence up as the plan.
|
|
51
|
+
*/
|
|
52
|
+
const planDocument = computed(() => (props.outputIsRendered ? (props.approval.proposal ?? '') : ''))
|
|
53
|
+
|
|
54
|
+
// The outline + collapse + scroll-spy, exactly as the step reader resolves them — minus its
|
|
55
|
+
// lead anchor: the reader renders a details card ahead of the prose and this rail renders the
|
|
56
|
+
// document alone, and the spy stops at the first anchor it cannot measure.
|
|
57
|
+
const prose = useStepProse(() => planDocument.value, { leadAnchorId: null })
|
|
58
|
+
const {
|
|
59
|
+
outline,
|
|
60
|
+
tocSections,
|
|
61
|
+
hasOutput,
|
|
62
|
+
collapsed,
|
|
63
|
+
activeId,
|
|
64
|
+
// The reader's own scroll container, bound straight through so the shared scroll-spy and the
|
|
65
|
+
// comment-highlight sync read the same element the template scrolls.
|
|
66
|
+
scrollEl,
|
|
67
|
+
sectionEls,
|
|
68
|
+
toggle,
|
|
69
|
+
goTo,
|
|
70
|
+
onScroll,
|
|
71
|
+
} = prose
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Per-block comment drafts over the plan, anchored to its source lines. Commenting follows the
|
|
75
|
+
* same RBAC gate as the actions: a viewer who cannot resolve the run cannot send the comments
|
|
76
|
+
* anywhere, so offering the composer would only invite work the Send-back button then refuses.
|
|
77
|
+
*/
|
|
78
|
+
const {
|
|
79
|
+
comments: planComments,
|
|
80
|
+
wireComments,
|
|
81
|
+
draftTarget,
|
|
82
|
+
draftBody,
|
|
83
|
+
onProseClick,
|
|
84
|
+
addDraftComment,
|
|
85
|
+
cancelDraft,
|
|
86
|
+
removeComment,
|
|
87
|
+
reset: resetComments,
|
|
88
|
+
} = useProseComments({
|
|
89
|
+
output: () => planDocument.value,
|
|
90
|
+
root: () => scrollEl.value,
|
|
91
|
+
enabled: () => props.canExecute,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const feedback = ref('')
|
|
95
|
+
const submitting = ref(false)
|
|
96
|
+
|
|
97
|
+
/** Changes can only be requested with something to act on — an empty send would re-plan blind. */
|
|
98
|
+
const canRequestChanges = computed(() => !!feedback.value.trim() || planComments.value.length > 0)
|
|
99
|
+
|
|
100
|
+
/** A fresh gate (a re-plan parked again) drops the drafts rather than carrying them over. */
|
|
101
|
+
watch(
|
|
102
|
+
() => props.approval.id,
|
|
103
|
+
() => {
|
|
104
|
+
resetComments()
|
|
105
|
+
feedback.value = ''
|
|
106
|
+
prose.reset()
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Accept the plan: the run advances to the committer, which persists it and arms the execution
|
|
112
|
+
* loop. The window stays open — the rail disappears with the approval (live) and the tracker is
|
|
113
|
+
* where the plan then executes.
|
|
114
|
+
*/
|
|
115
|
+
async function approve() {
|
|
116
|
+
if (submitting.value || !props.canExecute) return
|
|
117
|
+
submitting.value = true
|
|
118
|
+
try {
|
|
119
|
+
await execution.approveStep(props.instanceId, props.approval.id)
|
|
120
|
+
} finally {
|
|
121
|
+
submitting.value = false
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Send the plan back: the planner re-plans from the feedback + the anchored comments. */
|
|
126
|
+
async function requestChanges() {
|
|
127
|
+
if (submitting.value || !canRequestChanges.value || !props.canExecute) return
|
|
128
|
+
submitting.value = true
|
|
129
|
+
try {
|
|
130
|
+
const ok = await execution.requestStepChanges(props.instanceId, props.approval.id, {
|
|
131
|
+
feedback: feedback.value.trim() || undefined,
|
|
132
|
+
comments: wireComments.value,
|
|
133
|
+
})
|
|
134
|
+
if (ok) {
|
|
135
|
+
feedback.value = ''
|
|
136
|
+
resetComments()
|
|
137
|
+
}
|
|
138
|
+
} finally {
|
|
139
|
+
submitting.value = false
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const disabledTitle = computed(() => (props.canExecute ? undefined : t('access.noRunExecute')))
|
|
144
|
+
</script>
|
|
145
|
+
|
|
146
|
+
<template>
|
|
147
|
+
<section
|
|
148
|
+
class="mb-4 rounded-lg border border-amber-500/40 bg-amber-500/10"
|
|
149
|
+
data-testid="initiative-plan-review"
|
|
150
|
+
>
|
|
151
|
+
<header class="flex items-start gap-2.5 px-3.5 pt-3.5">
|
|
152
|
+
<UIcon name="i-lucide-clipboard-check" class="mt-0.5 h-4 w-4 shrink-0 text-amber-300" />
|
|
153
|
+
<div class="min-w-0 flex-1">
|
|
154
|
+
<h3 class="text-[13px] font-semibold text-amber-200">
|
|
155
|
+
{{ t('initiative.planReview.title') }}
|
|
156
|
+
</h3>
|
|
157
|
+
<p class="mt-0.5 text-[12px] leading-relaxed text-amber-100/80">
|
|
158
|
+
{{ t('initiative.planReview.body') }}
|
|
159
|
+
</p>
|
|
160
|
+
</div>
|
|
161
|
+
</header>
|
|
162
|
+
|
|
163
|
+
<!-- The plan document, shown only when the gate's proposal actually IS the plan rendering
|
|
164
|
+
(`outputIsRendered`). A run planned before the engine rendered plans parks on the
|
|
165
|
+
planner's transcript summary instead, so it takes the notice below — the structured
|
|
166
|
+
sections further down are still the plan in that case. -->
|
|
167
|
+
<div v-if="hasOutput" class="mt-3 flex min-h-0 gap-3 px-3.5">
|
|
168
|
+
<!-- Outline: the navigation the rail had none of. Inline rather than a full sidebar — the
|
|
169
|
+
tracker window already spends its end-side column on run metadata. -->
|
|
170
|
+
<nav
|
|
171
|
+
v-if="outline.hasToc"
|
|
172
|
+
data-testid="initiative-plan-toc"
|
|
173
|
+
class="hidden max-h-80 w-48 shrink-0 space-y-0.5 overflow-y-auto border-e border-amber-500/20 pe-2 md:block"
|
|
174
|
+
>
|
|
175
|
+
<button
|
|
176
|
+
v-for="s in tocSections"
|
|
177
|
+
:key="s.id"
|
|
178
|
+
class="block w-full truncate rounded px-1.5 py-1 text-start text-[12px] transition"
|
|
179
|
+
:class="
|
|
180
|
+
activeId === s.id
|
|
181
|
+
? 'bg-amber-500/20 font-medium text-amber-100'
|
|
182
|
+
: 'text-amber-200/60 hover:bg-amber-500/10 hover:text-amber-100'
|
|
183
|
+
"
|
|
184
|
+
:style="{ paddingLeft: `${(s.depth - outline.minDepth) * 0.7 + 0.4}rem` }"
|
|
185
|
+
:title="s.title"
|
|
186
|
+
@click="goTo(s.id)"
|
|
187
|
+
>
|
|
188
|
+
{{ s.title }}
|
|
189
|
+
</button>
|
|
190
|
+
</nav>
|
|
191
|
+
|
|
192
|
+
<div
|
|
193
|
+
ref="scrollEl"
|
|
194
|
+
data-testid="initiative-plan-document"
|
|
195
|
+
class="max-h-80 min-w-0 flex-1 overflow-y-auto rounded border border-amber-500/20 bg-slate-950/40 p-3"
|
|
196
|
+
@scroll="onScroll"
|
|
197
|
+
>
|
|
198
|
+
<section
|
|
199
|
+
v-for="s in outline.sections"
|
|
200
|
+
:id="s.id"
|
|
201
|
+
:key="s.id"
|
|
202
|
+
:ref="(el) => (sectionEls[s.id] = el as HTMLElement | null)"
|
|
203
|
+
class="scroll-mt-2"
|
|
204
|
+
>
|
|
205
|
+
<button
|
|
206
|
+
v-if="s.depth > 0"
|
|
207
|
+
class="group flex w-full items-center gap-1.5 rounded py-0.5 text-start transition hover:text-white"
|
|
208
|
+
@click="toggle(s.id)"
|
|
209
|
+
>
|
|
210
|
+
<UIcon
|
|
211
|
+
name="i-lucide-chevron-right"
|
|
212
|
+
class="h-3.5 w-3.5 shrink-0 text-slate-500 transition-transform"
|
|
213
|
+
:class="collapsed[s.id] ? '' : 'rotate-90'"
|
|
214
|
+
/>
|
|
215
|
+
<span
|
|
216
|
+
class="font-semibold text-slate-100"
|
|
217
|
+
:class="s.depth <= 1 ? 'text-sm' : 'text-[13px]'"
|
|
218
|
+
v-html="s.titleHtml"
|
|
219
|
+
/>
|
|
220
|
+
</button>
|
|
221
|
+
<!-- `review-mode` carries the click-to-comment affordance, so it tracks the same RBAC
|
|
222
|
+
gate the composer does — a viewer gets the document, not hover targets that lead
|
|
223
|
+
nowhere. -->
|
|
224
|
+
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
225
|
+
<div
|
|
226
|
+
v-show="!collapsed[s.id]"
|
|
227
|
+
class="reader-prose mt-0.5 text-[12px] leading-relaxed text-slate-300"
|
|
228
|
+
:class="[s.depth > 0 ? 'ps-5' : '', canExecute ? 'review-mode' : '']"
|
|
229
|
+
@click="onProseClick"
|
|
230
|
+
v-html="s.bodyHtml"
|
|
231
|
+
/>
|
|
232
|
+
</section>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
<p v-else class="mt-2 px-3.5 text-[12px] text-amber-100/70">
|
|
236
|
+
{{ t('initiative.planReview.noDocument') }}
|
|
237
|
+
</p>
|
|
238
|
+
|
|
239
|
+
<!-- Comment composer for the block just clicked, then the anchored comments so far. -->
|
|
240
|
+
<div v-if="draftTarget" class="mt-3 px-3.5" data-testid="initiative-plan-composer">
|
|
241
|
+
<div class="rounded-lg border border-indigo-500/40 bg-indigo-500/5 p-2.5">
|
|
242
|
+
<div class="mb-1 text-[10px] uppercase tracking-wide text-indigo-300">
|
|
243
|
+
{{ t('panels.stepDetail.commentingOn') }}
|
|
244
|
+
</div>
|
|
245
|
+
<pre
|
|
246
|
+
class="mb-2 max-h-20 overflow-auto whitespace-pre-wrap rounded bg-slate-950/60 p-1.5 text-[11px] text-slate-300"
|
|
247
|
+
>{{ draftTarget.quotedSource }}</pre>
|
|
248
|
+
<UTextarea
|
|
249
|
+
v-model="draftBody"
|
|
250
|
+
data-testid="initiative-plan-comment-body"
|
|
251
|
+
:rows="2"
|
|
252
|
+
autoresize
|
|
253
|
+
size="sm"
|
|
254
|
+
class="w-full"
|
|
255
|
+
:placeholder="t('panels.stepDetail.commentPlaceholder')"
|
|
256
|
+
/>
|
|
257
|
+
<div class="mt-2 flex justify-end gap-2">
|
|
258
|
+
<UButton color="neutral" variant="ghost" size="xs" @click="cancelDraft">
|
|
259
|
+
{{ t('common.cancel') }}
|
|
260
|
+
</UButton>
|
|
261
|
+
<UButton
|
|
262
|
+
color="primary"
|
|
263
|
+
size="xs"
|
|
264
|
+
data-testid="initiative-plan-comment-add"
|
|
265
|
+
:disabled="!draftBody.trim()"
|
|
266
|
+
@click="addDraftComment"
|
|
267
|
+
>
|
|
268
|
+
{{ t('panels.stepDetail.addComment') }}
|
|
269
|
+
</UButton>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<ul v-if="planComments.length" class="mt-3 space-y-2 px-3.5">
|
|
275
|
+
<li
|
|
276
|
+
v-for="(c, idx) in planComments"
|
|
277
|
+
:key="idx"
|
|
278
|
+
data-testid="initiative-plan-comment"
|
|
279
|
+
class="rounded-lg border border-slate-700 bg-slate-900/50 p-2.5"
|
|
280
|
+
>
|
|
281
|
+
<div class="mb-1 flex items-start justify-between gap-2">
|
|
282
|
+
<div class="text-[10px] uppercase tracking-wide text-slate-500">
|
|
283
|
+
{{ t('panels.stepDetail.commentN', { number: idx + 1 }) }}
|
|
284
|
+
</div>
|
|
285
|
+
<button
|
|
286
|
+
class="text-slate-500 transition hover:text-rose-400"
|
|
287
|
+
:title="t('panels.stepDetail.removeComment')"
|
|
288
|
+
@click="removeComment(idx)"
|
|
289
|
+
>
|
|
290
|
+
<UIcon name="i-lucide-x" class="h-3.5 w-3.5" />
|
|
291
|
+
</button>
|
|
292
|
+
</div>
|
|
293
|
+
<pre
|
|
294
|
+
class="mb-1 max-h-16 overflow-auto whitespace-pre-wrap rounded bg-slate-950/50 p-1.5 text-[10px] text-slate-400"
|
|
295
|
+
>{{ c.quotedSource }}</pre>
|
|
296
|
+
<p class="text-[12px] text-slate-200">{{ c.body }}</p>
|
|
297
|
+
</li>
|
|
298
|
+
</ul>
|
|
299
|
+
|
|
300
|
+
<!-- Overall feedback + the two actions. Feedback stays visible rather than hiding behind a
|
|
301
|
+
"request changes" step: with per-block comments in play the human is already composing a
|
|
302
|
+
review, and a hidden field reads as "there is nothing more to say". -->
|
|
303
|
+
<div class="mt-3 px-3.5 pb-3.5">
|
|
304
|
+
<UTextarea
|
|
305
|
+
v-model="feedback"
|
|
306
|
+
data-testid="initiative-plan-feedback"
|
|
307
|
+
:rows="2"
|
|
308
|
+
autoresize
|
|
309
|
+
size="sm"
|
|
310
|
+
class="w-full"
|
|
311
|
+
:placeholder="t('initiative.planReview.feedbackPlaceholder')"
|
|
312
|
+
/>
|
|
313
|
+
<div class="mt-2 flex flex-wrap items-center gap-2">
|
|
314
|
+
<UButton
|
|
315
|
+
color="primary"
|
|
316
|
+
size="xs"
|
|
317
|
+
icon="i-lucide-check"
|
|
318
|
+
data-testid="initiative-plan-approve"
|
|
319
|
+
:loading="submitting"
|
|
320
|
+
:disabled="!canExecute"
|
|
321
|
+
:title="disabledTitle"
|
|
322
|
+
@click="approve"
|
|
323
|
+
>
|
|
324
|
+
{{ t('initiative.planReview.approve') }}
|
|
325
|
+
</UButton>
|
|
326
|
+
<UButton
|
|
327
|
+
color="warning"
|
|
328
|
+
variant="soft"
|
|
329
|
+
size="xs"
|
|
330
|
+
icon="i-lucide-rotate-ccw"
|
|
331
|
+
data-testid="initiative-plan-send-back"
|
|
332
|
+
:loading="submitting"
|
|
333
|
+
:disabled="!canRequestChanges || !canExecute"
|
|
334
|
+
:title="
|
|
335
|
+
canExecute && !canRequestChanges
|
|
336
|
+
? t('initiative.planReview.needsFeedback')
|
|
337
|
+
: disabledTitle
|
|
338
|
+
"
|
|
339
|
+
@click="requestChanges"
|
|
340
|
+
>
|
|
341
|
+
{{ t('initiative.planReview.sendBack') }}
|
|
342
|
+
</UButton>
|
|
343
|
+
<!-- Only worth saying where clicking a block does something (a document + the RBAC to
|
|
344
|
+
act on it). -->
|
|
345
|
+
<p v-if="hasOutput && canExecute" class="text-[10px] text-amber-100/60">
|
|
346
|
+
{{ t('initiative.planReview.commentHint') }}
|
|
347
|
+
</p>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
</section>
|
|
351
|
+
</template>
|
|
@@ -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,
|