@cat-factory/app 0.180.0 → 0.182.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 +8 -2
- package/app/components/board/nodes/DraggableTask.vue +3 -2
- package/app/components/board/nodes/TaskPipelineMini.vue +9 -12
- package/app/components/docs/DocInterviewWindow.vue +24 -3
- package/app/components/documents/TaskContextDocs.vue +20 -4
- package/app/components/initiative/InitiativePlanningWindow.vue +141 -77
- package/app/components/initiative/InitiativeTrackerWindow.vue +408 -366
- package/app/components/tasks/TaskContextIssues.vue +22 -4
- package/app/composables/useInitiativePlanning.ts +26 -8
- package/app/composables/useResultViewRunMeta.spec.ts +74 -0
- package/app/composables/useResultViewRunMeta.ts +98 -0
- package/app/composables/useTaskExpansion.ts +30 -16
- package/app/docs/consumer-extensions.md +11 -10
- package/app/modular/panels/inspector.logic.spec.ts +12 -5
- package/app/modular/panels/inspector.logic.ts +15 -6
- package/app/stores/taskExpansion.spec.ts +88 -0
- package/app/stores/taskExpansion.ts +34 -14
- package/app/utils/catalog.ts +5 -4
- package/app/utils/interviewGate.spec.ts +87 -12
- package/app/utils/interviewGate.ts +57 -5
- package/i18n/locales/de.json +10 -2
- package/i18n/locales/en.json +10 -2
- package/i18n/locales/es.json +10 -2
- package/i18n/locales/fr.json +10 -2
- package/i18n/locales/he.json +10 -2
- package/i18n/locales/it.json +10 -2
- package/i18n/locales/ja.json +10 -2
- package/i18n/locales/pl.json +10 -2
- package/i18n/locales/tr.json +10 -2
- package/i18n/locales/uk.json +10 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -131,7 +131,10 @@ example ships in [`deploy/frontend`](../../deploy/frontend) (the `acme:security`
|
|
|
131
131
|
|
|
132
132
|
- **Board canvas** (`components/board`) — `BoardCanvas` + `nodes/` (`BlockNode`,
|
|
133
133
|
`ModuleFrame`, `TaskCard`), dependency edges, the per-block `AgentFailureCard` /
|
|
134
|
-
`AgentStopButton`, and a deep-zoom `focus/BlockFocusView`.
|
|
134
|
+
`AgentStopButton`, and a deep-zoom `focus/BlockFocusView`. A running task card expands
|
|
135
|
+
its build pipeline (`TaskPipelineMini`) on hover at any zoom level, and across every
|
|
136
|
+
on-screen card past the `steps` zoom band — the two grants are combined in the
|
|
137
|
+
`taskExpansion` store and driven by `useTaskExpansion`.
|
|
135
138
|
- **Sidebar & chrome** (`components/layout`) — board/account switchers, palettes
|
|
136
139
|
entry points, the language + [interface-mode](#interface-modes-basic--advanced)
|
|
137
140
|
switchers, the `SpendWarningBanner`, and the toolbar (zoom, LOD, decision queue).
|
|
@@ -148,7 +151,10 @@ example ships in [`deploy/frontend`](../../deploy/frontend) (the `acme:security`
|
|
|
148
151
|
- **Context attachments** (`components/context`) — `ContextAttachmentFields`, the
|
|
149
152
|
shared staged-attachment form used by both the add-task and create-initiative
|
|
150
153
|
modals. Picks are held locally and import-and-linked once the block exists (see
|
|
151
|
-
`composables/useContextLinking`), because linking needs a block id.
|
|
154
|
+
`composables/useContextLinking`), because linking needs a block id. Both hosts
|
|
155
|
+
attach to the SAME per-block linkage, so the inspector's `TaskContextDocs` /
|
|
156
|
+
`TaskContextIssues` sections render for a task AND an initiative — an initiative's
|
|
157
|
+
attachments would otherwise be invisible the moment the create modal closed.
|
|
152
158
|
- **Integrations** — modals/panels for `github` (the source-control panel, shared
|
|
153
159
|
by every VCS provider), `vcs` (the GitLab personal-access-token connect),
|
|
154
160
|
`bootstrap`, `documents`, `tasks`, `requirements` (review), `scenarios`
|
|
@@ -10,8 +10,9 @@ const task = computed(() => board.getBlock(props.taskId))
|
|
|
10
10
|
const { draggingId, startDrag } = useBlockDrag()
|
|
11
11
|
|
|
12
12
|
// An expanded pipeline grows downward over its neighbours, so it must stack above the
|
|
13
|
-
// other (compact) task cards — never let a neighbour render on top of the pipeline.
|
|
14
|
-
|
|
13
|
+
// other (compact) task cards — never let a neighbour render on top of the pipeline. Reads
|
|
14
|
+
// the same predicate the pipeline itself renders on, so the two can't disagree.
|
|
15
|
+
const expanded = computed(() => expansion.isExpanded(props.taskId))
|
|
15
16
|
|
|
16
17
|
// Once a task is merged it stops being a unit of work and becomes part of the
|
|
17
18
|
// architecture: it no longer renders as a draggable card (arrows fall back to its
|
|
@@ -12,11 +12,11 @@ import { lodAtLeast } from '~/composables/useSemanticZoom'
|
|
|
12
12
|
import { prReviewPhase } from '~/utils/prReviewProgress'
|
|
13
13
|
import PrReviewPhaseBadge from '~/components/prReview/PrReviewPhaseBadge.vue'
|
|
14
14
|
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// todo breakdown expands — done / in-progress / pending — exactly the way
|
|
18
|
-
// zoomed-in bootstrap card reads. Renders nothing until the task has a run and
|
|
19
|
-
//
|
|
15
|
+
// Drill-down inside a task card: the task's build-pipeline steps appear on hover (at any
|
|
16
|
+
// zoom level) or once the `steps` zoom band is reached, and one band deeper (`subtasks`)
|
|
17
|
+
// each step's live todo breakdown expands — done / in-progress / pending — exactly the way
|
|
18
|
+
// a zoomed-in bootstrap card reads. Renders nothing until the task has a run and the card
|
|
19
|
+
// is expanded, so it's safe to mount on every task card.
|
|
20
20
|
const props = defineProps<{ taskId: string }>()
|
|
21
21
|
|
|
22
22
|
const execution = useExecutionStore()
|
|
@@ -40,13 +40,10 @@ const runFailed = computed(() => instance.value?.status === 'failed')
|
|
|
40
40
|
// (spinning "Running") rather than a frozen subtask list.
|
|
41
41
|
const companionByStep = computed(() => steps.value.map((s) => gateCompanionFor(s, runFailed.value)))
|
|
42
42
|
|
|
43
|
-
// Expand the pipeline list
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
const showSteps = computed(
|
|
47
|
-
() =>
|
|
48
|
-
lodAtLeast(lod.value, 'steps') && steps.value.length > 0 && expansion.canExpand(props.taskId),
|
|
49
|
-
)
|
|
43
|
+
// Expand the pipeline list when the board driver says so: this card is hovered (at any
|
|
44
|
+
// zoom level), or the deep zoom bands granted it — on-screen, and the centre-most of any
|
|
45
|
+
// cards that would otherwise overlap. See stores/taskExpansion.ts.
|
|
46
|
+
const showSteps = computed(() => steps.value.length > 0 && expansion.isExpanded(props.taskId))
|
|
50
47
|
const showItems = computed(() => lodAtLeast(lod.value, 'subtasks'))
|
|
51
48
|
|
|
52
49
|
// Clicking a step opens the full agent step-detail overlay — execution metadata
|
|
@@ -19,7 +19,11 @@
|
|
|
19
19
|
import { computed, reactive, watch } from 'vue'
|
|
20
20
|
import InterviewGateNotice from '~/components/common/InterviewGateNotice.vue'
|
|
21
21
|
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
DOC_INTERVIEWER_KIND,
|
|
24
|
+
interviewGatePhase,
|
|
25
|
+
interviewStepReached,
|
|
26
|
+
} from '~/utils/interviewGate'
|
|
23
27
|
|
|
24
28
|
const board = useBoardStore()
|
|
25
29
|
const docInterview = useDocInterviewStore()
|
|
@@ -63,7 +67,13 @@ const resuming = computed(() => docInterview.resuming)
|
|
|
63
67
|
* actually says, so the questions come back rather than the window sticking on a spinner.
|
|
64
68
|
*/
|
|
65
69
|
const phase = computed(() =>
|
|
66
|
-
resuming.value
|
|
70
|
+
resuming.value
|
|
71
|
+
? 'working'
|
|
72
|
+
: interviewGatePhase(
|
|
73
|
+
session.value?.status,
|
|
74
|
+
run.value?.status,
|
|
75
|
+
interviewStepReached(run.value, DOC_INTERVIEWER_KIND),
|
|
76
|
+
),
|
|
67
77
|
)
|
|
68
78
|
/** The interview converged: the synthesized authoring brief is what the window shows. */
|
|
69
79
|
const converged = computed(() => phase.value === 'converged')
|
|
@@ -133,10 +143,21 @@ const onProceed = () => flushThen((id) => docInterview.proceedInterview(id))
|
|
|
133
143
|
{{ t('docInterview.intro') }}
|
|
134
144
|
</p>
|
|
135
145
|
|
|
146
|
+
<!-- The run is still ahead of the interview (the researcher + outliner steps). Same
|
|
147
|
+
chrome, different claim: nothing has been asked yet, so the "working on your answers"
|
|
148
|
+
copy below would describe answers that do not exist. -->
|
|
149
|
+
<InterviewGateNotice
|
|
150
|
+
v-if="phase === 'preparing'"
|
|
151
|
+
variant="working"
|
|
152
|
+
:title="t('docInterview.preparing')"
|
|
153
|
+
:hint="t('docInterview.preparingHint')"
|
|
154
|
+
testid="doc-interview-preparing"
|
|
155
|
+
/>
|
|
156
|
+
|
|
136
157
|
<!-- A pass is running: the human is waiting on the interviewer. Without this the window is
|
|
137
158
|
byte-identical to the parked state and the submit reads as a no-op. -->
|
|
138
159
|
<InterviewGateNotice
|
|
139
|
-
v-if="phase === 'working'"
|
|
160
|
+
v-else-if="phase === 'working'"
|
|
140
161
|
variant="working"
|
|
141
162
|
:title="t('docInterview.working')"
|
|
142
163
|
:hint="t('docInterview.workingHint')"
|
|
@@ -4,8 +4,12 @@ import type { Block } from '~/types/domain'
|
|
|
4
4
|
import ContextDocumentPicker from '~/components/documents/ContextDocumentPicker.vue'
|
|
5
5
|
import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
|
|
6
6
|
|
|
7
|
-
// Documents (from any source) attached to a task
|
|
8
|
-
//
|
|
7
|
+
// Documents (from any source) attached to a task OR an initiative as agent
|
|
8
|
+
// context, shown inside the InspectorPanel. An initiative takes the same
|
|
9
|
+
// attachments (the create-initiative modal stages them exactly as the add-task one
|
|
10
|
+
// does) and its whole planning pipeline reads them, so it gets the same section —
|
|
11
|
+
// only the prose differs, which is why the hint/empty copy is level-keyed below.
|
|
12
|
+
// Attaching uses the SAME inline picker as task creation
|
|
9
13
|
// (source selector + repo→file browse + free-text search + paste-by-reference —
|
|
10
14
|
// ContextDocumentPicker), NOT the old dropdown that opened a second, page-level
|
|
11
15
|
// "Import a page…" modal on top of the inspector. Stacked page-level modals don't
|
|
@@ -27,6 +31,18 @@ onMounted(() => {
|
|
|
27
31
|
})
|
|
28
32
|
|
|
29
33
|
const linked = computed(() => documents.docsForBlock(props.block.id))
|
|
34
|
+
|
|
35
|
+
// Two STATIC literal keys per string, picked by level — the copy names what reads the
|
|
36
|
+
// document (the agents implementing a task vs the pipeline that plans an initiative), which
|
|
37
|
+
// is the whole point of the hint. Assembling one key from `block.level` would defeat the
|
|
38
|
+
// typed message-key check for a two-member choice that gains nothing from being dynamic.
|
|
39
|
+
const isInitiative = computed(() => props.block.level === 'initiative')
|
|
40
|
+
const hint = computed(() =>
|
|
41
|
+
isInitiative.value ? t('documents.taskDocs.hintInitiative') : t('documents.taskDocs.hint'),
|
|
42
|
+
)
|
|
43
|
+
const emptyHint = computed(() =>
|
|
44
|
+
isInitiative.value ? t('documents.taskDocs.emptyInitiative') : t('documents.taskDocs.empty'),
|
|
45
|
+
)
|
|
30
46
|
// Already-linked docs, so the inline picker filters them out / never re-offers them.
|
|
31
47
|
const chosenKeys = computed(() =>
|
|
32
48
|
linked.value.map((d) =>
|
|
@@ -71,7 +87,7 @@ async function attach(item: PendingContext) {
|
|
|
71
87
|
<InspectorSection
|
|
72
88
|
v-if="documents.available"
|
|
73
89
|
:title="t('documents.taskDocs.heading')"
|
|
74
|
-
:hint="
|
|
90
|
+
:hint="hint"
|
|
75
91
|
:count="linked.length"
|
|
76
92
|
>
|
|
77
93
|
<template #actions>
|
|
@@ -130,7 +146,7 @@ async function attach(item: PendingContext) {
|
|
|
130
146
|
</a>
|
|
131
147
|
</div>
|
|
132
148
|
<p v-else class="text-[11px] text-slate-500">
|
|
133
|
-
{{
|
|
149
|
+
{{ emptyHint }}
|
|
134
150
|
</p>
|
|
135
151
|
</InspectorSection>
|
|
136
152
|
</template>
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// (pending `qa` entries with an empty answer) are shown here; the human answers them, then either
|
|
5
5
|
// SUBMITS them (the `continue` action: the interviewer re-runs and may ask follow-ups) or plans
|
|
6
6
|
// now (the `proceed` action: skip the remaining questions — the interviewer converges and the run
|
|
7
|
-
// advances to the analyst
|
|
8
|
-
// because the latter pair both read as "go forward"
|
|
7
|
+
// advances to the planner; the analyst already ran, ahead of this gate). The labels say
|
|
8
|
+
// submit/plan-now rather than continue/proceed because the latter pair both read as "go forward"
|
|
9
|
+
// and were indistinguishable in use.
|
|
9
10
|
// Opened via the universal result-view host: from the inspector / card
|
|
10
11
|
// (`ui.openInitiativePlanning`) or as the interviewer step's result view. Live `initiative`
|
|
11
12
|
// stream events patch the store, so an open window follows the interview as it progresses.
|
|
@@ -29,21 +30,44 @@ import {
|
|
|
29
30
|
isPendingQuestion,
|
|
30
31
|
orderInterviewQuestions,
|
|
31
32
|
} from '~/utils/initiative'
|
|
32
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
INITIATIVE_INTERVIEWER_KIND,
|
|
35
|
+
interviewGatePhase,
|
|
36
|
+
interviewStepReached,
|
|
37
|
+
} from '~/utils/interviewGate'
|
|
33
38
|
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
39
|
+
import StepRunMeta from '~/components/panels/StepRunMeta.vue'
|
|
34
40
|
|
|
35
41
|
const board = useBoardStore()
|
|
36
42
|
const initiatives = useInitiativesStore()
|
|
37
|
-
const execution = useExecutionStore()
|
|
38
43
|
const { t } = useI18n()
|
|
39
44
|
|
|
40
|
-
const { open, blockId, close } = useResultView('initiative-planning', {
|
|
45
|
+
const { open, blockId, instanceId, stepIndex, close } = useResultView('initiative-planning', {
|
|
41
46
|
onOpen: ({ blockId }) => void initiatives.load(blockId),
|
|
42
47
|
})
|
|
43
48
|
|
|
44
49
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
45
50
|
const initiative = computed(() => (blockId.value ? initiatives.forBlock(blockId.value) : null))
|
|
46
|
-
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The planning run + the interviewer step's run details, resolved through the shared seam so this
|
|
54
|
+
* window reports the same "which run is this / how did the model do" facts as every other agent
|
|
55
|
+
* window — including on the card/inspector entry point, which carries no step (see
|
|
56
|
+
* `useResultViewRunMeta`). `run` is the same instance the phase below reads.
|
|
57
|
+
*/
|
|
58
|
+
const {
|
|
59
|
+
instance: run,
|
|
60
|
+
step: metaStep,
|
|
61
|
+
instanceId: runId,
|
|
62
|
+
position,
|
|
63
|
+
totalSteps,
|
|
64
|
+
runFailed,
|
|
65
|
+
failureAt,
|
|
66
|
+
} = useResultViewRunMeta('initiative-planning', {
|
|
67
|
+
blockId: () => blockId.value,
|
|
68
|
+
instanceId: () => instanceId.value,
|
|
69
|
+
stepIndex: () => stepIndex.value,
|
|
70
|
+
})
|
|
47
71
|
|
|
48
72
|
/** Every interview exchange, with a stable key for the list + draft map. */
|
|
49
73
|
const questions = computed(() =>
|
|
@@ -98,7 +122,11 @@ const resuming = computed(() => initiatives.resuming)
|
|
|
98
122
|
const phase = computed(() =>
|
|
99
123
|
resuming.value
|
|
100
124
|
? 'working'
|
|
101
|
-
: interviewGatePhase(
|
|
125
|
+
: interviewGatePhase(
|
|
126
|
+
initiative.value?.interview?.status,
|
|
127
|
+
run.value?.status,
|
|
128
|
+
interviewStepReached(run.value, INITIATIVE_INTERVIEWER_KIND),
|
|
129
|
+
),
|
|
102
130
|
)
|
|
103
131
|
|
|
104
132
|
/**
|
|
@@ -184,7 +212,7 @@ async function onDiscard() {
|
|
|
184
212
|
icon-class="bg-indigo-500/15 text-indigo-300"
|
|
185
213
|
:title="initiative?.title ?? block?.title ?? t('initiative.planning.title')"
|
|
186
214
|
:subtitle="t('initiative.planning.subtitle')"
|
|
187
|
-
width="
|
|
215
|
+
width="4xl"
|
|
188
216
|
testid="initiative-planning-window"
|
|
189
217
|
@close="close"
|
|
190
218
|
>
|
|
@@ -194,86 +222,122 @@ async function onDiscard() {
|
|
|
194
222
|
</UBadge>
|
|
195
223
|
</template>
|
|
196
224
|
|
|
197
|
-
<div class="min-h-0 flex-1
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
225
|
+
<div class="flex min-h-0 flex-1">
|
|
226
|
+
<div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
|
|
227
|
+
<!-- No entity yet -->
|
|
228
|
+
<div
|
|
229
|
+
v-if="!initiative"
|
|
230
|
+
class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
|
|
231
|
+
>
|
|
232
|
+
<UIcon name="i-lucide-messages-square" class="h-8 w-8 opacity-40" />
|
|
233
|
+
<p class="text-sm">{{ t('initiative.planning.empty') }}</p>
|
|
234
|
+
</div>
|
|
206
235
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
236
|
+
<template v-else>
|
|
237
|
+
<p class="mb-4 text-[13px] leading-relaxed text-slate-300">
|
|
238
|
+
{{ t('initiative.planning.intro') }}
|
|
239
|
+
</p>
|
|
211
240
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
241
|
+
<!-- The run is still ahead of the interview — the codebase analysis that grounds it. It
|
|
242
|
+
wears the working chrome but says something different on purpose: nothing has been
|
|
243
|
+
asked yet, so "working on your answers" would describe answers that do not exist. -->
|
|
244
|
+
<InterviewGateNotice
|
|
245
|
+
v-if="phase === 'preparing'"
|
|
246
|
+
variant="working"
|
|
247
|
+
:title="t('initiative.planning.preparing')"
|
|
248
|
+
:hint="t('initiative.planning.preparingHint')"
|
|
249
|
+
testid="initiative-planning-preparing"
|
|
250
|
+
/>
|
|
221
251
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
252
|
+
<!-- A pass is running: the human is waiting on the planner. Without this the window is
|
|
253
|
+
byte-identical to the parked state and the submit reads as a no-op. -->
|
|
254
|
+
<InterviewGateNotice
|
|
255
|
+
v-else-if="phase === 'working'"
|
|
256
|
+
variant="working"
|
|
257
|
+
:title="t('initiative.planning.working')"
|
|
258
|
+
:hint="t('initiative.planning.workingHint')"
|
|
259
|
+
testid="initiative-planning-working"
|
|
260
|
+
/>
|
|
230
261
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
</div>
|
|
262
|
+
<!-- The planning run stopped before the interview settled — a dead end otherwise. -->
|
|
263
|
+
<InterviewGateNotice
|
|
264
|
+
v-else-if="phase === 'failed'"
|
|
265
|
+
variant="failed"
|
|
266
|
+
:title="t('initiative.planning.failed')"
|
|
267
|
+
:hint="t('initiative.planning.failedHint')"
|
|
268
|
+
testid="initiative-planning-failed"
|
|
269
|
+
/>
|
|
240
270
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
271
|
+
<!-- Planning was never started, so there is nothing to answer YET (distinct from
|
|
272
|
+
converged, which means the planner already has what it needs). -->
|
|
273
|
+
<div
|
|
274
|
+
v-else-if="phase === 'idle' && questions.length === 0"
|
|
275
|
+
class="rounded-lg border border-slate-800 bg-slate-950/40 p-4 text-center text-[13px] text-slate-400"
|
|
276
|
+
data-testid="initiative-planning-idle"
|
|
277
|
+
>
|
|
278
|
+
{{ t('initiative.planning.idle') }}
|
|
279
|
+
</div>
|
|
249
280
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
281
|
+
<!-- Converged / no pending questions -->
|
|
282
|
+
<div
|
|
283
|
+
v-else-if="phase === 'converged' || questions.length === 0"
|
|
284
|
+
class="rounded-lg border border-slate-800 bg-slate-950/40 p-4 text-center text-[13px] text-slate-400"
|
|
285
|
+
data-testid="initiative-planning-converged"
|
|
286
|
+
>
|
|
287
|
+
{{ t('initiative.planning.converged') }}
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<!-- Interview questions — the shared clarification surface (answer / not-relevant /
|
|
291
|
+
recommend), reused with the requirements-review window. -->
|
|
292
|
+
<ul v-else class="space-y-4">
|
|
293
|
+
<li
|
|
294
|
+
v-for="q in orderedQuestions"
|
|
295
|
+
:key="q.key"
|
|
296
|
+
data-testid="initiative-planning-question"
|
|
297
|
+
>
|
|
298
|
+
<ClarificationItem
|
|
299
|
+
v-model:answer="drafts[q.key]"
|
|
300
|
+
:prompt="q.question"
|
|
301
|
+
:dismissed="q.status === 'dismissed'"
|
|
302
|
+
:recommendation="q.recommendation"
|
|
303
|
+
:recommending="!!q.id && initiatives.recommending.has(q.id)"
|
|
304
|
+
:answer-placeholder="t('initiative.planning.answerPlaceholder')"
|
|
305
|
+
@persist="persist(q)"
|
|
306
|
+
@dismiss="setStatus(q, 'dismissed')"
|
|
307
|
+
@reopen="setStatus(q, 'open')"
|
|
308
|
+
@recommend="recommend(q)"
|
|
309
|
+
@use-recommendation="useRecommendation(q)"
|
|
310
|
+
/>
|
|
311
|
+
</li>
|
|
312
|
+
</ul>
|
|
313
|
+
</template>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<!-- Run details: the shared run-metadata + LLM model-activity block every agent window
|
|
317
|
+
carries (step position, live duration, model, run id, calls + token usage). Resolved
|
|
318
|
+
through `useResultViewRunMeta`, so it is present on the card / inspector entry point
|
|
319
|
+
too — where this window carries no step index of its own. -->
|
|
320
|
+
<aside
|
|
321
|
+
v-if="metaStep"
|
|
322
|
+
data-testid="initiative-planning-run-meta"
|
|
323
|
+
class="hidden w-60 shrink-0 flex-col gap-4 overflow-y-auto border-s border-slate-800 bg-slate-900/50 px-4 py-4 lg:flex"
|
|
324
|
+
>
|
|
325
|
+
<StepRunMeta
|
|
326
|
+
:step="metaStep"
|
|
327
|
+
:instance-id="runId"
|
|
328
|
+
:step-number="position"
|
|
329
|
+
:total-steps="totalSteps"
|
|
330
|
+
:run-failed="runFailed"
|
|
331
|
+
:failure-at="failureAt"
|
|
332
|
+
/>
|
|
333
|
+
</aside>
|
|
270
334
|
</div>
|
|
271
335
|
|
|
272
336
|
<!-- Action rail. The submit/plan-now pair shows only while the run is actually parked on the
|
|
273
337
|
human: mid-pass they would re-submit a question set already in flight, and the resume is a
|
|
274
338
|
no-op once it isn't. Discard is the opposite — it is offered for as long as a run owns the
|
|
275
|
-
block, because the phases where those two are hidden (working, failed) are
|
|
276
|
-
a wedged run sits in. -->
|
|
339
|
+
block, because the phases where those two are hidden (preparing, working, failed) are
|
|
340
|
+
exactly the ones a wedged run sits in. -->
|
|
277
341
|
<footer
|
|
278
342
|
v-if="initiative && (canDiscard || (phase === 'awaiting' && questions.length > 0))"
|
|
279
343
|
class="flex items-center justify-between gap-3 border-t border-slate-800 px-5 py-3"
|