@cat-factory/app 0.180.0 → 0.181.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 CHANGED
@@ -148,7 +148,10 @@ example ships in [`deploy/frontend`](../../deploy/frontend) (the `acme:security`
148
148
  - **Context attachments** (`components/context`) — `ContextAttachmentFields`, the
149
149
  shared staged-attachment form used by both the add-task and create-initiative
150
150
  modals. Picks are held locally and import-and-linked once the block exists (see
151
- `composables/useContextLinking`), because linking needs a block id.
151
+ `composables/useContextLinking`), because linking needs a block id. Both hosts
152
+ attach to the SAME per-block linkage, so the inspector's `TaskContextDocs` /
153
+ `TaskContextIssues` sections render for a task AND an initiative — an initiative's
154
+ attachments would otherwise be invisible the moment the create modal closed.
152
155
  - **Integrations** — modals/panels for `github` (the source-control panel, shared
153
156
  by every VCS provider), `vcs` (the GitLab personal-access-token connect),
154
157
  `bootstrap`, `documents`, `tasks`, `requirements` (review), `scenarios`
@@ -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 as agent context, shown inside
8
- // the InspectorPanel. Attaching uses the SAME inline picker as task creation
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="t('documents.taskDocs.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
- {{ t('documents.taskDocs.empty') }}
149
+ {{ emptyHint }}
134
150
  </p>
135
151
  </InspectorSection>
136
152
  </template>
@@ -31,19 +31,38 @@ import {
31
31
  } from '~/utils/initiative'
32
32
  import { interviewGatePhase } from '~/utils/interviewGate'
33
33
  import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
34
+ import StepRunMeta from '~/components/panels/StepRunMeta.vue'
34
35
 
35
36
  const board = useBoardStore()
36
37
  const initiatives = useInitiativesStore()
37
- const execution = useExecutionStore()
38
38
  const { t } = useI18n()
39
39
 
40
- const { open, blockId, close } = useResultView('initiative-planning', {
40
+ const { open, blockId, instanceId, stepIndex, close } = useResultView('initiative-planning', {
41
41
  onOpen: ({ blockId }) => void initiatives.load(blockId),
42
42
  })
43
43
 
44
44
  const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
45
45
  const initiative = computed(() => (blockId.value ? initiatives.forBlock(blockId.value) : null))
46
- const run = computed(() => (blockId.value ? execution.getByBlock(blockId.value) : undefined))
46
+
47
+ /**
48
+ * The planning run + the interviewer step's run details, resolved through the shared seam so this
49
+ * window reports the same "which run is this / how did the model do" facts as every other agent
50
+ * window — including on the card/inspector entry point, which carries no step (see
51
+ * `useResultViewRunMeta`). `run` is the same instance the phase below reads.
52
+ */
53
+ const {
54
+ instance: run,
55
+ step: metaStep,
56
+ instanceId: runId,
57
+ position,
58
+ totalSteps,
59
+ runFailed,
60
+ failureAt,
61
+ } = useResultViewRunMeta('initiative-planning', {
62
+ blockId: () => blockId.value,
63
+ instanceId: () => instanceId.value,
64
+ stepIndex: () => stepIndex.value,
65
+ })
47
66
 
48
67
  /** Every interview exchange, with a stable key for the list + draft map. */
49
68
  const questions = computed(() =>
@@ -184,7 +203,7 @@ async function onDiscard() {
184
203
  icon-class="bg-indigo-500/15 text-indigo-300"
185
204
  :title="initiative?.title ?? block?.title ?? t('initiative.planning.title')"
186
205
  :subtitle="t('initiative.planning.subtitle')"
187
- width="3xl"
206
+ width="4xl"
188
207
  testid="initiative-planning-window"
189
208
  @close="close"
190
209
  >
@@ -194,79 +213,104 @@ async function onDiscard() {
194
213
  </UBadge>
195
214
  </template>
196
215
 
197
- <div class="min-h-0 flex-1 overflow-y-auto px-5 py-4">
198
- <!-- No entity yet -->
199
- <div
200
- v-if="!initiative"
201
- class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
202
- >
203
- <UIcon name="i-lucide-messages-square" class="h-8 w-8 opacity-40" />
204
- <p class="text-sm">{{ t('initiative.planning.empty') }}</p>
205
- </div>
216
+ <div class="flex min-h-0 flex-1">
217
+ <div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
218
+ <!-- No entity yet -->
219
+ <div
220
+ v-if="!initiative"
221
+ class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
222
+ >
223
+ <UIcon name="i-lucide-messages-square" class="h-8 w-8 opacity-40" />
224
+ <p class="text-sm">{{ t('initiative.planning.empty') }}</p>
225
+ </div>
206
226
 
207
- <template v-else>
208
- <p class="mb-4 text-[13px] leading-relaxed text-slate-300">
209
- {{ t('initiative.planning.intro') }}
210
- </p>
227
+ <template v-else>
228
+ <p class="mb-4 text-[13px] leading-relaxed text-slate-300">
229
+ {{ t('initiative.planning.intro') }}
230
+ </p>
211
231
 
212
- <!-- A pass is running: the human is waiting on the planner. Without this the window is
213
- byte-identical to the parked state and the submit reads as a no-op. -->
214
- <InterviewGateNotice
215
- v-if="phase === 'working'"
216
- variant="working"
217
- :title="t('initiative.planning.working')"
218
- :hint="t('initiative.planning.workingHint')"
219
- testid="initiative-planning-working"
220
- />
232
+ <!-- A pass is running: the human is waiting on the planner. Without this the window is
233
+ byte-identical to the parked state and the submit reads as a no-op. -->
234
+ <InterviewGateNotice
235
+ v-if="phase === 'working'"
236
+ variant="working"
237
+ :title="t('initiative.planning.working')"
238
+ :hint="t('initiative.planning.workingHint')"
239
+ testid="initiative-planning-working"
240
+ />
221
241
 
222
- <!-- The planning run stopped before the interview settled — a dead end otherwise. -->
223
- <InterviewGateNotice
224
- v-else-if="phase === 'failed'"
225
- variant="failed"
226
- :title="t('initiative.planning.failed')"
227
- :hint="t('initiative.planning.failedHint')"
228
- testid="initiative-planning-failed"
229
- />
242
+ <!-- The planning run stopped before the interview settled — a dead end otherwise. -->
243
+ <InterviewGateNotice
244
+ v-else-if="phase === 'failed'"
245
+ variant="failed"
246
+ :title="t('initiative.planning.failed')"
247
+ :hint="t('initiative.planning.failedHint')"
248
+ testid="initiative-planning-failed"
249
+ />
230
250
 
231
- <!-- Planning was never started, so there is nothing to answer YET (distinct from
232
- converged, which means the planner already has what it needs). -->
233
- <div
234
- v-else-if="phase === 'idle' && questions.length === 0"
235
- class="rounded-lg border border-slate-800 bg-slate-950/40 p-4 text-center text-[13px] text-slate-400"
236
- data-testid="initiative-planning-idle"
237
- >
238
- {{ t('initiative.planning.idle') }}
239
- </div>
251
+ <!-- Planning was never started, so there is nothing to answer YET (distinct from
252
+ converged, which means the planner already has what it needs). -->
253
+ <div
254
+ v-else-if="phase === 'idle' && questions.length === 0"
255
+ class="rounded-lg border border-slate-800 bg-slate-950/40 p-4 text-center text-[13px] text-slate-400"
256
+ data-testid="initiative-planning-idle"
257
+ >
258
+ {{ t('initiative.planning.idle') }}
259
+ </div>
240
260
 
241
- <!-- Converged / no pending questions -->
242
- <div
243
- v-else-if="phase === 'converged' || questions.length === 0"
244
- class="rounded-lg border border-slate-800 bg-slate-950/40 p-4 text-center text-[13px] text-slate-400"
245
- data-testid="initiative-planning-converged"
246
- >
247
- {{ t('initiative.planning.converged') }}
248
- </div>
261
+ <!-- Converged / no pending questions -->
262
+ <div
263
+ v-else-if="phase === 'converged' || questions.length === 0"
264
+ class="rounded-lg border border-slate-800 bg-slate-950/40 p-4 text-center text-[13px] text-slate-400"
265
+ data-testid="initiative-planning-converged"
266
+ >
267
+ {{ t('initiative.planning.converged') }}
268
+ </div>
269
+
270
+ <!-- Interview questions — the shared clarification surface (answer / not-relevant /
271
+ recommend), reused with the requirements-review window. -->
272
+ <ul v-else class="space-y-4">
273
+ <li
274
+ v-for="q in orderedQuestions"
275
+ :key="q.key"
276
+ data-testid="initiative-planning-question"
277
+ >
278
+ <ClarificationItem
279
+ v-model:answer="drafts[q.key]"
280
+ :prompt="q.question"
281
+ :dismissed="q.status === 'dismissed'"
282
+ :recommendation="q.recommendation"
283
+ :recommending="!!q.id && initiatives.recommending.has(q.id)"
284
+ :answer-placeholder="t('initiative.planning.answerPlaceholder')"
285
+ @persist="persist(q)"
286
+ @dismiss="setStatus(q, 'dismissed')"
287
+ @reopen="setStatus(q, 'open')"
288
+ @recommend="recommend(q)"
289
+ @use-recommendation="useRecommendation(q)"
290
+ />
291
+ </li>
292
+ </ul>
293
+ </template>
294
+ </div>
249
295
 
250
- <!-- Interview questions the shared clarification surface (answer / not-relevant /
251
- recommend), reused with the requirements-review window. -->
252
- <ul v-else class="space-y-4">
253
- <li v-for="q in orderedQuestions" :key="q.key" data-testid="initiative-planning-question">
254
- <ClarificationItem
255
- v-model:answer="drafts[q.key]"
256
- :prompt="q.question"
257
- :dismissed="q.status === 'dismissed'"
258
- :recommendation="q.recommendation"
259
- :recommending="!!q.id && initiatives.recommending.has(q.id)"
260
- :answer-placeholder="t('initiative.planning.answerPlaceholder')"
261
- @persist="persist(q)"
262
- @dismiss="setStatus(q, 'dismissed')"
263
- @reopen="setStatus(q, 'open')"
264
- @recommend="recommend(q)"
265
- @use-recommendation="useRecommendation(q)"
266
- />
267
- </li>
268
- </ul>
269
- </template>
296
+ <!-- Run details: the shared run-metadata + LLM model-activity block every agent window
297
+ carries (step position, live duration, model, run id, calls + token usage). Resolved
298
+ through `useResultViewRunMeta`, so it is present on the card / inspector entry point
299
+ too where this window carries no step index of its own. -->
300
+ <aside
301
+ v-if="metaStep"
302
+ data-testid="initiative-planning-run-meta"
303
+ 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"
304
+ >
305
+ <StepRunMeta
306
+ :step="metaStep"
307
+ :instance-id="runId"
308
+ :step-number="position"
309
+ :total-steps="totalSteps"
310
+ :run-failed="runFailed"
311
+ :failure-at="failureAt"
312
+ />
313
+ </aside>
270
314
  </div>
271
315
 
272
316
  <!-- Action rail. The submit/plan-now pair shows only while the run is actually parked on the