@cat-factory/app 0.280.0 → 0.280.2

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.
Files changed (55) hide show
  1. package/README.md +26 -1
  2. package/app/components/binaryCandidates/BinaryCandidatesWindow.vue +142 -5
  3. package/app/components/board/AddTaskModal.vue +9 -0
  4. package/app/components/board/ReviewFrictionDialog.vue +35 -4
  5. package/app/components/board/TaskDependencyEdges.vue +26 -15
  6. package/app/components/board/nodes/TaskCard.vue +6 -1
  7. package/app/components/brainstorm/BrainstormWindow.vue +19 -1
  8. package/app/components/common/AsyncViewError.vue +30 -0
  9. package/app/components/common/ConfirmDialog.vue +26 -0
  10. package/app/components/docs/DocInterviewWindow.vue +40 -38
  11. package/app/components/followUp/FollowUpWindow.vue +32 -2
  12. package/app/components/forkDecision/ForkDecisionWindow.vue +44 -5
  13. package/app/components/gates/GateResultView.vue +14 -1
  14. package/app/components/humanTest/HumanTestWindow.vue +13 -1
  15. package/app/components/initiative/InitiativePlanDecision.vue +16 -3
  16. package/app/components/initiative/InitiativePlanReview.vue +16 -1
  17. package/app/components/initiative/InitiativePlanningWindow.vue +50 -52
  18. package/app/components/initiative/InitiativeTrackerWindow.vue +59 -1
  19. package/app/components/judge/JudgeResultView.vue +14 -1
  20. package/app/components/panels/AgentStepDetail.vue +651 -659
  21. package/app/components/panels/InspectorPanel.vue +6 -0
  22. package/app/components/panels/ResultWindowDrafts.logic.spec.ts +233 -0
  23. package/app/components/panels/inspector/ServiceTestSecrets.vue +17 -6
  24. package/app/components/pipeline/PipelineHealthModal.vue +55 -16
  25. package/app/components/prReview/PrReviewWindow.vue +23 -4
  26. package/app/components/visualConfirm/VisualConfirmationWindow.vue +19 -1
  27. package/app/composables/useBoardActivity.ts +62 -6
  28. package/app/composables/useConfirm.spec.ts +62 -0
  29. package/app/composables/useConfirm.ts +6 -1
  30. package/app/composables/useInterviewDrafts.spec.ts +198 -0
  31. package/app/composables/useInterviewDrafts.ts +184 -0
  32. package/app/composables/useTaskExpansion.ts +10 -25
  33. package/app/docs/consumer-extensions.md +9 -0
  34. package/app/modular/result-views.ts +58 -21
  35. package/app/pages/index.vue +68 -64
  36. package/app/stores/binaryCandidates.ts +26 -3
  37. package/app/stores/ui/modals.ts +11 -0
  38. package/app/utils/asyncView.ts +24 -0
  39. package/app/utils/binaryCandidates.spec.ts +45 -1
  40. package/app/utils/binaryCandidates.ts +33 -0
  41. package/app/utils/blockRects.spec.ts +82 -0
  42. package/app/utils/blockRects.ts +61 -0
  43. package/app/utils/boardWakeGate.spec.ts +101 -0
  44. package/app/utils/boardWakeGate.ts +78 -0
  45. package/i18n/locales/de.json +25 -2
  46. package/i18n/locales/en.json +25 -2
  47. package/i18n/locales/es.json +25 -2
  48. package/i18n/locales/fr.json +25 -2
  49. package/i18n/locales/he.json +25 -2
  50. package/i18n/locales/it.json +25 -2
  51. package/i18n/locales/ja.json +25 -2
  52. package/i18n/locales/pl.json +25 -2
  53. package/i18n/locales/tr.json +25 -2
  54. package/i18n/locales/uk.json +25 -2
  55. package/package.json +1 -1
@@ -356,710 +356,702 @@ async function copyOutput() {
356
356
  </script>
357
357
 
358
358
  <template>
359
- <Teleport to="body">
360
- <Transition name="reader-fade">
361
- <!-- `data-agent-kind` names WHICH step this window is scoped to, as the kind rather than as
362
- the heading beside it: the heading is a translated display label, so it is the wrong
363
- thing to read for anything that needs to know which agent's step is open. -->
364
- <div
365
- v-if="open && step && agent"
366
- data-testid="step-detail"
367
- :data-agent-kind="step?.agentKind"
368
- class="fixed inset-0 z-50 flex max-h-[100dvh] bg-slate-950/96 backdrop-blur-sm"
369
- role="dialog"
370
- aria-modal="true"
371
- >
372
- <!-- ToC sidebar (only meaningful when there are prose headings) -->
373
- <aside
374
- v-if="outline.hasToc"
375
- data-testid="step-detail-toc"
376
- class="hidden w-72 shrink-0 flex-col border-e border-slate-800 bg-slate-900/60 md:flex"
359
+ <!-- The Teleport and the reader-fade Transition are owned by the parent mount
360
+ (pages/index.vue), so the fade plays in BOTH directions across the v-if that mounts and
361
+ unmounts this chunk. An inner Transition cannot: it never animates its own first render
362
+ (a Transition without `appear` treats a mount as no transition at all), and it is torn
363
+ down before it could run a leave. Same split, and the same reason, as BlockFocusView. -->
364
+ <!-- `data-agent-kind` names WHICH step this window is scoped to, as the kind rather than as
365
+ the heading beside it: the heading is a translated display label, so it is the wrong
366
+ thing to read for anything that needs to know which agent's step is open. -->
367
+ <div
368
+ v-if="open && step && agent"
369
+ data-testid="step-detail"
370
+ :data-agent-kind="step?.agentKind"
371
+ class="fixed inset-0 z-50 flex max-h-[100dvh] bg-slate-950/96 backdrop-blur-sm"
372
+ role="dialog"
373
+ aria-modal="true"
374
+ >
375
+ <!-- ToC sidebar (only meaningful when there are prose headings) -->
376
+ <aside
377
+ v-if="outline.hasToc"
378
+ data-testid="step-detail-toc"
379
+ class="hidden w-72 shrink-0 flex-col border-e border-slate-800 bg-slate-900/60 md:flex"
380
+ >
381
+ <div class="border-b border-slate-800 px-4 py-3">
382
+ <div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
383
+ {{ t('panels.stepDetail.contents') }}
384
+ </div>
385
+ </div>
386
+ <nav class="flex-1 space-y-0.5 overflow-auto px-2 py-3">
387
+ <button
388
+ class="block w-full truncate rounded-md px-2 py-1 text-start text-[13px] transition"
389
+ :class="
390
+ activeId === 'step-details'
391
+ ? 'bg-indigo-500/15 font-medium text-indigo-200'
392
+ : 'text-slate-400 hover:bg-slate-800/60 hover:text-slate-200'
393
+ "
394
+ @click="goTo('step-details')"
377
395
  >
378
- <div class="border-b border-slate-800 px-4 py-3">
379
- <div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
380
- {{ t('panels.stepDetail.contents') }}
381
- </div>
382
- </div>
383
- <nav class="flex-1 space-y-0.5 overflow-auto px-2 py-3">
384
- <button
385
- class="block w-full truncate rounded-md px-2 py-1 text-start text-[13px] transition"
386
- :class="
387
- activeId === 'step-details'
388
- ? 'bg-indigo-500/15 font-medium text-indigo-200'
389
- : 'text-slate-400 hover:bg-slate-800/60 hover:text-slate-200'
390
- "
391
- @click="goTo('step-details')"
392
- >
393
- {{ t('panels.stepDetail.details') }}
394
- </button>
395
- <button
396
- v-for="s in tocSections"
397
- :key="s.id"
398
- class="block w-full truncate rounded-md px-2 py-1 text-start text-[13px] transition"
399
- :class="
400
- activeId === s.id
401
- ? 'bg-indigo-500/15 font-medium text-indigo-200'
402
- : 'text-slate-400 hover:bg-slate-800/60 hover:text-slate-200'
403
- "
404
- :style="{ paddingLeft: `${(s.depth - outline.minDepth) * 0.85 + 0.5}rem` }"
405
- :title="s.title"
406
- @click="goTo(s.id)"
407
- >
408
- {{ s.title }}
409
- </button>
410
- </nav>
411
- </aside>
396
+ {{ t('panels.stepDetail.details') }}
397
+ </button>
398
+ <button
399
+ v-for="s in tocSections"
400
+ :key="s.id"
401
+ class="block w-full truncate rounded-md px-2 py-1 text-start text-[13px] transition"
402
+ :class="
403
+ activeId === s.id
404
+ ? 'bg-indigo-500/15 font-medium text-indigo-200'
405
+ : 'text-slate-400 hover:bg-slate-800/60 hover:text-slate-200'
406
+ "
407
+ :style="{ paddingLeft: `${(s.depth - outline.minDepth) * 0.85 + 0.5}rem` }"
408
+ :title="s.title"
409
+ @click="goTo(s.id)"
410
+ >
411
+ {{ s.title }}
412
+ </button>
413
+ </nav>
414
+ </aside>
412
415
 
413
- <!-- main column -->
414
- <div class="flex min-w-0 flex-1 flex-col">
415
- <header class="flex items-center gap-3 border-b border-slate-800 px-6 py-4">
416
- <div
417
- class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg"
418
- :style="{ backgroundColor: agent.color + '22' }"
419
- >
420
- <UIcon :name="agent.icon" class="h-5 w-5" :style="{ color: agent.color }" />
421
- </div>
422
- <div class="min-w-0">
423
- <h1 class="truncate text-base font-semibold text-white">{{ agent.label }}</h1>
424
- <p v-if="block" class="truncate text-xs text-slate-500">{{ block.title }}</p>
425
- </div>
426
- <div class="ms-auto flex items-center gap-1.5">
427
- <UBadge
428
- v-if="genericApprovalPending"
429
- color="warning"
430
- variant="subtle"
431
- size="sm"
432
- class="me-1"
433
- >
434
- <UIcon name="i-lucide-shield-check" class="me-1 h-3 w-3" />
435
- {{ t('panels.stepDetail.approvalRequired') }}
436
- </UBadge>
437
- <UBadge
438
- v-else-if="companionExceeded || dedicatedPark"
439
- color="warning"
440
- variant="subtle"
441
- size="sm"
442
- class="me-1"
443
- >
444
- <UIcon name="i-lucide-alert-triangle" class="me-1 h-3 w-3" />
445
- {{ t('panels.stepDetail.decisionRequired') }}
446
- </UBadge>
447
- <UButton
448
- v-if="outline.sections.length"
449
- :icon="allCollapsed ? 'i-lucide-unfold-vertical' : 'i-lucide-fold-vertical'"
450
- color="neutral"
451
- variant="ghost"
452
- size="sm"
453
- :title="
454
- allCollapsed
455
- ? t('panels.stepDetail.expandAll')
456
- : t('panels.stepDetail.collapseAll')
457
- "
458
- @click="setAll(!allCollapsed)"
459
- />
460
- <UButton
461
- v-if="hasOutput"
462
- icon="i-lucide-copy"
463
- color="neutral"
464
- variant="ghost"
465
- size="sm"
466
- :title="t('panels.stepDetail.copyRawOutput')"
467
- @click="copyOutput"
468
- />
469
- <!-- Restart the pipeline from this step (shared two-click confirm; resetting
470
- later steps is destructive). Keyed on the step so its armed state resets
471
- when a different step opens within this overlay. -->
472
- <StepRestartControl
473
- :key="`${ctx?.instanceId}:${ctx?.stepIndex}`"
474
- :instance-id="ctx?.instanceId ?? null"
475
- :step-index="ctx?.stepIndex ?? null"
476
- @restarted="close"
477
- />
478
- <UButton
479
- icon="i-lucide-x"
480
- color="neutral"
481
- variant="ghost"
482
- size="sm"
483
- :title="t('panels.stepDetail.closeEsc')"
484
- @click="close"
485
- />
486
- </div>
487
- </header>
416
+ <!-- main column -->
417
+ <div class="flex min-w-0 flex-1 flex-col">
418
+ <header class="flex items-center gap-3 border-b border-slate-800 px-6 py-4">
419
+ <div
420
+ class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg"
421
+ :style="{ backgroundColor: agent.color + '22' }"
422
+ >
423
+ <UIcon :name="agent.icon" class="h-5 w-5" :style="{ color: agent.color }" />
424
+ </div>
425
+ <div class="min-w-0">
426
+ <h1 class="truncate text-base font-semibold text-white">{{ agent.label }}</h1>
427
+ <p v-if="block" class="truncate text-xs text-slate-500">{{ block.title }}</p>
428
+ </div>
429
+ <div class="ms-auto flex items-center gap-1.5">
430
+ <UBadge
431
+ v-if="genericApprovalPending"
432
+ color="warning"
433
+ variant="subtle"
434
+ size="sm"
435
+ class="me-1"
436
+ >
437
+ <UIcon name="i-lucide-shield-check" class="me-1 h-3 w-3" />
438
+ {{ t('panels.stepDetail.approvalRequired') }}
439
+ </UBadge>
440
+ <UBadge
441
+ v-else-if="companionExceeded || dedicatedPark"
442
+ color="warning"
443
+ variant="subtle"
444
+ size="sm"
445
+ class="me-1"
446
+ >
447
+ <UIcon name="i-lucide-alert-triangle" class="me-1 h-3 w-3" />
448
+ {{ t('panels.stepDetail.decisionRequired') }}
449
+ </UBadge>
450
+ <UButton
451
+ v-if="outline.sections.length"
452
+ :icon="allCollapsed ? 'i-lucide-unfold-vertical' : 'i-lucide-fold-vertical'"
453
+ color="neutral"
454
+ variant="ghost"
455
+ size="sm"
456
+ :title="
457
+ allCollapsed ? t('panels.stepDetail.expandAll') : t('panels.stepDetail.collapseAll')
458
+ "
459
+ @click="setAll(!allCollapsed)"
460
+ />
461
+ <UButton
462
+ v-if="hasOutput"
463
+ icon="i-lucide-copy"
464
+ color="neutral"
465
+ variant="ghost"
466
+ size="sm"
467
+ :title="t('panels.stepDetail.copyRawOutput')"
468
+ @click="copyOutput"
469
+ />
470
+ <!-- Restart the pipeline from this step (shared two-click confirm; resetting
471
+ later steps is destructive). Keyed on the step so its armed state resets
472
+ when a different step opens within this overlay. -->
473
+ <StepRestartControl
474
+ :key="`${ctx?.instanceId}:${ctx?.stepIndex}`"
475
+ :instance-id="ctx?.instanceId ?? null"
476
+ :step-index="ctx?.stepIndex ?? null"
477
+ @restarted="close"
478
+ />
479
+ <UButton
480
+ icon="i-lucide-x"
481
+ color="neutral"
482
+ variant="ghost"
483
+ size="sm"
484
+ :title="t('panels.stepDetail.closeEsc')"
485
+ @click="close"
486
+ />
487
+ </div>
488
+ </header>
488
489
 
489
- <div ref="scrollEl" class="flex-1 overflow-auto px-6 py-6" @scroll="onScroll">
490
- <div class="mx-auto max-w-3xl space-y-5">
491
- <!-- metadata card (always shown) -->
492
- <section
493
- id="step-details"
494
- :ref="(el) => (sectionEls['step-details'] = el as HTMLElement | null)"
495
- class="scroll-mt-4 rounded-xl border border-slate-800 bg-slate-900/50 p-4"
496
- >
497
- <StepMetadataCard
498
- :step="step"
499
- :run-failed="runFailed"
500
- :run-active="runActive"
501
- :duration-label="durationLabel"
502
- :is-running="isRunning"
503
- :step-number="stepNumber"
504
- :total-steps="totalSteps"
505
- :instance-id="instance?.id"
506
- :companion-verdicts="companionVerdicts"
507
- :latest-verdict="latestVerdict"
508
- />
509
- </section>
490
+ <div ref="scrollEl" class="flex-1 overflow-auto px-6 py-6" @scroll="onScroll">
491
+ <div class="mx-auto max-w-3xl space-y-5">
492
+ <!-- metadata card (always shown) -->
493
+ <section
494
+ id="step-details"
495
+ :ref="(el) => (sectionEls['step-details'] = el as HTMLElement | null)"
496
+ class="scroll-mt-4 rounded-xl border border-slate-800 bg-slate-900/50 p-4"
497
+ >
498
+ <StepMetadataCard
499
+ :step="step"
500
+ :run-failed="runFailed"
501
+ :run-active="runActive"
502
+ :duration-label="durationLabel"
503
+ :is-running="isRunning"
504
+ :step-number="stepNumber"
505
+ :total-steps="totalSteps"
506
+ :instance-id="instance?.id"
507
+ :companion-verdicts="companionVerdicts"
508
+ :latest-verdict="latestVerdict"
509
+ />
510
+ </section>
510
511
 
511
- <!-- post-run Kaizen grading status + results for this step (run-details only) -->
512
- <KaizenStepStatus
513
- :instance-id="ctx?.instanceId ?? null"
514
- :step-index="ctx?.stepIndex ?? null"
515
- />
512
+ <!-- post-run Kaizen grading status + results for this step (run-details only) -->
513
+ <KaizenStepStatus
514
+ :instance-id="ctx?.instanceId ?? null"
515
+ :step-index="ctx?.stepIndex ?? null"
516
+ />
516
517
 
517
- <!-- companion rework budget spent, OR the loop abandoned early as unproductive,
518
- with or without must-fix findings still open: the shared iteration-cap decision
519
- (one more round / proceed with the current output / stop & reset). One prompt,
520
- and the two slots are picked on different facts — the choices are the same but
521
- the reason is not, the spent-limit wording is untrue of a stalled loop, and an
522
- open blocker is what the person is actually being asked to overrule. -->
523
- <IterationCapPrompt
524
- v-if="companionExceeded"
525
- :heading="
526
- companionStalled
527
- ? t('panels.stepDetail.companionStalledHeading', {
518
+ <!-- companion rework budget spent, OR the loop abandoned early as unproductive,
519
+ with or without must-fix findings still open: the shared iteration-cap decision
520
+ (one more round / proceed with the current output / stop & reset). One prompt,
521
+ and the two slots are picked on different facts — the choices are the same but
522
+ the reason is not, the spent-limit wording is untrue of a stalled loop, and an
523
+ open blocker is what the person is actually being asked to overrule. -->
524
+ <IterationCapPrompt
525
+ v-if="companionExceeded"
526
+ :heading="
527
+ companionStalled
528
+ ? t('panels.stepDetail.companionStalledHeading', {
529
+ agent: agent.label,
530
+ attempts: step.companion?.attempts,
531
+ maxAttempts: step.companion?.maxAttempts,
532
+ })
533
+ : blockingFindings.length
534
+ ? t(
535
+ 'panels.stepDetail.companionCapBlockedHeading',
536
+ {
528
537
  agent: agent.label,
529
- attempts: step.companion?.attempts,
530
- maxAttempts: step.companion?.maxAttempts,
531
- })
532
- : blockingFindings.length
533
- ? t(
534
- 'panels.stepDetail.companionCapBlockedHeading',
535
- {
536
- agent: agent.label,
537
- attempts: step.companion?.maxAttempts,
538
- count: blockingFindings.length,
539
- },
540
- blockingFindings.length,
541
- )
542
- : t('panels.stepDetail.companionCapHeading', {
543
- agent: agent.label,
544
- attempts: step.companion?.maxAttempts,
545
- threshold: pctOf(latestVerdict?.threshold ?? 0),
546
- })
547
- "
548
- :detail="
549
- blockingFindings.length
550
- ? t('panels.stepDetail.companionCapBlockedDetail')
551
- : companionStalled
552
- ? t('panels.stepDetail.companionStalledDetail')
553
- : t('panels.stepDetail.companionCapDetail')
554
- "
555
- :loading="resolvingCap"
556
- @resolve="resolveCompanionCap"
557
- />
538
+ attempts: step.companion?.maxAttempts,
539
+ count: blockingFindings.length,
540
+ },
541
+ blockingFindings.length,
542
+ )
543
+ : t('panels.stepDetail.companionCapHeading', {
544
+ agent: agent.label,
545
+ attempts: step.companion?.maxAttempts,
546
+ threshold: pctOf(latestVerdict?.threshold ?? 0),
547
+ })
548
+ "
549
+ :detail="
550
+ blockingFindings.length
551
+ ? t('panels.stepDetail.companionCapBlockedDetail')
552
+ : companionStalled
553
+ ? t('panels.stepDetail.companionStalledDetail')
554
+ : t('panels.stepDetail.companionCapDetail')
555
+ "
556
+ :loading="resolvingCap"
557
+ @resolve="resolveCompanionCap"
558
+ />
558
559
 
559
- <!-- a park a dedicated window owns (fork choice / follow-up triage / candidate
560
- comparison): the generic approval rail can't resolve it (the server refuses),
561
- so point the human at the window that can. Copy and icon come from the shared
562
- per-park table, so a park added to the vocabulary can never inherit another
563
- one's wording here. -->
564
- <!-- the pre-dispatch input gate holds this step: answered here, in place -->
565
- <InputGateNotice
566
- v-if="inputGateVerdict && instance"
567
- :gate="inputGateVerdict"
568
- tone="blocked"
569
- :execution-id="instance.id"
570
- compact
571
- />
560
+ <!-- a park a dedicated window owns (fork choice / follow-up triage / candidate
561
+ comparison): the generic approval rail can't resolve it (the server refuses),
562
+ so point the human at the window that can. Copy and icon come from the shared
563
+ per-park table, so a park added to the vocabulary can never inherit another
564
+ one's wording here. -->
565
+ <!-- the pre-dispatch input gate holds this step: answered here, in place -->
566
+ <InputGateNotice
567
+ v-if="inputGateVerdict && instance"
568
+ :gate="inputGateVerdict"
569
+ tone="blocked"
570
+ :execution-id="instance.id"
571
+ compact
572
+ />
572
573
 
573
- <div
574
- v-if="parkPresentation"
575
- class="rounded-xl border border-amber-500/30 bg-amber-500/5 p-4"
576
- data-testid="dedicated-park-redirect"
577
- :data-park="dedicatedPark"
578
- >
579
- <p class="text-[13px] leading-relaxed text-amber-200/90">
580
- {{ t(parkPresentation.noticeKey) }}
581
- </p>
582
- <UButton
583
- class="mt-3"
584
- color="primary"
585
- size="sm"
586
- :icon="parkPresentation.icon"
587
- data-testid="dedicated-park-open"
588
- @click="openDedicatedWindow"
589
- >
590
- {{ t(parkPresentation.actionKey) }}
591
- </UButton>
592
- </div>
574
+ <div
575
+ v-if="parkPresentation"
576
+ class="rounded-xl border border-amber-500/30 bg-amber-500/5 p-4"
577
+ data-testid="dedicated-park-redirect"
578
+ :data-park="dedicatedPark"
579
+ >
580
+ <p class="text-[13px] leading-relaxed text-amber-200/90">
581
+ {{ t(parkPresentation.noticeKey) }}
582
+ </p>
583
+ <UButton
584
+ class="mt-3"
585
+ color="primary"
586
+ size="sm"
587
+ :icon="parkPresentation.icon"
588
+ data-testid="dedicated-park-open"
589
+ @click="openDedicatedWindow"
590
+ >
591
+ {{ t(parkPresentation.actionKey) }}
592
+ </UButton>
593
+ </div>
593
594
 
594
- <!-- ephemeral environment lifecycle (spinning up / running / shut down /
595
- errored + the exact error), when this step runs against one -->
596
- <EnvironmentStatusPanel
597
- v-if="stepEnvironment"
598
- :environment="stepEnvironment"
599
- :run-active="runActive"
600
- />
595
+ <!-- ephemeral environment lifecycle (spinning up / running / shut down /
596
+ errored + the exact error), when this step runs against one -->
597
+ <EnvironmentStatusPanel
598
+ v-if="stepEnvironment"
599
+ :environment="stepEnvironment"
600
+ :run-active="runActive"
601
+ />
601
602
 
602
- <!-- frontend UI-test: how the frame's backend bindings resolved (env var →
603
- live URL | mocked) + the run-start advisories (duplicate env vars /
604
- partially-mocked services) the engine stamped on the run. Rendered from the
605
- FROZEN start-time bindings so a finished run shows what it actually drove
606
- against, not a live re-resolution. -->
607
- <FrontendBindingsResolved
608
- v-if="frontendConfig"
609
- :config="frontendConfig"
610
- :resolved="frontendBindings"
611
- />
612
- <ul v-if="runNotes.length" class="space-y-1" data-testid="run-notes">
613
- <li
614
- v-for="(note, i) in runNotes"
615
- :key="i"
616
- class="flex items-start gap-1.5 text-[11px] leading-snug text-amber-300/80"
617
- >
618
- <UIcon name="i-lucide-info" class="mt-0.5 h-3.5 w-3.5 shrink-0" />
619
- <span>{{ note }}</span>
620
- </li>
621
- </ul>
603
+ <!-- frontend UI-test: how the frame's backend bindings resolved (env var →
604
+ live URL | mocked) + the run-start advisories (duplicate env vars /
605
+ partially-mocked services) the engine stamped on the run. Rendered from the
606
+ FROZEN start-time bindings so a finished run shows what it actually drove
607
+ against, not a live re-resolution. -->
608
+ <FrontendBindingsResolved
609
+ v-if="frontendConfig"
610
+ :config="frontendConfig"
611
+ :resolved="frontendBindings"
612
+ />
613
+ <ul v-if="runNotes.length" class="space-y-1" data-testid="run-notes">
614
+ <li
615
+ v-for="(note, i) in runNotes"
616
+ :key="i"
617
+ class="flex items-start gap-1.5 text-[11px] leading-snug text-amber-300/80"
618
+ >
619
+ <UIcon name="i-lucide-info" class="mt-0.5 h-3.5 w-3.5 shrink-0" />
620
+ <span>{{ note }}</span>
621
+ </li>
622
+ </ul>
622
623
 
623
- <!-- this run's infrastructure attempts (container/runner/env spin-up +
624
- tear-down): the surface for the per-run container log rows + the exact
625
- provider error, behind a toggle (most useful on a failed-to-start run) -->
626
- <div v-if="executionId">
627
- <UButton
628
- :icon="showProvisioning ? 'i-lucide-chevron-up' : 'i-lucide-scroll-text'"
629
- variant="ghost"
630
- size="xs"
631
- @click="
632
- () => {
633
- showProvisioning = !showProvisioning
634
- }
635
- "
636
- >
637
- {{
638
- showProvisioning
639
- ? t('panels.stepDetail.hideInfraAttempts')
640
- : t('panels.stepDetail.infraAttempts')
641
- }}
642
- </UButton>
643
- <ProvisioningLogsDrawer
644
- v-if="showProvisioning"
645
- class="mt-2"
646
- :execution-id="executionId"
647
- :live="runActive"
648
- />
649
- </div>
624
+ <!-- this run's infrastructure attempts (container/runner/env spin-up +
625
+ tear-down): the surface for the per-run container log rows + the exact
626
+ provider error, behind a toggle (most useful on a failed-to-start run) -->
627
+ <div v-if="executionId">
628
+ <UButton
629
+ :icon="showProvisioning ? 'i-lucide-chevron-up' : 'i-lucide-scroll-text'"
630
+ variant="ghost"
631
+ size="xs"
632
+ @click="
633
+ () => {
634
+ showProvisioning = !showProvisioning
635
+ }
636
+ "
637
+ >
638
+ {{
639
+ showProvisioning
640
+ ? t('panels.stepDetail.hideInfraAttempts')
641
+ : t('panels.stepDetail.infraAttempts')
642
+ }}
643
+ </UButton>
644
+ <ProvisioningLogsDrawer
645
+ v-if="showProvisioning"
646
+ class="mt-2"
647
+ :execution-id="executionId"
648
+ :live="runActive"
649
+ />
650
+ </div>
650
651
 
651
- <!-- this step's execution history (the run-level trail narrowed to this step),
652
- behind a toggle — a merged timeline of the SUCCESSFUL outputs a restart
653
- superseded and the FAILED attempts, scoped to the step being looked at -->
654
- <div v-if="hasStepHistory">
655
- <UButton
656
- :icon="showHistory ? 'i-lucide-chevron-up' : 'i-lucide-history'"
657
- variant="ghost"
658
- size="xs"
659
- data-testid="step-execution-history-toggle"
660
- @click="
661
- () => {
662
- showHistory = !showHistory
663
- }
664
- "
665
- >
666
- {{
667
- showHistory
668
- ? t('panels.stepDetail.hideExecutionHistory')
669
- : t('panels.stepDetail.executionHistory')
670
- }}
671
- </UButton>
672
- <StepExecutionHistory
673
- v-if="showHistory"
674
- class="mt-2"
675
- :failures="stepFailures"
676
- :outputs="stepOutputs"
677
- data-testid="step-execution-history"
678
- />
679
- </div>
652
+ <!-- this step's execution history (the run-level trail narrowed to this step),
653
+ behind a toggle — a merged timeline of the SUCCESSFUL outputs a restart
654
+ superseded and the FAILED attempts, scoped to the step being looked at -->
655
+ <div v-if="hasStepHistory">
656
+ <UButton
657
+ :icon="showHistory ? 'i-lucide-chevron-up' : 'i-lucide-history'"
658
+ variant="ghost"
659
+ size="xs"
660
+ data-testid="step-execution-history-toggle"
661
+ @click="
662
+ () => {
663
+ showHistory = !showHistory
664
+ }
665
+ "
666
+ >
667
+ {{
668
+ showHistory
669
+ ? t('panels.stepDetail.hideExecutionHistory')
670
+ : t('panels.stepDetail.executionHistory')
671
+ }}
672
+ </UButton>
673
+ <StepExecutionHistory
674
+ v-if="showHistory"
675
+ class="mt-2"
676
+ :failures="stepFailures"
677
+ :outputs="stepOutputs"
678
+ data-testid="step-execution-history"
679
+ />
680
+ </div>
680
681
 
681
- <!-- tester report: what was tested, the per-area outcomes, the concerns
682
- it raised and the greenlight verdict; plus the fixer-loop phase -->
683
- <StepTestReport v-if="testReport" :report="testReport" :phase="testPhase" />
682
+ <!-- tester report: what was tested, the per-area outcomes, the concerns
683
+ it raised and the greenlight verdict; plus the fixer-loop phase -->
684
+ <StepTestReport v-if="testReport" :report="testReport" :phase="testPhase" />
684
685
 
685
- <!-- code/PR reviewer's best-practice adherence: per standard, a 1..10 rating of how
686
- well the change adheres + the issues it surfaced. Only on a review step. -->
687
- <StepFragmentAdherence
688
- v-if="step.fragmentAdherence?.length"
689
- :items="step.fragmentAdherence"
690
- />
686
+ <!-- code/PR reviewer's best-practice adherence: per standard, a 1..10 rating of how
687
+ well the change adheres + the issues it surfaced. Only on a review step. -->
688
+ <StepFragmentAdherence
689
+ v-if="step.fragmentAdherence?.length"
690
+ :items="step.fragmentAdherence"
691
+ />
691
692
 
692
- <!-- container agent's effort self-assessment (how hard it was, what reduced its
693
- effectiveness, key obstacles). Only when the agent reported one. -->
694
- <StepEffortReport v-if="step.effortReport" :report="step.effortReport" />
693
+ <!-- container agent's effort self-assessment (how hard it was, what reduced its
694
+ effectiveness, key obstacles). Only when the agent reported one. -->
695
+ <StepEffortReport v-if="step.effortReport" :report="step.effortReport" />
695
696
 
696
- <!-- the tool servers (MCP) this dispatch wired, and the ones it dropped with the
697
- reason. Only on a container step, and self-hiding when the record holds
698
- nothing: a recorded pair of empty lists is a kind that declared none, which is
699
- every step on a deployment that registers no tool servers at all. -->
700
- <StepToolServers
701
- v-if="step.toolServers"
702
- :tool-servers="step.toolServers"
703
- :step-agent-kind="step.agentKind"
704
- />
697
+ <!-- the tool servers (MCP) this dispatch wired, and the ones it dropped with the
698
+ reason. Only on a container step, and self-hiding when the record holds
699
+ nothing: a recorded pair of empty lists is a kind that declared none, which is
700
+ every step on a deployment that registers no tool servers at all. -->
701
+ <StepToolServers
702
+ v-if="step.toolServers"
703
+ :tool-servers="step.toolServers"
704
+ :step-agent-kind="step.agentKind"
705
+ />
705
706
 
706
- <!-- the bugfix REPRODUCTION PROOF: the declared reproducing check run against the
707
- pre-fix tree and the final one, with both captured outputs, or the agent's
708
- structural declaration that the bug cannot be reproduced. This panel is the
709
- half that matters most: the engine records the proof on whichever step OPENED
710
- the pull request, and in every built-in pipeline that is the `coder` — a kind
711
- with no dedicated result view, so it opens HERE and the result-window shell is
712
- never involved. Self-hiding for a run that declared no reproducing check. -->
713
- <StepReproductionReport
714
- v-if="step.reproduction"
715
- :report="step.reproduction"
716
- variant="card"
717
- />
707
+ <!-- the bugfix REPRODUCTION PROOF: the declared reproducing check run against the
708
+ pre-fix tree and the final one, with both captured outputs, or the agent's
709
+ structural declaration that the bug cannot be reproduced. This panel is the
710
+ half that matters most: the engine records the proof on whichever step OPENED
711
+ the pull request, and in every built-in pipeline that is the `coder` — a kind
712
+ with no dedicated result view, so it opens HERE and the result-window shell is
713
+ never involved. Self-hiding for a run that declared no reproducing check. -->
714
+ <StepReproductionReport
715
+ v-if="step.reproduction"
716
+ :report="step.reproduction"
717
+ variant="card"
718
+ />
718
719
 
719
- <!-- what the step declared it stored through a foundational storage service, and
720
- every way that record is incomplete. This panel is the OTHER half of the
721
- result-window shell's trailing section: a step whose kind declares no
722
- dedicated result view opens here instead, and the shell is not involved — so
723
- without this the artifacts of exactly the kinds that need no bespoke window
724
- would have nowhere to appear. Self-hiding when the step has neither a report
725
- nor a selection, which is every step of every stock pipeline. -->
726
- <BinaryOutputReport :step="step" variant="card" />
720
+ <!-- what the step declared it stored through a foundational storage service, and
721
+ every way that record is incomplete. This panel is the OTHER half of the
722
+ result-window shell's trailing section: a step whose kind declares no
723
+ dedicated result view opens here instead, and the shell is not involved — so
724
+ without this the artifacts of exactly the kinds that need no bespoke window
725
+ would have nowhere to appear. Self-hiding when the step has neither a report
726
+ nor a selection, which is every step of every stock pipeline. -->
727
+ <BinaryOutputReport :step="step" variant="card" />
727
728
 
728
- <!-- edit-then-approve: a direct editor over the raw conclusions; the
729
- edits become the approved proposal that flows to the next step -->
730
- <section v-if="editing" class="scroll-mt-4">
731
- <div class="mb-2 flex items-center gap-1.5 text-[11px] text-amber-400">
732
- <UIcon name="i-lucide-pencil" class="h-3.5 w-3.5" />
733
- <span class="font-semibold uppercase tracking-wide">{{
734
- t('panels.stepDetail.editingConclusions')
735
- }}</span>
736
- </div>
737
- <UTextarea
738
- v-model="draftProposal"
739
- :rows="22"
740
- autoresize
741
- size="sm"
742
- class="w-full"
743
- :ui="{ base: 'font-mono text-[12px] leading-relaxed' }"
744
- :placeholder="t('panels.stepDetail.editConclusionsPlaceholder')"
729
+ <!-- edit-then-approve: a direct editor over the raw conclusions; the
730
+ edits become the approved proposal that flows to the next step -->
731
+ <section v-if="editing" class="scroll-mt-4">
732
+ <div class="mb-2 flex items-center gap-1.5 text-[11px] text-amber-400">
733
+ <UIcon name="i-lucide-pencil" class="h-3.5 w-3.5" />
734
+ <span class="font-semibold uppercase tracking-wide">{{
735
+ t('panels.stepDetail.editingConclusions')
736
+ }}</span>
737
+ </div>
738
+ <UTextarea
739
+ v-model="draftProposal"
740
+ :rows="22"
741
+ autoresize
742
+ size="sm"
743
+ class="w-full"
744
+ :ui="{ base: 'font-mono text-[12px] leading-relaxed' }"
745
+ :placeholder="t('panels.stepDetail.editConclusionsPlaceholder')"
746
+ />
747
+ </section>
748
+
749
+ <!-- the agent's prose output, sectioned + collapsible -->
750
+ <template v-else-if="hasOutput">
751
+ <section
752
+ v-for="s in outline.sections"
753
+ :id="s.id"
754
+ :key="s.id"
755
+ :ref="(el) => (sectionEls[s.id] = el as HTMLElement | null)"
756
+ class="scroll-mt-4"
757
+ >
758
+ <button
759
+ v-if="s.depth > 0"
760
+ class="group flex w-full items-center gap-2 rounded-md py-1 text-start transition hover:text-white"
761
+ @click="toggle(s.id)"
762
+ >
763
+ <UIcon
764
+ name="i-lucide-chevron-right"
765
+ class="h-4 w-4 shrink-0 text-slate-500 transition-transform group-hover:text-slate-300"
766
+ :class="collapsed[s.id] ? '' : 'rotate-90'"
767
+ />
768
+ <span
769
+ class="font-semibold text-slate-100"
770
+ :class="s.depth <= 1 ? 'text-lg' : s.depth === 2 ? 'text-base' : 'text-sm'"
771
+ v-html="s.titleHtml"
745
772
  />
746
- </section>
773
+ </button>
774
+ <!-- eslint-disable-next-line vue/no-v-html -->
775
+ <div
776
+ v-show="!collapsed[s.id]"
777
+ class="reader-prose mt-1 text-[13px] leading-relaxed text-slate-300"
778
+ :class="[
779
+ s.depth > 0 ? 'ps-6' : '',
780
+ genericApprovalPending && !editing ? 'review-mode' : '',
781
+ ]"
782
+ @click="onProseClick"
783
+ v-html="s.bodyHtml"
784
+ />
785
+ </section>
786
+ </template>
747
787
 
748
- <!-- the agent's prose output, sectioned + collapsible -->
749
- <template v-else-if="hasOutput">
750
- <section
751
- v-for="s in outline.sections"
752
- :id="s.id"
753
- :key="s.id"
754
- :ref="(el) => (sectionEls[s.id] = el as HTMLElement | null)"
755
- class="scroll-mt-4"
756
- >
757
- <button
758
- v-if="s.depth > 0"
759
- class="group flex w-full items-center gap-2 rounded-md py-1 text-start transition hover:text-white"
760
- @click="toggle(s.id)"
761
- >
762
- <UIcon
763
- name="i-lucide-chevron-right"
764
- class="h-4 w-4 shrink-0 text-slate-500 transition-transform group-hover:text-slate-300"
765
- :class="collapsed[s.id] ? '' : 'rotate-90'"
766
- />
767
- <span
768
- class="font-semibold text-slate-100"
769
- :class="s.depth <= 1 ? 'text-lg' : s.depth === 2 ? 'text-base' : 'text-sm'"
770
- v-html="s.titleHtml"
771
- />
772
- </button>
773
- <!-- eslint-disable-next-line vue/no-v-html -->
774
- <div
775
- v-show="!collapsed[s.id]"
776
- class="reader-prose mt-1 text-[13px] leading-relaxed text-slate-300"
777
- :class="[
778
- s.depth > 0 ? 'ps-6' : '',
779
- genericApprovalPending && !editing ? 'review-mode' : '',
780
- ]"
781
- @click="onProseClick"
782
- v-html="s.bodyHtml"
783
- />
784
- </section>
785
- </template>
788
+ <p
789
+ v-else
790
+ class="rounded-lg border border-dashed border-slate-800 py-6 text-center text-sm text-slate-500"
791
+ >
792
+ {{ t('panels.stepDetail.noProseOutput') }}
793
+ </p>
794
+ </div>
795
+ </div>
796
+ </div>
786
797
 
787
- <p
788
- v-else
789
- class="rounded-lg border border-dashed border-slate-800 py-6 text-center text-sm text-slate-500"
790
- >
791
- {{ t('panels.stepDetail.noProseOutput') }}
792
- </p>
793
- </div>
794
- </div>
798
+ <!-- review rail (approval mode): per-block comments + overall feedback +
799
+ Approve / Request changes / Reject. A end-side rail on wide screens; a
800
+ bottom sheet (still reachable) below lg, so the gate is always actionable. -->
801
+ <aside
802
+ v-if="genericApprovalPending"
803
+ class="absolute inset-x-0 bottom-0 z-10 flex max-h-[70dvh] flex-col rounded-t-2xl border-t border-slate-700 bg-slate-900/95 shadow-2xl backdrop-blur lg:static lg:inset-auto lg:z-auto lg:max-h-none lg:w-96 lg:shrink-0 lg:rounded-none lg:border-s lg:border-t-0 lg:border-slate-800 lg:bg-slate-900/60 lg:shadow-none lg:backdrop-blur-none"
804
+ >
805
+ <div class="border-b border-slate-800 px-4 py-3">
806
+ <div class="text-[11px] font-semibold uppercase tracking-wide text-amber-400">
807
+ {{
808
+ editing
809
+ ? t('panels.stepDetail.approveWithCorrections')
810
+ : t('panels.stepDetail.reviewAndApprove')
811
+ }}
795
812
  </div>
813
+ <p class="mt-1 text-[12px] text-slate-400">
814
+ {{ editing ? t('panels.stepDetail.editHint') : t('panels.stepDetail.reviewHint') }}
815
+ </p>
816
+ <!-- The gate's configured POLICY, when it has one. Both lines exist because an
817
+ approve on such a gate legitimately may not advance the run: without the tally, a
818
+ correctly-recorded approval is indistinguishable from a call that failed, and
819
+ without the refusal a person would press a button the server answers 403. -->
820
+ <p v-if="gateQuorum" class="mt-1 text-[12px] text-amber-300/90" data-testid="gate-quorum">
821
+ {{
822
+ t('panels.stepDetail.quorumProgress', {
823
+ recorded: gateQuorum.recorded,
824
+ required: gateQuorum.required,
825
+ })
826
+ }}
827
+ <span v-if="viewerHasApproved">{{ t('panels.stepDetail.quorumYours') }}</span>
828
+ </p>
829
+ <p
830
+ v-if="gateRefusal"
831
+ class="mt-1 text-[12px] text-slate-400"
832
+ data-testid="gate-not-approver"
833
+ >
834
+ {{ t(GATE_REFUSAL_KEYS[gateRefusal]) }}
835
+ </p>
836
+ </div>
796
837
 
797
- <!-- review rail (approval mode): per-block comments + overall feedback +
798
- Approve / Request changes / Reject. A end-side rail on wide screens; a
799
- bottom sheet (still reachable) below lg, so the gate is always actionable. -->
800
- <aside
801
- v-if="genericApprovalPending"
802
- class="absolute inset-x-0 bottom-0 z-10 flex max-h-[70dvh] flex-col rounded-t-2xl border-t border-slate-700 bg-slate-900/95 shadow-2xl backdrop-blur lg:static lg:inset-auto lg:z-auto lg:max-h-none lg:w-96 lg:shrink-0 lg:rounded-none lg:border-s lg:border-t-0 lg:border-slate-800 lg:bg-slate-900/60 lg:shadow-none lg:backdrop-blur-none"
838
+ <div class="flex-1 space-y-3 overflow-auto overscroll-contain px-4 py-3">
839
+ <p
840
+ v-if="editing"
841
+ class="rounded-lg border border-amber-500/30 bg-amber-500/5 p-3 text-[12px] leading-relaxed text-amber-200/90"
803
842
  >
804
- <div class="border-b border-slate-800 px-4 py-3">
805
- <div class="text-[11px] font-semibold uppercase tracking-wide text-amber-400">
806
- {{
807
- editing
808
- ? t('panels.stepDetail.approveWithCorrections')
809
- : t('panels.stepDetail.reviewAndApprove')
810
- }}
843
+ {{ t('panels.stepDetail.editingNotice') }}
844
+ </p>
845
+ <template v-else>
846
+ <!-- composer for the block the human just clicked -->
847
+ <div
848
+ v-if="draftTarget"
849
+ data-testid="step-review-composer"
850
+ class="rounded-lg border border-indigo-500/40 bg-indigo-500/5 p-3"
851
+ >
852
+ <div class="mb-1 text-[10px] uppercase tracking-wide text-indigo-300">
853
+ {{ t('panels.stepDetail.commentingOn') }}
854
+ </div>
855
+ <pre
856
+ class="mb-2 max-h-24 overflow-auto whitespace-pre-wrap rounded bg-slate-950/60 p-2 text-[11px] text-slate-300"
857
+ >{{ draftTarget.quotedSource }}</pre>
858
+ <UTextarea
859
+ v-model="draftBody"
860
+ data-testid="step-review-comment-body"
861
+ :rows="3"
862
+ autoresize
863
+ size="sm"
864
+ class="w-full"
865
+ :placeholder="t('panels.stepDetail.commentPlaceholder')"
866
+ />
867
+ <div class="mt-2 flex justify-end gap-2">
868
+ <UButton color="neutral" variant="ghost" size="xs" @click="cancelDraft">
869
+ {{ t('common.cancel') }}
870
+ </UButton>
871
+ <UButton
872
+ color="primary"
873
+ size="xs"
874
+ data-testid="step-review-comment-add"
875
+ :disabled="!draftBody.trim()"
876
+ @click="addDraftComment"
877
+ >
878
+ {{ t('panels.stepDetail.addComment') }}
879
+ </UButton>
811
880
  </div>
812
- <p class="mt-1 text-[12px] text-slate-400">
813
- {{ editing ? t('panels.stepDetail.editHint') : t('panels.stepDetail.reviewHint') }}
814
- </p>
815
- <!-- The gate's configured POLICY, when it has one. Both lines exist because an
816
- approve on such a gate legitimately may not advance the run: without the tally, a
817
- correctly-recorded approval is indistinguishable from a call that failed, and
818
- without the refusal a person would press a button the server answers 403. -->
819
- <p
820
- v-if="gateQuorum"
821
- class="mt-1 text-[12px] text-amber-300/90"
822
- data-testid="gate-quorum"
823
- >
824
- {{
825
- t('panels.stepDetail.quorumProgress', {
826
- recorded: gateQuorum.recorded,
827
- required: gateQuorum.required,
828
- })
829
- }}
830
- <span v-if="viewerHasApproved">{{ t('panels.stepDetail.quorumYours') }}</span>
831
- </p>
832
- <p
833
- v-if="gateRefusal"
834
- class="mt-1 text-[12px] text-slate-400"
835
- data-testid="gate-not-approver"
836
- >
837
- {{ t(GATE_REFUSAL_KEYS[gateRefusal]) }}
838
- </p>
839
881
  </div>
840
882
 
841
- <div class="flex-1 space-y-3 overflow-auto overscroll-contain px-4 py-3">
842
- <p
843
- v-if="editing"
844
- class="rounded-lg border border-amber-500/30 bg-amber-500/5 p-3 text-[12px] leading-relaxed text-amber-200/90"
845
- >
846
- {{ t('panels.stepDetail.editingNotice') }}
847
- </p>
848
- <template v-else>
849
- <!-- composer for the block the human just clicked -->
850
- <div
851
- v-if="draftTarget"
852
- data-testid="step-review-composer"
853
- class="rounded-lg border border-indigo-500/40 bg-indigo-500/5 p-3"
854
- >
855
- <div class="mb-1 text-[10px] uppercase tracking-wide text-indigo-300">
856
- {{ t('panels.stepDetail.commentingOn') }}
857
- </div>
858
- <pre
859
- class="mb-2 max-h-24 overflow-auto whitespace-pre-wrap rounded bg-slate-950/60 p-2 text-[11px] text-slate-300"
860
- >{{ draftTarget.quotedSource }}</pre>
861
- <UTextarea
862
- v-model="draftBody"
863
- data-testid="step-review-comment-body"
864
- :rows="3"
865
- autoresize
866
- size="sm"
867
- class="w-full"
868
- :placeholder="t('panels.stepDetail.commentPlaceholder')"
869
- />
870
- <div class="mt-2 flex justify-end gap-2">
871
- <UButton color="neutral" variant="ghost" size="xs" @click="cancelDraft">
872
- {{ t('common.cancel') }}
873
- </UButton>
874
- <UButton
875
- color="primary"
876
- size="xs"
877
- data-testid="step-review-comment-add"
878
- :disabled="!draftBody.trim()"
879
- @click="addDraftComment"
880
- >
881
- {{ t('panels.stepDetail.addComment') }}
882
- </UButton>
883
- </div>
883
+ <!-- comments added so far -->
884
+ <div
885
+ v-for="(c, idx) in reviewComments"
886
+ :key="idx"
887
+ data-testid="step-review-comment"
888
+ class="rounded-lg border border-slate-800 bg-slate-900/50 p-3"
889
+ >
890
+ <div class="mb-1 flex items-start justify-between gap-2">
891
+ <div class="text-[10px] uppercase tracking-wide text-slate-500">
892
+ {{ t('panels.stepDetail.commentN', { number: idx + 1 }) }}
884
893
  </div>
885
-
886
- <!-- comments added so far -->
887
- <div
888
- v-for="(c, idx) in reviewComments"
889
- :key="idx"
890
- data-testid="step-review-comment"
891
- class="rounded-lg border border-slate-800 bg-slate-900/50 p-3"
894
+ <button
895
+ class="text-slate-500 transition hover:text-rose-400"
896
+ :title="t('panels.stepDetail.removeComment')"
897
+ @click="removeComment(idx)"
892
898
  >
893
- <div class="mb-1 flex items-start justify-between gap-2">
894
- <div class="text-[10px] uppercase tracking-wide text-slate-500">
895
- {{ t('panels.stepDetail.commentN', { number: idx + 1 }) }}
896
- </div>
897
- <button
898
- class="text-slate-500 transition hover:text-rose-400"
899
- :title="t('panels.stepDetail.removeComment')"
900
- @click="removeComment(idx)"
901
- >
902
- <UIcon name="i-lucide-x" class="h-3.5 w-3.5" />
903
- </button>
904
- </div>
905
- <pre
906
- class="mb-1 max-h-20 overflow-auto whitespace-pre-wrap rounded bg-slate-950/50 p-1.5 text-[10px] text-slate-400"
907
- >{{ c.quotedSource }}</pre>
908
- <p class="text-[12px] text-slate-200">{{ c.body }}</p>
909
- </div>
910
-
911
- <div>
912
- <label
913
- class="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-slate-400"
914
- >
915
- {{ t('panels.stepDetail.overallFeedback') }}
916
- </label>
917
- <UTextarea
918
- v-model="feedback"
919
- data-testid="step-review-feedback"
920
- :rows="3"
921
- autoresize
922
- size="sm"
923
- class="w-full"
924
- :placeholder="t('panels.stepDetail.overallFeedbackPlaceholder')"
925
- />
926
- </div>
927
- </template>
899
+ <UIcon name="i-lucide-x" class="h-3.5 w-3.5" />
900
+ </button>
901
+ </div>
902
+ <pre
903
+ class="mb-1 max-h-20 overflow-auto whitespace-pre-wrap rounded bg-slate-950/50 p-1.5 text-[10px] text-slate-400"
904
+ >{{ c.quotedSource }}</pre>
905
+ <p class="text-[12px] text-slate-200">{{ c.body }}</p>
928
906
  </div>
929
907
 
930
- <!-- edit-then-approve actions -->
931
- <div
932
- v-if="editing"
933
- class="space-y-2 border-t border-slate-800 px-4 pt-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))]"
934
- >
935
- <UButton
936
- color="primary"
937
- size="sm"
938
- icon="i-lucide-check"
939
- block
940
- :loading="submitting"
941
- @click="approveWithEdits"
908
+ <div>
909
+ <label
910
+ class="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-slate-400"
942
911
  >
943
- {{ t('panels.stepDetail.approveWithEdits') }}
944
- </UButton>
912
+ {{ t('panels.stepDetail.overallFeedback') }}
913
+ </label>
914
+ <UTextarea
915
+ v-model="feedback"
916
+ data-testid="step-review-feedback"
917
+ :rows="3"
918
+ autoresize
919
+ size="sm"
920
+ class="w-full"
921
+ :placeholder="t('panels.stepDetail.overallFeedbackPlaceholder')"
922
+ />
923
+ </div>
924
+ </template>
925
+ </div>
926
+
927
+ <!-- edit-then-approve actions -->
928
+ <div
929
+ v-if="editing"
930
+ class="space-y-2 border-t border-slate-800 px-4 pt-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))]"
931
+ >
932
+ <UButton
933
+ color="primary"
934
+ size="sm"
935
+ icon="i-lucide-check"
936
+ block
937
+ :loading="submitting"
938
+ @click="approveWithEdits"
939
+ >
940
+ {{ t('panels.stepDetail.approveWithEdits') }}
941
+ </UButton>
942
+ <UButton
943
+ color="neutral"
944
+ variant="ghost"
945
+ size="sm"
946
+ block
947
+ :disabled="submitting"
948
+ @click="cancelEditing"
949
+ >
950
+ {{ t('panels.stepDetail.cancelEdits') }}
951
+ </UButton>
952
+ </div>
953
+
954
+ <div
955
+ v-else
956
+ class="space-y-2 border-t border-slate-800 px-4 pt-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))]"
957
+ >
958
+ <UButton
959
+ color="primary"
960
+ data-testid="step-approve"
961
+ size="sm"
962
+ icon="i-lucide-check"
963
+ block
964
+ :disabled="rejectArmed || !!gateRefusal"
965
+ :loading="submitting"
966
+ @click="approve"
967
+ >
968
+ {{ t('panels.stepDetail.approveAndProceed') }}
969
+ </UButton>
970
+ <UButton
971
+ v-if="proposalEditableNow"
972
+ color="primary"
973
+ variant="soft"
974
+ size="sm"
975
+ icon="i-lucide-pencil"
976
+ block
977
+ :disabled="rejectArmed || submitting || !!gateRefusal"
978
+ @click="startEditing"
979
+ >
980
+ {{ t('panels.stepDetail.approveWithCorrections') }}
981
+ </UButton>
982
+ <p
983
+ v-else-if="!proposalEditable"
984
+ class="text-[10px] text-slate-500"
985
+ data-testid="step-rendered-output-note"
986
+ >
987
+ {{ t('panels.stepDetail.renderedOutputNote') }}
988
+ </p>
989
+ <!-- Withheld only until the quorum is one approval away, so it says so rather than
990
+ leaving a reviewer to wonder where the affordance went. -->
991
+ <p v-else class="text-[10px] text-slate-500" data-testid="step-quorum-edit-locked">
992
+ {{ t('panels.stepDetail.quorumEditLocked') }}
993
+ </p>
994
+
995
+ <!-- destructive: a two-step inline confirm instead of a native dialog -->
996
+ <div v-if="rejectArmed" class="rounded-lg border border-rose-500/40 bg-rose-500/5 p-2.5">
997
+ <p class="mb-2 text-[11px] text-rose-200">
998
+ {{ t('panels.stepDetail.rejectConfirmPrompt') }}
999
+ </p>
1000
+ <div class="flex gap-2">
945
1001
  <UButton
946
1002
  color="neutral"
947
1003
  variant="ghost"
948
- size="sm"
949
- block
1004
+ size="xs"
1005
+ class="flex-1"
950
1006
  :disabled="submitting"
951
- @click="cancelEditing"
1007
+ @click="disarmReject"
952
1008
  >
953
- {{ t('panels.stepDetail.cancelEdits') }}
1009
+ {{ t('common.cancel') }}
954
1010
  </UButton>
955
- </div>
956
-
957
- <div
958
- v-else
959
- class="space-y-2 border-t border-slate-800 px-4 pt-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))]"
960
- >
961
1011
  <UButton
962
- color="primary"
963
- data-testid="step-approve"
964
- size="sm"
965
- icon="i-lucide-check"
966
- block
967
- :disabled="rejectArmed || !!gateRefusal"
1012
+ color="error"
1013
+ size="xs"
1014
+ icon="i-lucide-ban"
1015
+ class="flex-1"
968
1016
  :loading="submitting"
969
- @click="approve"
970
- >
971
- {{ t('panels.stepDetail.approveAndProceed') }}
972
- </UButton>
973
- <UButton
974
- v-if="proposalEditableNow"
975
- color="primary"
976
- variant="soft"
977
- size="sm"
978
- icon="i-lucide-pencil"
979
- block
980
- :disabled="rejectArmed || submitting || !!gateRefusal"
981
- @click="startEditing"
1017
+ @click="reject"
982
1018
  >
983
- {{ t('panels.stepDetail.approveWithCorrections') }}
1019
+ {{ t('panels.stepDetail.confirmReject') }}
984
1020
  </UButton>
985
- <p
986
- v-else-if="!proposalEditable"
987
- class="text-[10px] text-slate-500"
988
- data-testid="step-rendered-output-note"
989
- >
990
- {{ t('panels.stepDetail.renderedOutputNote') }}
991
- </p>
992
- <!-- Withheld only until the quorum is one approval away, so it says so rather than
993
- leaving a reviewer to wonder where the affordance went. -->
994
- <p v-else class="text-[10px] text-slate-500" data-testid="step-quorum-edit-locked">
995
- {{ t('panels.stepDetail.quorumEditLocked') }}
996
- </p>
997
-
998
- <!-- destructive: a two-step inline confirm instead of a native dialog -->
999
- <div
1000
- v-if="rejectArmed"
1001
- class="rounded-lg border border-rose-500/40 bg-rose-500/5 p-2.5"
1002
- >
1003
- <p class="mb-2 text-[11px] text-rose-200">
1004
- {{ t('panels.stepDetail.rejectConfirmPrompt') }}
1005
- </p>
1006
- <div class="flex gap-2">
1007
- <UButton
1008
- color="neutral"
1009
- variant="ghost"
1010
- size="xs"
1011
- class="flex-1"
1012
- :disabled="submitting"
1013
- @click="disarmReject"
1014
- >
1015
- {{ t('common.cancel') }}
1016
- </UButton>
1017
- <UButton
1018
- color="error"
1019
- size="xs"
1020
- icon="i-lucide-ban"
1021
- class="flex-1"
1022
- :loading="submitting"
1023
- @click="reject"
1024
- >
1025
- {{ t('panels.stepDetail.confirmReject') }}
1026
- </UButton>
1027
- </div>
1028
- </div>
1029
- <div v-else class="flex gap-2">
1030
- <UButton
1031
- color="warning"
1032
- variant="soft"
1033
- size="sm"
1034
- icon="i-lucide-rotate-ccw"
1035
- class="flex-1"
1036
- data-testid="step-request-changes"
1037
- :disabled="!canRequestChanges || !!gateRefusal"
1038
- :loading="submitting"
1039
- @click="requestChanges"
1040
- >
1041
- {{ t('panels.stepDetail.requestChanges') }}
1042
- </UButton>
1043
- <UButton
1044
- color="error"
1045
- variant="soft"
1046
- size="sm"
1047
- icon="i-lucide-ban"
1048
- class="flex-1"
1049
- :disabled="submitting || !!gateRefusal"
1050
- @click="armReject"
1051
- >
1052
- {{ t('panels.stepDetail.reject') }}
1053
- </UButton>
1054
- </div>
1055
- <p class="text-[10px] text-slate-500">
1056
- {{ t('panels.stepDetail.requestChangesHint') }}
1057
- </p>
1058
1021
  </div>
1059
- </aside>
1022
+ </div>
1023
+ <div v-else class="flex gap-2">
1024
+ <UButton
1025
+ color="warning"
1026
+ variant="soft"
1027
+ size="sm"
1028
+ icon="i-lucide-rotate-ccw"
1029
+ class="flex-1"
1030
+ data-testid="step-request-changes"
1031
+ :disabled="!canRequestChanges || !!gateRefusal"
1032
+ :loading="submitting"
1033
+ @click="requestChanges"
1034
+ >
1035
+ {{ t('panels.stepDetail.requestChanges') }}
1036
+ </UButton>
1037
+ <UButton
1038
+ color="error"
1039
+ variant="soft"
1040
+ size="sm"
1041
+ icon="i-lucide-ban"
1042
+ class="flex-1"
1043
+ :disabled="submitting || !!gateRefusal"
1044
+ @click="armReject"
1045
+ >
1046
+ {{ t('panels.stepDetail.reject') }}
1047
+ </UButton>
1048
+ </div>
1049
+ <p class="text-[10px] text-slate-500">
1050
+ {{ t('panels.stepDetail.requestChangesHint') }}
1051
+ </p>
1060
1052
  </div>
1061
- </Transition>
1062
- </Teleport>
1053
+ </aside>
1054
+ </div>
1063
1055
  </template>
1064
1056
 
1065
1057
  <style scoped>