@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.
@@ -19,19 +19,40 @@ import {
19
19
  pendingCheckpointPhase,
20
20
  } from '~/utils/initiative'
21
21
  import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
22
+ import StepRunMeta from '~/components/panels/StepRunMeta.vue'
22
23
 
23
24
  const board = useBoardStore()
24
25
  const initiatives = useInitiativesStore()
25
26
  const { t } = useI18n()
26
27
  const toast = useToast()
27
28
 
28
- const { open, blockId, close } = useResultView('initiative-tracker', {
29
+ const { open, blockId, instanceId, stepIndex, close } = useResultView('initiative-tracker', {
29
30
  onOpen: ({ blockId }) => void initiatives.load(blockId),
30
31
  })
31
32
 
32
33
  const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
33
34
  const initiative = computed(() => (blockId.value ? initiatives.forBlock(blockId.value) : null))
34
35
 
36
+ /**
37
+ * The planning run's step this window speaks for, plus its run details, resolved through the
38
+ * shared seam so the tracker reports the same "which run is this / how did the model do" facts as
39
+ * every other agent window. The window is registered for the analyst, the planner AND the
40
+ * (model-less) committer, and is reached from the board card / inspector as often as from the run
41
+ * timeline — `useResultViewRunMeta` owns picking the right step for both cases.
42
+ */
43
+ const {
44
+ step: metaStep,
45
+ instanceId: runId,
46
+ position,
47
+ totalSteps,
48
+ runFailed,
49
+ failureAt,
50
+ } = useResultViewRunMeta('initiative-tracker', {
51
+ blockId: () => blockId.value,
52
+ instanceId: () => instanceId.value,
53
+ stepIndex: () => stepIndex.value,
54
+ })
55
+
35
56
  const phases = computed(() => initiative.value?.phases ?? [])
36
57
  function itemsOf(phaseId: string): InitiativeItem[] {
37
58
  return (initiative.value?.items ?? []).filter((i) => i.phaseId === phaseId)
@@ -180,7 +201,7 @@ async function savePolicy() {
180
201
  icon-class="bg-indigo-500/15 text-indigo-300"
181
202
  :title="initiative?.title ?? block?.title ?? t('initiative.tracker.title')"
182
203
  :subtitle="t('initiative.tracker.subtitle')"
183
- width="4xl"
204
+ width="5xl"
184
205
  testid="initiative-tracker-window"
185
206
  @close="close"
186
207
  >
@@ -201,396 +222,417 @@ async function savePolicy() {
201
222
  </UBadge>
202
223
  </template>
203
224
 
204
- <div class="min-h-0 flex-1 overflow-y-auto px-5 py-4">
205
- <!-- No entity yet (module unwired / still creating) -->
206
- <div
207
- v-if="!initiative"
208
- class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
209
- >
210
- <UIcon name="i-lucide-milestone" class="h-8 w-8 opacity-40" />
211
- <p class="text-sm">{{ t('initiative.tracker.empty') }}</p>
212
- </div>
213
-
214
- <template v-else>
215
- <!-- Paused at a phase checkpoint (D2): a completed checkpoint phase is awaiting
216
- review before the next phase spawns. Read the phase's artifacts/PRs below,
217
- then resume (continue) or cancel (stop) the initiative right here. -->
218
- <section
219
- v-if="pausedAtCheckpoint"
220
- class="mb-4 rounded-lg border border-amber-500/40 bg-amber-500/10 p-3.5"
221
- data-testid="initiative-checkpoint-pause"
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 (module unwired / still creating) -->
228
+ <div
229
+ v-if="!initiative"
230
+ class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
222
231
  >
223
- <div class="flex items-start gap-2.5">
224
- <UIcon name="i-lucide-pause-circle" class="mt-0.5 h-4 w-4 shrink-0 text-amber-300" />
225
- <div class="min-w-0 flex-1">
226
- <h3 class="text-[13px] font-semibold text-amber-200">
227
- {{ t('initiative.checkpoint.pausedTitle') }}
232
+ <UIcon name="i-lucide-milestone" class="h-8 w-8 opacity-40" />
233
+ <p class="text-sm">{{ t('initiative.tracker.empty') }}</p>
234
+ </div>
235
+
236
+ <template v-else>
237
+ <!-- Paused at a phase checkpoint (D2): a completed checkpoint phase is awaiting
238
+ review before the next phase spawns. Read the phase's artifacts/PRs below,
239
+ then resume (continue) or cancel (stop) the initiative right here. -->
240
+ <section
241
+ v-if="pausedAtCheckpoint"
242
+ class="mb-4 rounded-lg border border-amber-500/40 bg-amber-500/10 p-3.5"
243
+ data-testid="initiative-checkpoint-pause"
244
+ >
245
+ <div class="flex items-start gap-2.5">
246
+ <UIcon name="i-lucide-pause-circle" class="mt-0.5 h-4 w-4 shrink-0 text-amber-300" />
247
+ <div class="min-w-0 flex-1">
248
+ <h3 class="text-[13px] font-semibold text-amber-200">
249
+ {{ t('initiative.checkpoint.pausedTitle') }}
250
+ </h3>
251
+ <p class="mt-0.5 text-[12px] leading-relaxed text-amber-100/80">
252
+ {{ t('initiative.checkpoint.pausedBody', { phase: checkpointPhase!.title }) }}
253
+ </p>
254
+ <div class="mt-2.5 flex flex-wrap gap-2">
255
+ <button
256
+ class="rounded bg-indigo-600 px-2.5 py-1 text-[11px] font-medium text-white hover:bg-indigo-500 disabled:opacity-50"
257
+ :disabled="initiatives.controlling"
258
+ data-testid="initiative-checkpoint-resume"
259
+ @click="checkpointControl('resume')"
260
+ >
261
+ {{ t('initiative.inspector.resume') }}
262
+ </button>
263
+ <button
264
+ class="rounded border border-rose-500/50 px-2.5 py-1 text-[11px] font-medium text-rose-300 hover:bg-rose-500/10 disabled:opacity-50"
265
+ :disabled="initiatives.controlling"
266
+ data-testid="initiative-checkpoint-cancel"
267
+ @click="checkpointControl('cancel')"
268
+ >
269
+ {{ t('initiative.inspector.cancel') }}
270
+ </button>
271
+ </div>
272
+ </div>
273
+ </div>
274
+ </section>
275
+
276
+ <!-- Goal & constraints -->
277
+ <section v-if="initiative.goal" class="mb-4">
278
+ <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
279
+ {{ t('initiative.tracker.goal') }}
280
+ </h3>
281
+ <p class="whitespace-pre-wrap text-[13px] leading-relaxed text-slate-300">
282
+ {{ initiative.goal }}
283
+ </p>
284
+ </section>
285
+ <section v-if="initiative.constraints?.length" class="mb-4">
286
+ <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
287
+ {{ t('initiative.tracker.constraints') }}
288
+ </h3>
289
+ <ul class="list-inside list-disc text-[13px] text-slate-300">
290
+ <li v-for="(c, i) in initiative.constraints" :key="i">{{ c }}</li>
291
+ </ul>
292
+ </section>
293
+ <section v-if="initiative.nonGoals?.length" class="mb-4">
294
+ <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
295
+ {{ t('initiative.tracker.nonGoals') }}
296
+ </h3>
297
+ <ul class="list-inside list-disc text-[13px] text-slate-300">
298
+ <li v-for="(g, i) in initiative.nonGoals" :key="i">{{ g }}</li>
299
+ </ul>
300
+ </section>
301
+ <section v-if="initiative.analysisSummary" class="mb-4">
302
+ <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
303
+ {{ t('initiative.tracker.analysis') }}
304
+ </h3>
305
+ <p class="whitespace-pre-wrap text-[13px] leading-relaxed text-slate-300">
306
+ {{ initiative.analysisSummary }}
307
+ </p>
308
+ </section>
309
+
310
+ <!-- Awaiting planning -->
311
+ <div
312
+ v-if="phases.length === 0"
313
+ class="mb-4 rounded-lg border border-dashed border-slate-700 p-4 text-center text-[12px] text-slate-400"
314
+ >
315
+ {{ t('initiative.tracker.noPlan') }}
316
+ </div>
317
+
318
+ <!-- Phases + items -->
319
+ <section v-for="phase in phases" :key="phase.id" class="mb-5">
320
+ <h3 class="mb-1 flex items-center gap-2 text-sm font-semibold text-slate-200">
321
+ <span>{{ t('initiative.tracker.phase', { title: phase.title }) }}</span>
322
+ <!-- Checkpoint annotation (D2): this phase pauses the initiative for human review
323
+ once its items settle. Cleared → already reviewed; the pending one → awaiting
324
+ review; otherwise an upcoming gate. -->
325
+ <UBadge
326
+ v-if="phase.checkpoint"
327
+ :color="
328
+ phase.checkpointClearedAt
329
+ ? 'neutral'
330
+ : awaitingReviewPhaseId === phase.id
331
+ ? 'warning'
332
+ : 'info'
333
+ "
334
+ variant="subtle"
335
+ size="sm"
336
+ :data-testid="`initiative-phase-checkpoint-${phase.id}`"
337
+ >
338
+ <UIcon name="i-lucide-flag" class="mr-1 h-3 w-3" />
339
+ {{
340
+ phase.checkpointClearedAt
341
+ ? t('initiative.checkpoint.cleared')
342
+ : awaitingReviewPhaseId === phase.id
343
+ ? t('initiative.checkpoint.awaiting')
344
+ : t('initiative.checkpoint.badge')
345
+ }}
346
+ </UBadge>
347
+ </h3>
348
+ <p v-if="phase.goal" class="mb-2 text-[12px] text-slate-400">{{ phase.goal }}</p>
349
+ <div class="overflow-x-auto rounded-lg border border-slate-800">
350
+ <table class="w-full text-[12px]">
351
+ <thead>
352
+ <tr class="border-b border-slate-800 text-left text-slate-500">
353
+ <th class="px-3 py-2 font-medium">{{ t('initiative.tracker.colItem') }}</th>
354
+ <th class="px-3 py-2 font-medium">{{ t('initiative.tracker.colStatus') }}</th>
355
+ <th class="px-3 py-2 font-medium">{{ t('initiative.tracker.colPr') }}</th>
356
+ </tr>
357
+ </thead>
358
+ <tbody>
359
+ <tr
360
+ v-for="item in itemsOf(phase.id)"
361
+ :key="item.id"
362
+ class="border-b border-slate-800/60 last:border-0"
363
+ >
364
+ <td class="px-3 py-2 align-top">
365
+ <div class="font-medium text-slate-200">{{ item.title }}</div>
366
+ <div v-if="item.dependsOn?.length" class="mt-0.5 text-[10px] text-slate-500">
367
+ {{
368
+ t('initiative.tracker.dependsOn', {
369
+ items: item.dependsOn.join(', '),
370
+ })
371
+ }}
372
+ </div>
373
+ <div v-if="item.note" class="mt-0.5 text-[10px] text-amber-300/80">
374
+ {{ item.note }}
375
+ </div>
376
+ <div
377
+ v-if="editable && (item.status === 'blocked' || item.status === 'pending')"
378
+ class="mt-1 flex gap-1.5"
379
+ >
380
+ <button
381
+ v-if="item.status === 'blocked'"
382
+ class="rounded border border-slate-700 px-1.5 py-0.5 text-[10px] text-slate-300 hover:bg-slate-800 disabled:opacity-50"
383
+ :disabled="initiatives.curating"
384
+ :data-testid="`initiative-item-retry-${item.id}`"
385
+ @click="itemAction(item, 'retry')"
386
+ >
387
+ {{ t('initiative.curation.retry') }}
388
+ </button>
389
+ <button
390
+ class="rounded border border-slate-700 px-1.5 py-0.5 text-[10px] text-slate-300 hover:bg-slate-800 disabled:opacity-50"
391
+ :disabled="initiatives.curating"
392
+ :data-testid="`initiative-item-skip-${item.id}`"
393
+ @click="itemAction(item, 'skip')"
394
+ >
395
+ {{ t('initiative.curation.skip') }}
396
+ </button>
397
+ </div>
398
+ </td>
399
+ <td class="px-3 py-2 align-top">
400
+ <UBadge
401
+ :color="INITIATIVE_ITEM_STATUS_CHIPS[item.status]"
402
+ variant="subtle"
403
+ size="sm"
404
+ >
405
+ {{ t(INITIATIVE_ITEM_STATUS_LABEL_KEYS[item.status]) }}
406
+ </UBadge>
407
+ </td>
408
+ <td class="px-3 py-2 align-top">
409
+ <a
410
+ v-if="item.pr"
411
+ :href="item.pr.url"
412
+ target="_blank"
413
+ rel="noopener"
414
+ class="text-sky-400 hover:underline"
415
+ >
416
+ {{ item.pr.number ? `#${item.pr.number}` : t('initiative.tracker.prLink') }}
417
+ </a>
418
+ <span v-else class="text-slate-600">—</span>
419
+ </td>
420
+ </tr>
421
+ </tbody>
422
+ </table>
423
+ </div>
424
+ </section>
425
+
426
+ <!-- Execution policy -->
427
+ <section v-if="initiative.policy" class="mb-4">
428
+ <div class="mb-1 flex items-center gap-2">
429
+ <h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
430
+ {{ t('initiative.tracker.policy') }}
228
431
  </h3>
229
- <p class="mt-0.5 text-[12px] leading-relaxed text-amber-100/80">
230
- {{ t('initiative.checkpoint.pausedBody', { phase: checkpointPhase!.title }) }}
231
- </p>
232
- <div class="mt-2.5 flex flex-wrap gap-2">
432
+ <button
433
+ v-if="editable && !editingPolicy"
434
+ class="rounded border border-slate-700 px-1.5 py-0.5 text-[10px] text-slate-300 hover:bg-slate-800"
435
+ data-testid="initiative-policy-edit"
436
+ @click="startEditPolicy"
437
+ >
438
+ {{ t('initiative.curation.edit') }}
439
+ </button>
440
+ </div>
441
+ <ul v-if="!editingPolicy" class="text-[12px] text-slate-300">
442
+ <li>
443
+ {{
444
+ t('initiative.tracker.maxConcurrent', {
445
+ count: initiative.policy.maxConcurrent,
446
+ })
447
+ }}
448
+ </li>
449
+ <li v-for="(rule, i) in policyRules" :key="i">
450
+ <code class="text-sky-300">{{ rule.pipelineId }}</code>
451
+ · {{ ruleAxes(rule) }}
452
+ </li>
453
+ <li>
454
+ {{ t('initiative.tracker.defaultPipeline') }}
455
+ <code class="text-sky-300">{{ initiative.policy.defaultPipelineId }}</code>
456
+ </li>
457
+ </ul>
458
+ <!-- Edit form: the two scalar knobs; planner-authored rules are preserved. -->
459
+ <div v-else class="flex flex-col gap-2 rounded-lg border border-slate-800 p-3">
460
+ <label class="flex items-center gap-2 text-[12px] text-slate-300">
461
+ <span class="w-40">{{ t('initiative.curation.maxConcurrentField') }}</span>
462
+ <input
463
+ v-model.number="policyForm.maxConcurrent"
464
+ type="number"
465
+ min="1"
466
+ max="20"
467
+ class="w-20 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-slate-200"
468
+ data-testid="initiative-policy-max-concurrent"
469
+ />
470
+ </label>
471
+ <label class="flex items-center gap-2 text-[12px] text-slate-300">
472
+ <span class="w-40">{{ t('initiative.curation.defaultPipelineField') }}</span>
473
+ <input
474
+ v-model="policyForm.defaultPipelineId"
475
+ type="text"
476
+ class="flex-1 rounded border border-slate-700 bg-slate-950 px-2 py-1 font-mono text-[11px] text-slate-200"
477
+ data-testid="initiative-policy-default-pipeline"
478
+ />
479
+ </label>
480
+ <div class="flex gap-2">
233
481
  <button
234
- class="rounded bg-indigo-600 px-2.5 py-1 text-[11px] font-medium text-white hover:bg-indigo-500 disabled:opacity-50"
235
- :disabled="initiatives.controlling"
236
- data-testid="initiative-checkpoint-resume"
237
- @click="checkpointControl('resume')"
482
+ class="rounded bg-indigo-600 px-2 py-1 text-[11px] text-white hover:bg-indigo-500 disabled:opacity-50"
483
+ :disabled="initiatives.curating"
484
+ data-testid="initiative-policy-save"
485
+ @click="savePolicy"
238
486
  >
239
- {{ t('initiative.inspector.resume') }}
487
+ {{ t('initiative.curation.save') }}
240
488
  </button>
241
489
  <button
242
- class="rounded border border-rose-500/50 px-2.5 py-1 text-[11px] font-medium text-rose-300 hover:bg-rose-500/10 disabled:opacity-50"
243
- :disabled="initiatives.controlling"
244
- data-testid="initiative-checkpoint-cancel"
245
- @click="checkpointControl('cancel')"
490
+ class="rounded border border-slate-700 px-2 py-1 text-[11px] text-slate-300 hover:bg-slate-800"
491
+ @click="editingPolicy = false"
246
492
  >
247
- {{ t('initiative.inspector.cancel') }}
493
+ {{ t('initiative.curation.cancel') }}
248
494
  </button>
249
495
  </div>
250
496
  </div>
251
- </div>
252
- </section>
253
-
254
- <!-- Goal & constraints -->
255
- <section v-if="initiative.goal" class="mb-4">
256
- <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
257
- {{ t('initiative.tracker.goal') }}
258
- </h3>
259
- <p class="whitespace-pre-wrap text-[13px] leading-relaxed text-slate-300">
260
- {{ initiative.goal }}
261
- </p>
262
- </section>
263
- <section v-if="initiative.constraints?.length" class="mb-4">
264
- <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
265
- {{ t('initiative.tracker.constraints') }}
266
- </h3>
267
- <ul class="list-inside list-disc text-[13px] text-slate-300">
268
- <li v-for="(c, i) in initiative.constraints" :key="i">{{ c }}</li>
269
- </ul>
270
- </section>
271
- <section v-if="initiative.nonGoals?.length" class="mb-4">
272
- <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
273
- {{ t('initiative.tracker.nonGoals') }}
274
- </h3>
275
- <ul class="list-inside list-disc text-[13px] text-slate-300">
276
- <li v-for="(g, i) in initiative.nonGoals" :key="i">{{ g }}</li>
277
- </ul>
278
- </section>
279
- <section v-if="initiative.analysisSummary" class="mb-4">
280
- <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
281
- {{ t('initiative.tracker.analysis') }}
282
- </h3>
283
- <p class="whitespace-pre-wrap text-[13px] leading-relaxed text-slate-300">
284
- {{ initiative.analysisSummary }}
285
- </p>
286
- </section>
287
-
288
- <!-- Awaiting planning -->
289
- <div
290
- v-if="phases.length === 0"
291
- class="mb-4 rounded-lg border border-dashed border-slate-700 p-4 text-center text-[12px] text-slate-400"
292
- >
293
- {{ t('initiative.tracker.noPlan') }}
294
- </div>
497
+ </section>
295
498
 
296
- <!-- Phases + items -->
297
- <section v-for="phase in phases" :key="phase.id" class="mb-5">
298
- <h3 class="mb-1 flex items-center gap-2 text-sm font-semibold text-slate-200">
299
- <span>{{ t('initiative.tracker.phase', { title: phase.title }) }}</span>
300
- <!-- Checkpoint annotation (D2): this phase pauses the initiative for human review
301
- once its items settle. Cleared → already reviewed; the pending one → awaiting
302
- review; otherwise an upcoming gate. -->
303
- <UBadge
304
- v-if="phase.checkpoint"
305
- :color="
306
- phase.checkpointClearedAt
307
- ? 'neutral'
308
- : awaitingReviewPhaseId === phase.id
309
- ? 'warning'
310
- : 'info'
311
- "
312
- variant="subtle"
313
- size="sm"
314
- :data-testid="`initiative-phase-checkpoint-${phase.id}`"
315
- >
316
- <UIcon name="i-lucide-flag" class="mr-1 h-3 w-3" />
317
- {{
318
- phase.checkpointClearedAt
319
- ? t('initiative.checkpoint.cleared')
320
- : awaitingReviewPhaseId === phase.id
321
- ? t('initiative.checkpoint.awaiting')
322
- : t('initiative.checkpoint.badge')
323
- }}
324
- </UBadge>
325
- </h3>
326
- <p v-if="phase.goal" class="mb-2 text-[12px] text-slate-400">{{ phase.goal }}</p>
327
- <div class="overflow-x-auto rounded-lg border border-slate-800">
328
- <table class="w-full text-[12px]">
329
- <thead>
330
- <tr class="border-b border-slate-800 text-left text-slate-500">
331
- <th class="px-3 py-2 font-medium">{{ t('initiative.tracker.colItem') }}</th>
332
- <th class="px-3 py-2 font-medium">{{ t('initiative.tracker.colStatus') }}</th>
333
- <th class="px-3 py-2 font-medium">{{ t('initiative.tracker.colPr') }}</th>
334
- </tr>
335
- </thead>
336
- <tbody>
337
- <tr
338
- v-for="item in itemsOf(phase.id)"
339
- :key="item.id"
340
- class="border-b border-slate-800/60 last:border-0"
341
- >
342
- <td class="px-3 py-2 align-top">
343
- <div class="font-medium text-slate-200">{{ item.title }}</div>
344
- <div v-if="item.dependsOn?.length" class="mt-0.5 text-[10px] text-slate-500">
345
- {{
346
- t('initiative.tracker.dependsOn', {
347
- items: item.dependsOn.join(', '),
348
- })
349
- }}
350
- </div>
351
- <div v-if="item.note" class="mt-0.5 text-[10px] text-amber-300/80">
352
- {{ item.note }}
353
- </div>
354
- <div
355
- v-if="editable && (item.status === 'blocked' || item.status === 'pending')"
356
- class="mt-1 flex gap-1.5"
357
- >
499
+ <!-- Logs -->
500
+ <section v-if="initiative.decisions?.length" class="mb-4">
501
+ <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
502
+ {{ t('initiative.tracker.decisions') }}
503
+ </h3>
504
+ <ul class="list-inside list-disc text-[13px] text-slate-300">
505
+ <li v-for="d in initiative.decisions" :key="d.id">
506
+ <span class="font-medium">{{ d.title }}</span>
507
+ <span v-if="d.detail" class="text-slate-400"> — {{ d.detail }}</span>
508
+ </li>
509
+ </ul>
510
+ </section>
511
+ <section v-if="initiative.deviations?.length" class="mb-4">
512
+ <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
513
+ {{ t('initiative.tracker.deviations') }}
514
+ </h3>
515
+ <ul class="list-inside list-disc text-[13px] text-slate-300">
516
+ <li v-for="d in initiative.deviations" :key="d.id">
517
+ <code v-if="d.itemId" class="text-slate-400">{{ d.itemId }}</code>
518
+ {{ d.description }}
519
+ <span v-if="d.resolution" class="text-slate-400"> → {{ d.resolution }}</span>
520
+ </li>
521
+ </ul>
522
+ </section>
523
+ <section v-if="initiative.followUps?.length" class="mb-4">
524
+ <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
525
+ {{ t('initiative.tracker.followUps') }}
526
+ </h3>
527
+ <ul class="flex flex-col gap-2 text-[13px] text-slate-300">
528
+ <li
529
+ v-for="f in initiative.followUps"
530
+ :key="f.id"
531
+ class="rounded-lg border border-slate-800 p-2.5"
532
+ :data-testid="`initiative-followup-${f.id}`"
533
+ >
534
+ <div class="flex items-start gap-2">
535
+ <div class="min-w-0 flex-1">
536
+ <span class="font-medium">{{ f.title }}</span>
537
+ <span v-if="f.detail" class="text-slate-400"> — {{ f.detail }}</span>
538
+ </div>
539
+ <UBadge
540
+ :color="INITIATIVE_FOLLOWUP_STATUS_CHIPS[f.status]"
541
+ variant="subtle"
542
+ size="sm"
543
+ >
544
+ {{ t(INITIATIVE_FOLLOWUP_STATUS_LABEL_KEYS[f.status]) }}
545
+ </UBadge>
546
+ </div>
547
+ <!-- Triage actions for an open follow-up (only while executing) -->
548
+ <div v-if="editable && f.status === 'open'" class="mt-2">
549
+ <div v-if="promotingId === f.id" class="flex flex-col gap-2">
550
+ <label class="flex items-center gap-2 text-[12px]">
551
+ <span class="text-slate-400">{{ t('initiative.curation.phaseField') }}</span>
552
+ <select
553
+ v-model="promoteForm.phaseId"
554
+ class="flex-1 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-slate-200"
555
+ data-testid="initiative-promote-phase"
556
+ >
557
+ <option v-for="p in phases" :key="p.id" :value="p.id">
558
+ {{ p.title }}
559
+ </option>
560
+ </select>
561
+ </label>
562
+ <input
563
+ v-model="promoteForm.title"
564
+ type="text"
565
+ class="rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[12px] text-slate-200"
566
+ :placeholder="t('initiative.curation.itemTitlePlaceholder')"
567
+ data-testid="initiative-promote-title"
568
+ />
569
+ <div class="flex gap-2">
358
570
  <button
359
- v-if="item.status === 'blocked'"
360
- class="rounded border border-slate-700 px-1.5 py-0.5 text-[10px] text-slate-300 hover:bg-slate-800 disabled:opacity-50"
361
- :disabled="initiatives.curating"
362
- :data-testid="`initiative-item-retry-${item.id}`"
363
- @click="itemAction(item, 'retry')"
571
+ class="rounded bg-indigo-600 px-2 py-1 text-[11px] text-white hover:bg-indigo-500 disabled:opacity-50"
572
+ :disabled="initiatives.curating || !promoteForm.phaseId"
573
+ data-testid="initiative-promote-submit"
574
+ @click="submitPromote(f)"
364
575
  >
365
- {{ t('initiative.curation.retry') }}
576
+ {{ t('initiative.curation.promoteConfirm') }}
366
577
  </button>
367
578
  <button
368
- class="rounded border border-slate-700 px-1.5 py-0.5 text-[10px] text-slate-300 hover:bg-slate-800 disabled:opacity-50"
369
- :disabled="initiatives.curating"
370
- :data-testid="`initiative-item-skip-${item.id}`"
371
- @click="itemAction(item, 'skip')"
579
+ class="rounded border border-slate-700 px-2 py-1 text-[11px] text-slate-300 hover:bg-slate-800"
580
+ @click="promotingId = null"
372
581
  >
373
- {{ t('initiative.curation.skip') }}
582
+ {{ t('initiative.curation.cancel') }}
374
583
  </button>
375
584
  </div>
376
- </td>
377
- <td class="px-3 py-2 align-top">
378
- <UBadge
379
- :color="INITIATIVE_ITEM_STATUS_CHIPS[item.status]"
380
- variant="subtle"
381
- size="sm"
382
- >
383
- {{ t(INITIATIVE_ITEM_STATUS_LABEL_KEYS[item.status]) }}
384
- </UBadge>
385
- </td>
386
- <td class="px-3 py-2 align-top">
387
- <a
388
- v-if="item.pr"
389
- :href="item.pr.url"
390
- target="_blank"
391
- rel="noopener"
392
- class="text-sky-400 hover:underline"
393
- >
394
- {{ item.pr.number ? `#${item.pr.number}` : t('initiative.tracker.prLink') }}
395
- </a>
396
- <span v-else class="text-slate-600">—</span>
397
- </td>
398
- </tr>
399
- </tbody>
400
- </table>
401
- </div>
402
- </section>
403
-
404
- <!-- Execution policy -->
405
- <section v-if="initiative.policy" class="mb-4">
406
- <div class="mb-1 flex items-center gap-2">
407
- <h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
408
- {{ t('initiative.tracker.policy') }}
409
- </h3>
410
- <button
411
- v-if="editable && !editingPolicy"
412
- class="rounded border border-slate-700 px-1.5 py-0.5 text-[10px] text-slate-300 hover:bg-slate-800"
413
- data-testid="initiative-policy-edit"
414
- @click="startEditPolicy"
415
- >
416
- {{ t('initiative.curation.edit') }}
417
- </button>
418
- </div>
419
- <ul v-if="!editingPolicy" class="text-[12px] text-slate-300">
420
- <li>
421
- {{
422
- t('initiative.tracker.maxConcurrent', {
423
- count: initiative.policy.maxConcurrent,
424
- })
425
- }}
426
- </li>
427
- <li v-for="(rule, i) in policyRules" :key="i">
428
- <code class="text-sky-300">{{ rule.pipelineId }}</code>
429
- · {{ ruleAxes(rule) }}
430
- </li>
431
- <li>
432
- {{ t('initiative.tracker.defaultPipeline') }}
433
- <code class="text-sky-300">{{ initiative.policy.defaultPipelineId }}</code>
434
- </li>
435
- </ul>
436
- <!-- Edit form: the two scalar knobs; planner-authored rules are preserved. -->
437
- <div v-else class="flex flex-col gap-2 rounded-lg border border-slate-800 p-3">
438
- <label class="flex items-center gap-2 text-[12px] text-slate-300">
439
- <span class="w-40">{{ t('initiative.curation.maxConcurrentField') }}</span>
440
- <input
441
- v-model.number="policyForm.maxConcurrent"
442
- type="number"
443
- min="1"
444
- max="20"
445
- class="w-20 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-slate-200"
446
- data-testid="initiative-policy-max-concurrent"
447
- />
448
- </label>
449
- <label class="flex items-center gap-2 text-[12px] text-slate-300">
450
- <span class="w-40">{{ t('initiative.curation.defaultPipelineField') }}</span>
451
- <input
452
- v-model="policyForm.defaultPipelineId"
453
- type="text"
454
- class="flex-1 rounded border border-slate-700 bg-slate-950 px-2 py-1 font-mono text-[11px] text-slate-200"
455
- data-testid="initiative-policy-default-pipeline"
456
- />
457
- </label>
458
- <div class="flex gap-2">
459
- <button
460
- class="rounded bg-indigo-600 px-2 py-1 text-[11px] text-white hover:bg-indigo-500 disabled:opacity-50"
461
- :disabled="initiatives.curating"
462
- data-testid="initiative-policy-save"
463
- @click="savePolicy"
464
- >
465
- {{ t('initiative.curation.save') }}
466
- </button>
467
- <button
468
- class="rounded border border-slate-700 px-2 py-1 text-[11px] text-slate-300 hover:bg-slate-800"
469
- @click="editingPolicy = false"
470
- >
471
- {{ t('initiative.curation.cancel') }}
472
- </button>
473
- </div>
474
- </div>
475
- </section>
476
-
477
- <!-- Logs -->
478
- <section v-if="initiative.decisions?.length" class="mb-4">
479
- <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
480
- {{ t('initiative.tracker.decisions') }}
481
- </h3>
482
- <ul class="list-inside list-disc text-[13px] text-slate-300">
483
- <li v-for="d in initiative.decisions" :key="d.id">
484
- <span class="font-medium">{{ d.title }}</span>
485
- <span v-if="d.detail" class="text-slate-400"> — {{ d.detail }}</span>
486
- </li>
487
- </ul>
488
- </section>
489
- <section v-if="initiative.deviations?.length" class="mb-4">
490
- <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
491
- {{ t('initiative.tracker.deviations') }}
492
- </h3>
493
- <ul class="list-inside list-disc text-[13px] text-slate-300">
494
- <li v-for="d in initiative.deviations" :key="d.id">
495
- <code v-if="d.itemId" class="text-slate-400">{{ d.itemId }}</code>
496
- {{ d.description }}
497
- <span v-if="d.resolution" class="text-slate-400"> → {{ d.resolution }}</span>
498
- </li>
499
- </ul>
500
- </section>
501
- <section v-if="initiative.followUps?.length" class="mb-4">
502
- <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
503
- {{ t('initiative.tracker.followUps') }}
504
- </h3>
505
- <ul class="flex flex-col gap-2 text-[13px] text-slate-300">
506
- <li
507
- v-for="f in initiative.followUps"
508
- :key="f.id"
509
- class="rounded-lg border border-slate-800 p-2.5"
510
- :data-testid="`initiative-followup-${f.id}`"
511
- >
512
- <div class="flex items-start gap-2">
513
- <div class="min-w-0 flex-1">
514
- <span class="font-medium">{{ f.title }}</span>
515
- <span v-if="f.detail" class="text-slate-400"> — {{ f.detail }}</span>
516
- </div>
517
- <UBadge
518
- :color="INITIATIVE_FOLLOWUP_STATUS_CHIPS[f.status]"
519
- variant="subtle"
520
- size="sm"
521
- >
522
- {{ t(INITIATIVE_FOLLOWUP_STATUS_LABEL_KEYS[f.status]) }}
523
- </UBadge>
524
- </div>
525
- <!-- Triage actions for an open follow-up (only while executing) -->
526
- <div v-if="editable && f.status === 'open'" class="mt-2">
527
- <div v-if="promotingId === f.id" class="flex flex-col gap-2">
528
- <label class="flex items-center gap-2 text-[12px]">
529
- <span class="text-slate-400">{{ t('initiative.curation.phaseField') }}</span>
530
- <select
531
- v-model="promoteForm.phaseId"
532
- class="flex-1 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-slate-200"
533
- data-testid="initiative-promote-phase"
534
- >
535
- <option v-for="p in phases" :key="p.id" :value="p.id">
536
- {{ p.title }}
537
- </option>
538
- </select>
539
- </label>
540
- <input
541
- v-model="promoteForm.title"
542
- type="text"
543
- class="rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[12px] text-slate-200"
544
- :placeholder="t('initiative.curation.itemTitlePlaceholder')"
545
- data-testid="initiative-promote-title"
546
- />
547
- <div class="flex gap-2">
585
+ </div>
586
+ <div v-else class="flex gap-1.5">
548
587
  <button
549
- class="rounded bg-indigo-600 px-2 py-1 text-[11px] text-white hover:bg-indigo-500 disabled:opacity-50"
550
- :disabled="initiatives.curating || !promoteForm.phaseId"
551
- data-testid="initiative-promote-submit"
552
- @click="submitPromote(f)"
588
+ class="rounded border border-slate-700 px-1.5 py-0.5 text-[10px] text-slate-300 hover:bg-slate-800"
589
+ data-testid="initiative-followup-promote"
590
+ @click="startPromote(f)"
553
591
  >
554
- {{ t('initiative.curation.promoteConfirm') }}
592
+ {{ t('initiative.curation.promote') }}
555
593
  </button>
556
594
  <button
557
- class="rounded border border-slate-700 px-2 py-1 text-[11px] text-slate-300 hover:bg-slate-800"
558
- @click="promotingId = null"
595
+ class="rounded border border-slate-700 px-1.5 py-0.5 text-[10px] text-slate-300 hover:bg-slate-800 disabled:opacity-50"
596
+ :disabled="initiatives.curating"
597
+ data-testid="initiative-followup-dismiss"
598
+ @click="dismissFollowUp(f)"
559
599
  >
560
- {{ t('initiative.curation.cancel') }}
600
+ {{ t('initiative.curation.dismiss') }}
561
601
  </button>
562
602
  </div>
563
603
  </div>
564
- <div v-else class="flex gap-1.5">
565
- <button
566
- class="rounded border border-slate-700 px-1.5 py-0.5 text-[10px] text-slate-300 hover:bg-slate-800"
567
- data-testid="initiative-followup-promote"
568
- @click="startPromote(f)"
569
- >
570
- {{ t('initiative.curation.promote') }}
571
- </button>
572
- <button
573
- class="rounded border border-slate-700 px-1.5 py-0.5 text-[10px] text-slate-300 hover:bg-slate-800 disabled:opacity-50"
574
- :disabled="initiatives.curating"
575
- data-testid="initiative-followup-dismiss"
576
- @click="dismissFollowUp(f)"
577
- >
578
- {{ t('initiative.curation.dismiss') }}
579
- </button>
580
- </div>
581
- </div>
582
- </li>
583
- </ul>
584
- </section>
585
- <section v-if="initiative.caveats?.length" class="mb-4">
586
- <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
587
- {{ t('initiative.tracker.caveats') }}
588
- </h3>
589
- <ul class="list-inside list-disc text-[13px] text-slate-300">
590
- <li v-for="(c, i) in initiative.caveats" :key="i">{{ c }}</li>
591
- </ul>
592
- </section>
593
- </template>
604
+ </li>
605
+ </ul>
606
+ </section>
607
+ <section v-if="initiative.caveats?.length" class="mb-4">
608
+ <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
609
+ {{ t('initiative.tracker.caveats') }}
610
+ </h3>
611
+ <ul class="list-inside list-disc text-[13px] text-slate-300">
612
+ <li v-for="(c, i) in initiative.caveats" :key="i">{{ c }}</li>
613
+ </ul>
614
+ </section>
615
+ </template>
616
+ </div>
617
+
618
+ <!-- Run details: the shared run-metadata + LLM model-activity block every agent window
619
+ carries (step position, live duration, model, run id, calls + token usage). Resolved
620
+ through `useResultViewRunMeta`, so it is present on the card / inspector entry point
621
+ too — where this window carries no step index of its own. -->
622
+ <aside
623
+ v-if="metaStep"
624
+ data-testid="initiative-tracker-run-meta"
625
+ 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"
626
+ >
627
+ <StepRunMeta
628
+ :step="metaStep"
629
+ :instance-id="runId"
630
+ :step-number="position"
631
+ :total-steps="totalSteps"
632
+ :run-failed="runFailed"
633
+ :failure-at="failureAt"
634
+ />
635
+ </aside>
594
636
  </div>
595
637
  </ResultWindowShell>
596
638
  </template>