@cat-factory/app 0.194.0 → 0.195.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/initiative/InitiativePlanDecision.vue +134 -0
- package/app/components/initiative/InitiativePlanNotice.vue +69 -0
- package/app/components/initiative/InitiativePlanReview.vue +278 -254
- package/app/components/initiative/InitiativeTrackerWindow.vue +72 -25
- package/app/components/pipeline/AgentPromptEditor.vue +40 -0
- package/app/components/pipeline/OutputBudgetInput.vue +69 -0
- package/app/components/pipeline/PipelineBuilder.vue +42 -0
- package/app/composables/api/agentSettings.ts +32 -0
- package/app/composables/useApi.ts +2 -0
- package/app/stores/agentSettings.ts +86 -0
- package/app/stores/outputBudget.spec.ts +55 -0
- package/app/stores/pipelines/draftStepConfig.ts +23 -0
- package/app/types/agent-settings.ts +12 -0
- package/app/utils/initiative.spec.ts +43 -0
- package/app/utils/initiative.ts +23 -0
- package/i18n/locales/de.json +12 -3
- package/i18n/locales/en.json +12 -3
- package/i18n/locales/es.json +12 -3
- package/i18n/locales/fr.json +12 -3
- package/i18n/locales/he.json +12 -3
- package/i18n/locales/it.json +12 -3
- package/i18n/locales/ja.json +12 -3
- package/i18n/locales/pl.json +12 -3
- package/i18n/locales/tr.json +12 -3
- package/i18n/locales/uk.json +12 -3
- package/package.json +2 -2
|
@@ -1,79 +1,76 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// The planner's human gate, reviewed on the PLAN — the tracker window
|
|
2
|
+
// The planner's human gate, reviewed on the PLAN — the surface the tracker window hands its WHOLE
|
|
3
|
+
// body to while a rendered plan is parked.
|
|
3
4
|
//
|
|
4
|
-
// The rail this replaces could approve or send back, but it judged nothing: the plan was a wall
|
|
5
|
-
//
|
|
6
|
-
//
|
|
5
|
+
// The rail this replaces could approve or send back, but it judged nothing: the plan was a wall of
|
|
6
|
+
// structured sections above it, with no way to navigate a long one and no way to say WHICH part
|
|
7
|
+
// needed changing. So it renders the plan as the document it is — the engine puts a markdown
|
|
7
8
|
// rendering of the INGESTED plan on the gate's proposal (`renderInitiativePlanForReview`, authored
|
|
8
|
-
// by the planner's step resolver,
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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.
|
|
9
|
+
// by the planner's step resolver, the only thing that knows what ingest committed) — and gives it
|
|
10
|
+
// the same three tools the step reader gives the architect's prose: an outline to navigate by,
|
|
11
|
+
// click-to-comment on any block, and overall feedback.
|
|
14
12
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
13
|
+
// The LAYOUT is the step reader's too, and for the same reasons (`AgentStepDetail.vue`). The first
|
|
14
|
+
// cut of this surface was a card INSIDE the tracker's scrolling column: outline and document split
|
|
15
|
+
// that column's width between them, the document capped at a 20rem window, and the tracker's own
|
|
16
|
+
// goal / phases / policy / logs sections — the very same plan, since the render reads the ingested
|
|
17
|
+
// entity — repeated underneath. Reviewers scrolled a letterbox while a second copy of what they
|
|
18
|
+
// were reading sat below it. Now the outline is a sidebar OUTSIDE the document, the document takes
|
|
19
|
+
// the full height of the window, the commands sit in an end-side rail, and the duplicate is gone
|
|
20
|
+
// because the window renders this INSTEAD of the tracker while the gate is parked (the tracker's
|
|
21
|
+
// remaining panels — PR links, curation, checkpoints, follow-ups — are all execution-time state
|
|
22
|
+
// that cannot exist yet at plan time).
|
|
23
|
+
//
|
|
24
|
+
// Everything under the layout is shared with that reader rather than re-implemented: `useStepProse`
|
|
25
|
+
// for the outline/collapse/scroll-spy, `useProseComments` for the anchoring, `InitiativePlanDecision`
|
|
26
|
+
// for the two commands, and the global `.reader-prose` sheet for the presentation — so the surfaces
|
|
27
|
+
// cannot drift.
|
|
28
|
+
import { ref, watch } from 'vue'
|
|
21
29
|
import type { StepApproval } from '~/types/execution'
|
|
22
30
|
import { useStepProse } from '~/composables/useStepProse'
|
|
23
31
|
import { useProseComments } from '~/composables/useProseComments'
|
|
32
|
+
import InitiativePlanDecision from '~/components/initiative/InitiativePlanDecision.vue'
|
|
24
33
|
|
|
25
34
|
const props = defineProps<{
|
|
26
|
-
/** The parked gate
|
|
35
|
+
/** The parked gate under review. */
|
|
27
36
|
approval: StepApproval
|
|
28
37
|
/** The run the gate belongs to, for the approve / request-changes commands. */
|
|
29
38
|
instanceId: string
|
|
30
39
|
/** Whether the viewer may resolve runs at all (RBAC); false renders the actions disabled. */
|
|
31
40
|
canExecute: boolean
|
|
32
41
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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.
|
|
42
|
+
* The rendered plan under review, resolved by the host through `planReviewDocument` — the same
|
|
43
|
+
* value that decided to mount THIS surface rather than the notice, so it is non-empty here.
|
|
38
44
|
*/
|
|
39
|
-
|
|
45
|
+
planDocument: string
|
|
40
46
|
}>()
|
|
41
47
|
|
|
42
|
-
const execution = useExecutionStore()
|
|
43
48
|
const { t } = useI18n()
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 })
|
|
50
|
+
// The outline + collapse + scroll-spy, exactly as the step reader resolves them — minus its lead
|
|
51
|
+
// anchor: the reader renders a details card ahead of the prose, while here the run details sit in
|
|
52
|
+
// the sidebar (never scrolled past), and the spy stops at the first anchor it cannot measure.
|
|
53
|
+
const prose = useStepProse(() => props.planDocument, { leadAnchorId: null })
|
|
58
54
|
const {
|
|
59
55
|
outline,
|
|
60
56
|
tocSections,
|
|
61
|
-
hasOutput,
|
|
62
57
|
collapsed,
|
|
63
58
|
activeId,
|
|
64
|
-
// The
|
|
65
|
-
//
|
|
59
|
+
// The scroll container, bound straight through so the shared scroll-spy and the comment-highlight
|
|
60
|
+
// sync read the same element the template scrolls.
|
|
66
61
|
scrollEl,
|
|
67
62
|
sectionEls,
|
|
68
63
|
toggle,
|
|
64
|
+
setAll,
|
|
65
|
+
allCollapsed,
|
|
69
66
|
goTo,
|
|
70
67
|
onScroll,
|
|
71
68
|
} = prose
|
|
72
69
|
|
|
73
70
|
/**
|
|
74
|
-
* Per-block comment drafts over the plan, anchored to its source lines. Commenting follows the
|
|
75
|
-
*
|
|
76
|
-
*
|
|
71
|
+
* Per-block comment drafts over the plan, anchored to its source lines. Commenting follows the same
|
|
72
|
+
* RBAC gate as the commands: a viewer who cannot resolve the run cannot send the comments anywhere,
|
|
73
|
+
* so offering the composer would only invite work the Send-back button then refuses.
|
|
77
74
|
*/
|
|
78
75
|
const {
|
|
79
76
|
comments: planComments,
|
|
@@ -86,266 +83,293 @@ const {
|
|
|
86
83
|
removeComment,
|
|
87
84
|
reset: resetComments,
|
|
88
85
|
} = useProseComments({
|
|
89
|
-
output: () => planDocument
|
|
86
|
+
output: () => props.planDocument,
|
|
90
87
|
root: () => scrollEl.value,
|
|
91
88
|
enabled: () => props.canExecute,
|
|
92
89
|
})
|
|
93
90
|
|
|
94
|
-
|
|
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. */
|
|
91
|
+
/** A fresh gate (a re-plan parked again) reviews the new plan clean, from the top. */
|
|
101
92
|
watch(
|
|
102
93
|
() => props.approval.id,
|
|
103
94
|
() => {
|
|
104
95
|
resetComments()
|
|
105
|
-
feedback.value = ''
|
|
106
96
|
prose.reset()
|
|
107
97
|
},
|
|
108
98
|
)
|
|
109
99
|
|
|
110
|
-
/**
|
|
111
|
-
|
|
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
|
-
}
|
|
100
|
+
/** Whether the sidebar's run-details stack is expanded (it is, until a reviewer wants the outline). */
|
|
101
|
+
const runDetailsOpen = ref(true)
|
|
124
102
|
|
|
125
|
-
|
|
126
|
-
async function
|
|
127
|
-
|
|
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
|
-
}
|
|
103
|
+
const { copy } = useCopyToClipboard()
|
|
104
|
+
async function copyPlan() {
|
|
105
|
+
await copy(props.planDocument)
|
|
141
106
|
}
|
|
142
|
-
|
|
143
|
-
const disabledTitle = computed(() => (props.canExecute ? undefined : t('access.noRunExecute')))
|
|
144
107
|
</script>
|
|
145
108
|
|
|
146
109
|
<template>
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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>
|
|
110
|
+
<div class="flex min-h-0 flex-1 flex-col lg:flex-row" data-testid="initiative-plan-review">
|
|
111
|
+
<!-- Navigation column: the outline OUTSIDE the document rather than splitting its width — a
|
|
112
|
+
sidebar of the window, as the step reader's is. Narrower than the reader's (`w-52` against
|
|
113
|
+
its `w-72`) and held back to `lg` rather than its `md`, because this is a THREE-column
|
|
114
|
+
layout: at 768px the document would be left ~240px between the outline and the review rail,
|
|
115
|
+
which reads worse than no outline at all. Below `lg` the whole column goes; the document and
|
|
116
|
+
the commands are what a narrow screen needs.
|
|
162
117
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
118
|
+
Its presence tracks its two contents INDEPENDENTLY rather than the outline alone. Gating the
|
|
119
|
+
run details on `outline.hasToc` would make whether this window still reports its model / run
|
|
120
|
+
id / token spend depend on whether the plan renderer happened to emit a heading — a fact
|
|
121
|
+
owned by `renderInitiativePlanForReview`, in another package, with nothing pinning it. The
|
|
122
|
+
step reader keeps the same document-level affordances out of its own `hasToc` guard for that
|
|
123
|
+
reason, in a main-column header this surface does not have. -->
|
|
124
|
+
<aside
|
|
125
|
+
v-if="outline.hasToc || $slots['run-details']"
|
|
126
|
+
class="hidden w-52 shrink-0 flex-col border-e border-slate-800 bg-slate-900/60 lg:flex"
|
|
127
|
+
>
|
|
128
|
+
<div class="flex items-center gap-0.5 border-b border-slate-800 px-3 py-2">
|
|
129
|
+
<span
|
|
130
|
+
v-if="outline.hasToc"
|
|
131
|
+
class="min-w-0 flex-1 truncate text-[11px] font-semibold uppercase tracking-wide text-slate-500"
|
|
132
|
+
>
|
|
133
|
+
{{ t('panels.stepDetail.contents') }}
|
|
134
|
+
</span>
|
|
135
|
+
<span v-else class="flex-1" />
|
|
136
|
+
<!-- Collapse-all tracks the outline: with no headings the only section is the untitled
|
|
137
|
+
preamble, which renders no toggle of its own, so collapsing it would hide the whole
|
|
138
|
+
plan with nothing on screen to bring it back. Copying it does not — that is about the
|
|
139
|
+
document, which exists either way. -->
|
|
140
|
+
<UButton
|
|
141
|
+
v-if="outline.hasToc"
|
|
142
|
+
:icon="allCollapsed ? 'i-lucide-unfold-vertical' : 'i-lucide-fold-vertical'"
|
|
143
|
+
color="neutral"
|
|
144
|
+
variant="ghost"
|
|
145
|
+
size="xs"
|
|
146
|
+
:title="
|
|
147
|
+
allCollapsed ? t('panels.stepDetail.expandAll') : t('panels.stepDetail.collapseAll')
|
|
148
|
+
"
|
|
149
|
+
@click="setAll(!allCollapsed)"
|
|
150
|
+
/>
|
|
151
|
+
<UButton
|
|
152
|
+
icon="i-lucide-copy"
|
|
153
|
+
color="neutral"
|
|
154
|
+
variant="ghost"
|
|
155
|
+
size="xs"
|
|
156
|
+
:title="t('panels.stepDetail.copyRawOutput')"
|
|
157
|
+
@click="copyPlan"
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
170
160
|
<nav
|
|
171
161
|
v-if="outline.hasToc"
|
|
172
162
|
data-testid="initiative-plan-toc"
|
|
173
|
-
|
|
163
|
+
:aria-label="t('panels.stepDetail.contents')"
|
|
164
|
+
class="flex-1 space-y-0.5 overflow-y-auto px-2 py-2"
|
|
174
165
|
>
|
|
175
166
|
<button
|
|
176
167
|
v-for="s in tocSections"
|
|
177
168
|
:key="s.id"
|
|
178
|
-
class="block w-full truncate rounded px-
|
|
169
|
+
class="block w-full truncate rounded-md px-2 py-1 text-start text-[12px] transition"
|
|
179
170
|
:class="
|
|
180
171
|
activeId === s.id
|
|
181
|
-
? 'bg-amber-500/
|
|
182
|
-
: 'text-
|
|
172
|
+
? 'bg-amber-500/15 font-medium text-amber-100'
|
|
173
|
+
: 'text-slate-400 hover:bg-slate-800/60 hover:text-slate-200'
|
|
183
174
|
"
|
|
184
|
-
:style="{ paddingLeft: `${(s.depth - outline.minDepth) * 0.7 + 0.
|
|
175
|
+
:style="{ paddingLeft: `${(s.depth - outline.minDepth) * 0.7 + 0.5}rem` }"
|
|
185
176
|
:title="s.title"
|
|
186
177
|
@click="goTo(s.id)"
|
|
187
178
|
>
|
|
188
179
|
{{ s.title }}
|
|
189
180
|
</button>
|
|
190
181
|
</nav>
|
|
191
|
-
|
|
182
|
+
<!-- Run details (the model, the run id, the token telemetry). Filled by the host, which
|
|
183
|
+
already resolves the bundle through `useResultViewRunMeta`; it keeps its home in a
|
|
184
|
+
sidebar here rather than disappearing for the duration of the review. Open, but
|
|
185
|
+
collapsible: it is a stack of seven labelled fields, and a reviewer navigating a long
|
|
186
|
+
plan should be able to give the outline the whole column. With no outline above it there
|
|
187
|
+
is no column to give back, so it takes the space instead of leaving 55% of it empty. -->
|
|
192
188
|
<div
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
class="
|
|
196
|
-
|
|
189
|
+
v-if="$slots['run-details']"
|
|
190
|
+
class="flex flex-col"
|
|
191
|
+
:class="
|
|
192
|
+
outline.hasToc ? 'max-h-[45%] shrink-0 border-t border-slate-800' : 'min-h-0 flex-1'
|
|
193
|
+
"
|
|
197
194
|
>
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
:
|
|
203
|
-
|
|
195
|
+
<button
|
|
196
|
+
type="button"
|
|
197
|
+
data-testid="initiative-plan-run-meta-toggle"
|
|
198
|
+
class="flex shrink-0 items-center gap-1.5 px-3 py-2 text-start transition hover:bg-slate-800/40"
|
|
199
|
+
:aria-expanded="runDetailsOpen"
|
|
200
|
+
@click="runDetailsOpen = !runDetailsOpen"
|
|
204
201
|
>
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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"
|
|
202
|
+
<span class="flex-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
203
|
+
{{ t('panels.stepDetail.details') }}
|
|
204
|
+
</span>
|
|
205
|
+
<UIcon
|
|
206
|
+
:name="runDetailsOpen ? 'i-lucide-chevron-down' : 'i-lucide-chevron-up'"
|
|
207
|
+
class="h-3.5 w-3.5 shrink-0 text-slate-500"
|
|
231
208
|
/>
|
|
232
|
-
</
|
|
209
|
+
</button>
|
|
210
|
+
<div
|
|
211
|
+
v-if="runDetailsOpen"
|
|
212
|
+
data-testid="initiative-plan-run-meta"
|
|
213
|
+
class="min-h-0 flex-1 overflow-y-auto px-3 pb-3"
|
|
214
|
+
>
|
|
215
|
+
<slot name="run-details" />
|
|
216
|
+
</div>
|
|
233
217
|
</div>
|
|
234
|
-
</
|
|
235
|
-
<p v-else class="mt-2 px-3.5 text-[12px] text-amber-100/70">
|
|
236
|
-
{{ t('initiative.planReview.noDocument') }}
|
|
237
|
-
</p>
|
|
218
|
+
</aside>
|
|
238
219
|
|
|
239
|
-
<!--
|
|
240
|
-
<div
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
220
|
+
<!-- The plan itself, with the full height of the window to be read in. -->
|
|
221
|
+
<div
|
|
222
|
+
ref="scrollEl"
|
|
223
|
+
data-testid="initiative-plan-document"
|
|
224
|
+
class="min-h-0 min-w-0 flex-1 overflow-y-auto px-5 py-4"
|
|
225
|
+
@scroll="onScroll"
|
|
226
|
+
>
|
|
227
|
+
<!-- No `max-w-*` reading measure here: with the outline and the review rail both taking a
|
|
228
|
+
fixed column out of the shell's `5xl`, this one is ~490px wide at every size that renders
|
|
229
|
+
it, so a cap would only ever be dead markup. -->
|
|
230
|
+
<section
|
|
231
|
+
v-for="s in outline.sections"
|
|
232
|
+
:id="s.id"
|
|
233
|
+
:key="s.id"
|
|
234
|
+
:ref="(el) => (sectionEls[s.id] = el as HTMLElement | null)"
|
|
235
|
+
class="scroll-mt-2"
|
|
236
|
+
>
|
|
237
|
+
<button
|
|
238
|
+
v-if="s.depth > 0"
|
|
239
|
+
class="group flex w-full items-center gap-1.5 rounded py-0.5 text-start transition hover:text-white"
|
|
240
|
+
:aria-expanded="!collapsed[s.id]"
|
|
241
|
+
@click="toggle(s.id)"
|
|
242
|
+
>
|
|
243
|
+
<UIcon
|
|
244
|
+
name="i-lucide-chevron-right"
|
|
245
|
+
class="h-3.5 w-3.5 shrink-0 text-slate-500 transition-transform group-hover:text-slate-300"
|
|
246
|
+
:class="collapsed[s.id] ? '' : 'rotate-90'"
|
|
247
|
+
/>
|
|
248
|
+
<span
|
|
249
|
+
class="font-semibold text-slate-100"
|
|
250
|
+
:class="s.depth <= 1 ? 'text-base' : s.depth === 2 ? 'text-sm' : 'text-[13px]'"
|
|
251
|
+
v-html="s.titleHtml"
|
|
252
|
+
/>
|
|
253
|
+
</button>
|
|
254
|
+
<!-- `review-mode` carries the click-to-comment affordance, so it tracks the same RBAC
|
|
255
|
+
gate the composer does — a viewer gets the document, not hover targets that lead
|
|
256
|
+
nowhere. -->
|
|
257
|
+
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
258
|
+
<div
|
|
259
|
+
v-show="!collapsed[s.id]"
|
|
260
|
+
class="reader-prose mt-0.5 text-[13px] leading-relaxed text-slate-300"
|
|
261
|
+
:class="[s.depth > 0 ? 'ps-5' : '', canExecute ? 'review-mode' : '']"
|
|
262
|
+
@click="onProseClick"
|
|
263
|
+
v-html="s.bodyHtml"
|
|
256
264
|
/>
|
|
257
|
-
|
|
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>
|
|
265
|
+
</section>
|
|
272
266
|
</div>
|
|
273
267
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
268
|
+
<!-- Review rail: what the human is being asked, the anchored comments so far, and the two
|
|
269
|
+
commands — the step reader's end-side rail. Below `lg` it drops under the document, capped
|
|
270
|
+
so a long comment list can't crowd the plan off the screen. -->
|
|
271
|
+
<aside
|
|
272
|
+
:aria-label="t('initiative.planReview.title')"
|
|
273
|
+
class="flex max-h-[55%] w-full shrink-0 flex-col border-t border-slate-800 bg-slate-900/60 lg:max-h-none lg:w-72 lg:border-s lg:border-t-0"
|
|
274
|
+
>
|
|
275
|
+
<div class="border-b border-slate-800 px-4 py-3">
|
|
276
|
+
<!-- A HEADING, not a styled div: this rail is what the window is now for, so the surface
|
|
277
|
+
that asks the human for a decision has to be reachable as one. -->
|
|
278
|
+
<h3
|
|
279
|
+
class="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-amber-400"
|
|
280
|
+
>
|
|
281
|
+
<UIcon name="i-lucide-clipboard-check" class="h-3.5 w-3.5 shrink-0" />
|
|
282
|
+
{{ t('initiative.planReview.title') }}
|
|
283
|
+
</h3>
|
|
284
|
+
<p class="mt-1 text-[12px] leading-relaxed text-slate-400">
|
|
285
|
+
{{ t('initiative.planReview.body') }}
|
|
286
|
+
</p>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
<div class="flex-1 space-y-3 overflow-y-auto overscroll-contain px-4 py-3">
|
|
290
|
+
<!-- Composer for the block just clicked. -->
|
|
291
|
+
<div
|
|
292
|
+
v-if="draftTarget"
|
|
293
|
+
data-testid="initiative-plan-composer"
|
|
294
|
+
class="rounded-lg border border-indigo-500/40 bg-indigo-500/5 p-2.5"
|
|
295
|
+
>
|
|
296
|
+
<div class="mb-1 text-[10px] uppercase tracking-wide text-indigo-300">
|
|
297
|
+
{{ t('panels.stepDetail.commentingOn') }}
|
|
298
|
+
</div>
|
|
299
|
+
<pre
|
|
300
|
+
class="mb-2 max-h-20 overflow-auto whitespace-pre-wrap rounded bg-slate-950/60 p-1.5 text-[11px] text-slate-300"
|
|
301
|
+
>{{ draftTarget.quotedSource }}</pre>
|
|
302
|
+
<UTextarea
|
|
303
|
+
v-model="draftBody"
|
|
304
|
+
data-testid="initiative-plan-comment-body"
|
|
305
|
+
:rows="2"
|
|
306
|
+
autoresize
|
|
307
|
+
size="sm"
|
|
308
|
+
class="w-full"
|
|
309
|
+
:placeholder="t('panels.stepDetail.commentPlaceholder')"
|
|
310
|
+
/>
|
|
311
|
+
<div class="mt-2 flex justify-end gap-2">
|
|
312
|
+
<UButton color="neutral" variant="ghost" size="xs" @click="cancelDraft">
|
|
313
|
+
{{ t('common.cancel') }}
|
|
314
|
+
</UButton>
|
|
315
|
+
<UButton
|
|
316
|
+
color="primary"
|
|
317
|
+
size="xs"
|
|
318
|
+
data-testid="initiative-plan-comment-add"
|
|
319
|
+
:disabled="!draftBody.trim()"
|
|
320
|
+
@click="addDraftComment"
|
|
321
|
+
>
|
|
322
|
+
{{ t('panels.stepDetail.addComment') }}
|
|
323
|
+
</UButton>
|
|
284
324
|
</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
325
|
</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
326
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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"
|
|
327
|
+
<!-- The anchored comments so far. -->
|
|
328
|
+
<div
|
|
329
|
+
v-for="(c, idx) in planComments"
|
|
330
|
+
:key="idx"
|
|
331
|
+
data-testid="initiative-plan-comment"
|
|
332
|
+
class="rounded-lg border border-slate-800 bg-slate-900/50 p-2.5"
|
|
323
333
|
>
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
334
|
+
<div class="mb-1 flex items-start justify-between gap-2">
|
|
335
|
+
<div class="text-[10px] uppercase tracking-wide text-slate-500">
|
|
336
|
+
{{ t('panels.stepDetail.commentN', { number: idx + 1 }) }}
|
|
337
|
+
</div>
|
|
338
|
+
<button
|
|
339
|
+
class="text-slate-500 transition hover:text-rose-400"
|
|
340
|
+
:title="t('panels.stepDetail.removeComment')"
|
|
341
|
+
@click="removeComment(idx)"
|
|
342
|
+
>
|
|
343
|
+
<UIcon name="i-lucide-x" class="h-3.5 w-3.5" />
|
|
344
|
+
</button>
|
|
345
|
+
</div>
|
|
346
|
+
<pre
|
|
347
|
+
class="mb-1 max-h-16 overflow-auto whitespace-pre-wrap rounded bg-slate-950/50 p-1.5 text-[10px] text-slate-400"
|
|
348
|
+
>{{ c.quotedSource }}</pre>
|
|
349
|
+
<p class="text-[12px] text-slate-200">{{ c.body }}</p>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<!-- Only worth saying where clicking a block does something (the RBAC to act on it) and
|
|
353
|
+
where nothing has been said yet. -->
|
|
354
|
+
<p
|
|
355
|
+
v-if="canExecute && !draftTarget && !planComments.length"
|
|
356
|
+
class="text-[11px] leading-relaxed text-slate-400"
|
|
340
357
|
>
|
|
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
358
|
{{ t('initiative.planReview.commentHint') }}
|
|
347
359
|
</p>
|
|
348
360
|
</div>
|
|
349
|
-
|
|
350
|
-
|
|
361
|
+
|
|
362
|
+
<!-- Overall feedback + the two commands. Feedback stays visible rather than hiding behind a
|
|
363
|
+
"request changes" step: with per-block comments in play the human is already composing a
|
|
364
|
+
review, and a hidden field reads as "there is nothing more to say". -->
|
|
365
|
+
<InitiativePlanDecision
|
|
366
|
+
class="border-t border-slate-800 px-4 pt-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))]"
|
|
367
|
+
:approval-id="approval.id"
|
|
368
|
+
:instance-id="instanceId"
|
|
369
|
+
:can-execute="canExecute"
|
|
370
|
+
:comments="wireComments"
|
|
371
|
+
@sent="resetComments"
|
|
372
|
+
/>
|
|
373
|
+
</aside>
|
|
374
|
+
</div>
|
|
351
375
|
</template>
|