@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.
Files changed (33) hide show
  1. package/README.md +4 -1
  2. package/app/assets/css/main.css +2 -0
  3. package/app/assets/css/prose.css +135 -0
  4. package/app/components/board/AddTaskModal.vue +31 -1
  5. package/app/components/brainstorm/BrainstormWindow.vue +4 -37
  6. package/app/components/clarity/ClarityReviewWindow.vue +4 -37
  7. package/app/components/fragments/FragmentLibraryManager.vue +39 -2
  8. package/app/components/initiative/InitiativePlanReview.vue +351 -0
  9. package/app/components/initiative/InitiativeTrackerWindow.vue +12 -126
  10. package/app/components/panels/AgentStepDetail.vue +19 -122
  11. package/app/components/panels/inspector/TaskReviewTarget.vue +69 -0
  12. package/app/components/requirements/RequirementsReviewWindow.vue +4 -37
  13. package/app/composables/useProseComments.ts +126 -0
  14. package/app/composables/useStepApproval.ts +29 -85
  15. package/app/composables/useStepProse.spec.ts +67 -0
  16. package/app/composables/useStepProse.ts +22 -10
  17. package/app/modular/nav-contributions.spec.ts +3 -13
  18. package/app/modular/panels/inspector.logic.spec.ts +7 -0
  19. package/app/modular/panels/inspector.logic.ts +5 -0
  20. package/app/modular/panels/inspector.ts +2 -0
  21. package/app/stores/execution.ts +9 -0
  22. package/app/utils/initiative.spec.ts +24 -0
  23. package/i18n/locales/de.json +16 -3
  24. package/i18n/locales/en.json +17 -3
  25. package/i18n/locales/es.json +16 -3
  26. package/i18n/locales/fr.json +16 -3
  27. package/i18n/locales/he.json +16 -3
  28. package/i18n/locales/it.json +16 -3
  29. package/i18n/locales/ja.json +16 -3
  30. package/i18n/locales/pl.json +16 -3
  31. package/i18n/locales/tr.json +16 -3
  32. package/i18n/locales/uk.json +16 -3
  33. 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, watch } from 'vue'
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: the plan below is drafted but NOT yet committed. This
298
- is the only surface that can resolve it (the generic approval panel is never
299
- reached — the planner's archetype routes its park to this window), so approve /
300
- request changes live here, beside the plan they judge. -->
301
- <section
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
+ itand 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
- class="mb-4 rounded-lg border border-amber-500/40 bg-amber-500/10 p-3.5"
304
- data-testid="initiative-plan-review"
305
- >
306
- <div class="flex items-start gap-2.5">
307
- <UIcon
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,