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