@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.
@@ -8,9 +8,8 @@
8
8
  // document — not the original description + linked docs/tasks — is what every downstream
9
9
  // agent step and the spec-writer consume.
10
10
  import { parseOutputOutline } from '~/utils/agentOutput'
11
- import StepRestartControl from '~/components/panels/StepRestartControl.vue'
12
11
  import IterationCapPrompt from '~/components/pipeline/IterationCapPrompt.vue'
13
- import IconButton from '~/components/common/IconButton.vue'
12
+ import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
14
13
  import type {
15
14
  RequirementRecommendation,
16
15
  RequirementReview,
@@ -556,624 +555,580 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
556
555
  </script>
557
556
 
558
557
  <template>
559
- <Teleport to="body">
560
- <div
561
- v-if="open"
562
- class="fixed inset-0 z-50 flex max-h-[100dvh] items-center justify-center bg-slate-950/70 p-4 backdrop-blur-sm"
563
- @click.self="close"
564
- >
565
- <div
566
- class="flex max-h-[90dvh] w-full max-w-5xl flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl"
567
- role="dialog"
568
- aria-modal="true"
569
- >
570
- <!-- header -->
571
- <header class="flex items-center gap-3 border-b border-slate-800 px-6 py-4">
558
+ <ResultWindowShell
559
+ :open="open"
560
+ icon="i-lucide-clipboard-check"
561
+ icon-class="bg-indigo-500/15 text-indigo-300"
562
+ :title="t('requirements.title')"
563
+ :subtitle="block?.title"
564
+ :step-ref="{ instanceId, stepIndex }"
565
+ variant="centered"
566
+ width="5xl"
567
+ @close="close"
568
+ >
569
+ <template v-if="review" #header-extras>
570
+ <UBadge color="neutral" variant="subtle" size="sm">
571
+ {{ t('requirements.iteration', { current: iteration, max: maxIterations }) }}
572
+ </UBadge>
573
+ </template>
574
+
575
+ <div class="flex min-h-0 flex-1 flex-col lg:flex-row">
576
+ <!-- main column -->
577
+ <div class="min-w-0 flex-1 overflow-y-auto px-6 py-5">
578
+ <i18n-t
579
+ keypath="requirements.intro"
580
+ tag="p"
581
+ class="mb-4 text-sm text-slate-400"
582
+ scope="global"
583
+ >
584
+ <template #level>{{ block?.level ?? t('requirements.levelFallback') }}</template>
585
+ <template #answer
586
+ ><span class="text-slate-300">{{ t('requirements.answerVerb') }}</span></template
587
+ >
588
+ <template #dismiss
589
+ ><span class="text-slate-300">{{ t('requirements.dismissVerb') }}</span></template
590
+ >
591
+ </i18n-t>
592
+
593
+ <!-- empty state — the reviewer runs automatically as the first pipeline
594
+ gate step, so there's nothing to do here until then -->
595
+ <div
596
+ v-if="!review && !busy && !loading"
597
+ class="rounded-lg border border-dashed border-slate-700 p-8 text-center text-sm text-slate-500"
598
+ >
599
+ {{ t('requirements.empty') }}
600
+ </div>
601
+
602
+ <!-- working state (initial fetch on open, or a reviewer pass running) -->
603
+ <div
604
+ v-else-if="(busy || loading) && !review"
605
+ class="flex items-center justify-center gap-2 p-8 text-sm text-slate-400"
606
+ >
607
+ <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
608
+ {{ loading && !busy ? t('requirements.loadingReview') : t('requirements.reviewing') }}
609
+ </div>
610
+
611
+ <template v-else-if="review">
612
+ <!-- converged: reviewer satisfied -->
572
613
  <div
573
- class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-indigo-500/15"
614
+ v-if="incorporated"
615
+ class="mb-4 flex items-center gap-2 rounded-lg border border-emerald-900/60 bg-emerald-950/30 p-4 text-sm text-emerald-300"
574
616
  >
575
- <UIcon name="i-lucide-clipboard-check" class="h-5 w-5 text-indigo-300" />
576
- </div>
577
- <div class="min-w-0">
578
- <h1 class="truncate text-base font-semibold text-white">
579
- {{ t('requirements.title') }}
580
- </h1>
581
- <p v-if="block" class="truncate text-xs text-slate-500">{{ block.title }}</p>
617
+ <UIcon name="i-lucide-circle-check" class="h-5 w-5 shrink-0" />
618
+ {{ t('requirements.settled') }}
582
619
  </div>
583
- <div class="ms-auto flex items-center gap-1.5">
584
- <UBadge v-if="review" color="neutral" variant="subtle" size="sm">
585
- {{ t('requirements.iteration', { current: iteration, max: maxIterations }) }}
586
- </UBadge>
587
- <StepRestartControl
588
- :instance-id="instanceId"
589
- :step-index="stepIndex"
590
- @restarted="close"
591
- />
592
- <IconButton
593
- icon="i-lucide-x"
594
- color="neutral"
595
- variant="ghost"
596
- size="sm"
597
- :label="t('common.close')"
598
- @click="close"
599
- />
600
- </div>
601
- </header>
602
620
 
603
- <div class="flex min-h-0 flex-1 flex-col lg:flex-row">
604
- <!-- main column -->
605
- <div class="min-w-0 flex-1 overflow-y-auto px-6 py-5">
606
- <i18n-t
607
- keypath="requirements.intro"
608
- tag="p"
609
- class="mb-4 text-sm text-slate-400"
610
- scope="global"
611
- >
612
- <template #level>{{ block?.level ?? t('requirements.levelFallback') }}</template>
613
- <template #answer
614
- ><span class="text-slate-300">{{ t('requirements.answerVerb') }}</span></template
615
- >
616
- <template #dismiss
617
- ><span class="text-slate-300">{{ t('requirements.dismissVerb') }}</span></template
618
- >
619
- </i18n-t>
621
+ <!-- iteration cap hit -->
622
+ <IterationCapPrompt
623
+ v-else-if="exceeded"
624
+ class="mb-4"
625
+ :heading="t('requirements.exceeded.heading', { max: maxIterations })"
626
+ :detail="t('requirements.exceeded.detail')"
627
+ :loading="acting"
628
+ @resolve="resolveExceeded"
629
+ />
620
630
 
621
- <!-- empty state the reviewer runs automatically as the first pipeline
622
- gate step, so there's nothing to do here until then -->
623
- <div
624
- v-if="!review && !busy && !loading"
625
- class="rounded-lg border border-dashed border-slate-700 p-8 text-center text-sm text-slate-500"
626
- >
627
- {{ t('requirements.empty') }}
628
- </div>
631
+ <!-- working: the async cycle is running in the driver. Two distinct stages so
632
+ the human can see which of the two LLM calls is currently in progress. -->
633
+ <div
634
+ v-else-if="working"
635
+ class="mb-4 flex items-center gap-2 rounded-lg border border-indigo-900/60 bg-indigo-950/30 p-4 text-sm text-indigo-200"
636
+ >
637
+ <UIcon name="i-lucide-loader-circle" class="h-5 w-5 shrink-0 animate-spin" />
638
+ <span v-if="incorporating">
639
+ {{ t('requirements.working.incorporating') }}
640
+ </span>
641
+ <span v-else>
642
+ {{ t('requirements.working.reReviewing') }}
643
+ </span>
644
+ </div>
629
645
 
630
- <!-- working state (initial fetch on open, or a reviewer pass running) -->
646
+ <!-- findings to react to -->
647
+ <div v-if="review.items.length" class="flex flex-col gap-3">
631
648
  <div
632
- v-else-if="(busy || loading) && !review"
633
- class="flex items-center justify-center gap-2 p-8 text-sm text-slate-400"
649
+ v-for="item in sortedItems"
650
+ :key="item.id"
651
+ class="rounded-lg border border-slate-800 bg-slate-900/60 p-3"
652
+ :class="{ 'opacity-60': item.status === 'dismissed' }"
634
653
  >
635
- <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
636
- {{ loading && !busy ? t('requirements.loadingReview') : t('requirements.reviewing') }}
637
- </div>
638
-
639
- <template v-else-if="review">
640
- <!-- converged: reviewer satisfied -->
641
- <div
642
- v-if="incorporated"
643
- class="mb-4 flex items-center gap-2 rounded-lg border border-emerald-900/60 bg-emerald-950/30 p-4 text-sm text-emerald-300"
644
- >
645
- <UIcon name="i-lucide-circle-check" class="h-5 w-5 shrink-0" />
646
- {{ t('requirements.settled') }}
647
- </div>
654
+ <div class="flex items-start gap-2">
655
+ <UIcon
656
+ :name="CATEGORY_ICON[item.category]"
657
+ class="mt-0.5 h-4 w-4 shrink-0 text-slate-400"
658
+ />
659
+ <div class="min-w-0 flex-1">
660
+ <div class="flex flex-wrap items-center gap-1.5">
661
+ <span class="text-sm font-medium text-white">{{ item.title }}</span>
662
+ <UBadge size="xs" variant="subtle" :color="SEVERITY_COLOR[item.severity]">
663
+ {{ SEVERITY_LABELS[item.severity] }}
664
+ </UBadge>
665
+ <UBadge size="xs" variant="outline" color="neutral">
666
+ {{ CATEGORY_LABELS[item.category] }}
667
+ </UBadge>
668
+ <!-- Once the automation has pre-answered some findings, flag the ones it
669
+ left open as the genuine business decisions that need the human. -->
670
+ <UBadge
671
+ v-if="
672
+ hasAutoDefaults && item.status === 'open' && item.autoAnswerable === false
673
+ "
674
+ size="xs"
675
+ variant="subtle"
676
+ color="warning"
677
+ >
678
+ {{ t('requirements.needsYourInput') }}
679
+ </UBadge>
680
+ <UBadge
681
+ size="xs"
682
+ variant="soft"
683
+ :color="STATUS_COLOR[item.status]"
684
+ class="ms-auto"
685
+ >
686
+ {{ STATUS_LABELS[item.status] }}
687
+ </UBadge>
688
+ </div>
689
+ <p class="mt-1 whitespace-pre-line text-sm text-slate-400">
690
+ {{ item.detail }}
691
+ </p>
648
692
 
649
- <!-- iteration cap hit -->
650
- <IterationCapPrompt
651
- v-else-if="exceeded"
652
- class="mb-4"
653
- :heading="t('requirements.exceeded.heading', { max: maxIterations })"
654
- :detail="t('requirements.exceeded.detail')"
655
- :loading="acting"
656
- @resolve="resolveExceeded"
657
- />
693
+ <!-- recorded answer (only for non-editable findings — for editable
694
+ ones the answer lives in the textarea below, seeded from the reply) -->
695
+ <div
696
+ v-if="item.reply && item.status !== 'open' && item.status !== 'answered'"
697
+ class="mt-2 rounded-md border-s-2 border-slate-700 bg-slate-950/40 px-3 py-1.5 text-sm text-slate-300"
698
+ >
699
+ <span class="text-[10px] uppercase tracking-wide text-slate-500">
700
+ {{ t('requirements.answerLabel') }}
701
+ </span>
702
+ <p class="whitespace-pre-line">{{ item.reply }}</p>
703
+ </div>
658
704
 
659
- <!-- working: the async cycle is running in the driver. Two distinct stages so
660
- the human can see which of the two LLM calls is currently in progress. -->
661
- <div
662
- v-else-if="working"
663
- class="mb-4 flex items-center gap-2 rounded-lg border border-indigo-900/60 bg-indigo-950/30 p-4 text-sm text-indigo-200"
664
- >
665
- <UIcon name="i-lucide-loader-circle" class="h-5 w-5 shrink-0 animate-spin" />
666
- <span v-if="incorporating">
667
- {{ t('requirements.working.incorporating') }}
668
- </span>
669
- <span v-else>
670
- {{ t('requirements.working.reReviewing') }}
671
- </span>
672
- </div>
705
+ <!-- per-finding 3-way selector: Answer (write it) / Dismiss (irrelevant) /
706
+ Recommend (let the Requirement Writer suggest one). The active mode
707
+ drives the content below, IN PLACE — no separate section. Disabled once
708
+ the requirements are settled / a cycle is running; hidden for a
709
+ `resolved` finding (its recorded answer shows above). -->
710
+ <template v-if="item.status !== 'resolved'">
711
+ <div class="mt-2 flex flex-wrap items-center gap-1">
712
+ <UButton
713
+ v-for="opt in FINDING_MODES"
714
+ :key="opt.mode"
715
+ :color="modeFor(item) === opt.mode ? 'primary' : 'neutral'"
716
+ :variant="modeFor(item) === opt.mode ? 'soft' : 'ghost'"
717
+ size="xs"
718
+ :icon="opt.icon"
719
+ :disabled="frozen"
720
+ @click="setMode(item, opt.mode)"
721
+ >
722
+ {{ t(opt.labelKey) }}
723
+ <UIcon
724
+ v-if="opt.mode === 'answer' && isUserAnswered(item)"
725
+ name="i-lucide-check"
726
+ class="h-3.5 w-3.5 text-emerald-400"
727
+ />
728
+ </UButton>
729
+ </div>
673
730
 
674
- <!-- findings to react to -->
675
- <div v-if="review.items.length" class="flex flex-col gap-3">
676
- <div
677
- v-for="item in sortedItems"
678
- :key="item.id"
679
- class="rounded-lg border border-slate-800 bg-slate-900/60 p-3"
680
- :class="{ 'opacity-60': item.status === 'dismissed' }"
681
- >
682
- <div class="flex items-start gap-2">
683
- <UIcon
684
- :name="CATEGORY_ICON[item.category]"
685
- class="mt-0.5 h-4 w-4 shrink-0 text-slate-400"
686
- />
687
- <div class="min-w-0 flex-1">
688
- <div class="flex flex-wrap items-center gap-1.5">
689
- <span class="text-sm font-medium text-white">{{ item.title }}</span>
690
- <UBadge size="xs" variant="subtle" :color="SEVERITY_COLOR[item.severity]">
691
- {{ SEVERITY_LABELS[item.severity] }}
692
- </UBadge>
693
- <UBadge size="xs" variant="outline" color="neutral">
694
- {{ CATEGORY_LABELS[item.category] }}
695
- </UBadge>
696
- <!-- Once the automation has pre-answered some findings, flag the ones it
697
- left open as the genuine business decisions that need the human. -->
731
+ <!-- ANSWER: type the answer directly (auto-saves on blur) -->
732
+ <template v-if="modeFor(item) === 'answer'">
733
+ <!-- Auto-generated recommended default: the automation pre-filled this
734
+ answer; the human can keep it, edit it, or switch modes. -->
735
+ <div
736
+ v-if="autoDefaults.get(item.id)"
737
+ class="mt-2 flex flex-wrap items-center gap-1.5 text-xs text-indigo-300"
738
+ >
739
+ <UIcon name="i-lucide-sparkles" class="h-3.5 w-3.5 shrink-0" />
740
+ <span>{{ t('requirements.recommendedDefault') }}</span>
698
741
  <UBadge
699
- v-if="
700
- hasAutoDefaults &&
701
- item.status === 'open' &&
702
- item.autoAnswerable === false
703
- "
742
+ v-if="autoDefaults.get(item.id)!.groundedInFragment"
704
743
  size="xs"
705
744
  variant="subtle"
706
- color="warning"
745
+ color="primary"
707
746
  >
708
- {{ t('requirements.needsYourInput') }}
709
- </UBadge>
710
- <UBadge
711
- size="xs"
712
- variant="soft"
713
- :color="STATUS_COLOR[item.status]"
714
- class="ms-auto"
715
- >
716
- {{ STATUS_LABELS[item.status] }}
747
+ {{
748
+ t('requirements.currentStandard', {
749
+ title: autoDefaults.get(item.id)!.groundedInFragment!.title,
750
+ })
751
+ }}
717
752
  </UBadge>
718
753
  </div>
719
- <p class="mt-1 whitespace-pre-line text-sm text-slate-400">
720
- {{ item.detail }}
754
+ <UTextarea
755
+ v-model="drafts[item.id]"
756
+ :rows="2"
757
+ autoresize
758
+ size="sm"
759
+ class="mt-2 w-full"
760
+ :placeholder="t('requirements.answerPlaceholder')"
761
+ :disabled="frozen"
762
+ @blur="persistDraft(item)"
763
+ />
764
+ <p
765
+ v-if="isUserAnswered(item)"
766
+ class="mt-1 flex items-center gap-1 text-[11px] text-emerald-400"
767
+ >
768
+ <UIcon name="i-lucide-check" class="h-3 w-3 shrink-0" />
769
+ {{ t('requirements.userAnswered') }}
721
770
  </p>
771
+ </template>
722
772
 
723
- <!-- recorded answer (only for non-editable findings for editable
724
- ones the answer lives in the textarea below, seeded from the reply) -->
773
+ <!-- RECOMMEND: generating / the ready suggestion / a guidance box, all
774
+ rendered inline where the question was asked. -->
775
+ <template v-else-if="modeFor(item) === 'recommend'">
725
776
  <div
726
- v-if="item.reply && item.status !== 'open' && item.status !== 'answered'"
727
- class="mt-2 rounded-md border-s-2 border-slate-700 bg-slate-950/40 px-3 py-1.5 text-sm text-slate-300"
777
+ v-if="pendingRecFor(item)"
778
+ class="mt-2 flex items-center gap-1.5 text-xs text-indigo-300"
728
779
  >
729
- <span class="text-[10px] uppercase tracking-wide text-slate-500">
730
- {{ t('requirements.answerLabel') }}
731
- </span>
732
- <p class="whitespace-pre-line">{{ item.reply }}</p>
780
+ <UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 animate-spin" />
781
+ {{ t('requirements.generatingSuggestion') }}
733
782
  </div>
734
-
735
- <!-- per-finding 3-way selector: Answer (write it) / Dismiss (irrelevant) /
736
- Recommend (let the Requirement Writer suggest one). The active mode
737
- drives the content below, IN PLACE — no separate section. Disabled once
738
- the requirements are settled / a cycle is running; hidden for a
739
- `resolved` finding (its recorded answer shows above). -->
740
- <template v-if="item.status !== 'resolved'">
741
- <div class="mt-2 flex flex-wrap items-center gap-1">
742
- <UButton
743
- v-for="opt in FINDING_MODES"
744
- :key="opt.mode"
745
- :color="modeFor(item) === opt.mode ? 'primary' : 'neutral'"
746
- :variant="modeFor(item) === opt.mode ? 'soft' : 'ghost'"
783
+ <template v-else-if="readyRecFor(item)">
784
+ <div
785
+ v-for="rec in [readyRecFor(item)!]"
786
+ :key="rec.id"
787
+ class="mt-2 rounded-lg border border-indigo-900/50 bg-indigo-950/20 p-3"
788
+ >
789
+ <UBadge
790
+ v-if="rec.groundedInFragment"
747
791
  size="xs"
748
- :icon="opt.icon"
749
- :disabled="frozen"
750
- @click="setMode(item, opt.mode)"
792
+ variant="subtle"
793
+ color="success"
794
+ icon="i-lucide-badge-check"
751
795
  >
752
- {{ t(opt.labelKey) }}
753
- <UIcon
754
- v-if="opt.mode === 'answer' && isUserAnswered(item)"
755
- name="i-lucide-check"
756
- class="h-3.5 w-3.5 text-emerald-400"
757
- />
758
- </UButton>
759
- </div>
760
-
761
- <!-- ANSWER: type the answer directly (auto-saves on blur) -->
762
- <template v-if="modeFor(item) === 'answer'">
763
- <!-- Auto-generated recommended default: the automation pre-filled this
764
- answer; the human can keep it, edit it, or switch modes. -->
765
- <div
766
- v-if="autoDefaults.get(item.id)"
767
- class="mt-2 flex flex-wrap items-center gap-1.5 text-xs text-indigo-300"
768
- >
769
- <UIcon name="i-lucide-sparkles" class="h-3.5 w-3.5 shrink-0" />
770
- <span>{{ t('requirements.recommendedDefault') }}</span>
771
- <UBadge
772
- v-if="autoDefaults.get(item.id)!.groundedInFragment"
773
- size="xs"
774
- variant="subtle"
796
+ {{
797
+ t('requirements.currentStandard', {
798
+ title: rec.groundedInFragment.title,
799
+ })
800
+ }}
801
+ </UBadge>
802
+ <p class="mt-1 whitespace-pre-line text-sm text-slate-300">
803
+ {{ rec.recommendedText }}
804
+ </p>
805
+ <div class="mt-2 flex flex-wrap items-center gap-2">
806
+ <UButton
775
807
  color="primary"
808
+ variant="soft"
809
+ size="xs"
810
+ icon="i-lucide-check"
811
+ :disabled="frozen || !access.canExecuteRuns.value"
812
+ :title="
813
+ access.canExecuteRuns.value ? undefined : t('access.noRunExecute')
814
+ "
815
+ @click="acceptRecommendation(rec)"
776
816
  >
777
- {{
778
- t('requirements.currentStandard', {
779
- title: autoDefaults.get(item.id)!.groundedInFragment!.title,
780
- })
781
- }}
782
- </UBadge>
783
- </div>
784
- <UTextarea
785
- v-model="drafts[item.id]"
786
- :rows="2"
787
- autoresize
788
- size="sm"
789
- class="mt-2 w-full"
790
- :placeholder="t('requirements.answerPlaceholder')"
791
- :disabled="frozen"
792
- @blur="persistDraft(item)"
793
- />
794
- <p
795
- v-if="isUserAnswered(item)"
796
- class="mt-1 flex items-center gap-1 text-[11px] text-emerald-400"
797
- >
798
- <UIcon name="i-lucide-check" class="h-3 w-3 shrink-0" />
799
- {{ t('requirements.userAnswered') }}
800
- </p>
801
- </template>
802
-
803
- <!-- RECOMMEND: generating / the ready suggestion / a guidance box, all
804
- rendered inline where the question was asked. -->
805
- <template v-else-if="modeFor(item) === 'recommend'">
806
- <div
807
- v-if="pendingRecFor(item)"
808
- class="mt-2 flex items-center gap-1.5 text-xs text-indigo-300"
809
- >
810
- <UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 animate-spin" />
811
- {{ t('requirements.generatingSuggestion') }}
812
- </div>
813
- <template v-else-if="readyRecFor(item)">
814
- <div
815
- v-for="rec in [readyRecFor(item)!]"
816
- :key="rec.id"
817
- class="mt-2 rounded-lg border border-indigo-900/50 bg-indigo-950/20 p-3"
817
+ {{ t('requirements.accept') }}
818
+ </UButton>
819
+ <UButton
820
+ color="neutral"
821
+ variant="ghost"
822
+ size="xs"
823
+ icon="i-lucide-x"
824
+ :disabled="frozen || !access.canExecuteRuns.value"
825
+ :title="
826
+ access.canExecuteRuns.value ? undefined : t('access.noRunExecute')
827
+ "
828
+ @click="rejectRecommendation(rec)"
818
829
  >
819
- <UBadge
820
- v-if="rec.groundedInFragment"
821
- size="xs"
822
- variant="subtle"
823
- color="success"
824
- icon="i-lucide-badge-check"
825
- >
826
- {{
827
- t('requirements.currentStandard', {
828
- title: rec.groundedInFragment.title,
829
- })
830
- }}
831
- </UBadge>
832
- <p class="mt-1 whitespace-pre-line text-sm text-slate-300">
833
- {{ rec.recommendedText }}
834
- </p>
835
- <div class="mt-2 flex flex-wrap items-center gap-2">
836
- <UButton
837
- color="primary"
838
- variant="soft"
839
- size="xs"
840
- icon="i-lucide-check"
841
- :disabled="frozen || !access.canExecuteRuns.value"
842
- :title="
843
- access.canExecuteRuns.value
844
- ? undefined
845
- : t('access.noRunExecute')
846
- "
847
- @click="acceptRecommendation(rec)"
848
- >
849
- {{ t('requirements.accept') }}
850
- </UButton>
851
- <UButton
852
- color="neutral"
853
- variant="ghost"
854
- size="xs"
855
- icon="i-lucide-x"
856
- :disabled="frozen || !access.canExecuteRuns.value"
857
- :title="
858
- access.canExecuteRuns.value
859
- ? undefined
860
- : t('access.noRunExecute')
861
- "
862
- @click="rejectRecommendation(rec)"
863
- >
864
- {{ t('requirements.reject') }}
865
- </UButton>
866
- </div>
867
- <div class="mt-2 flex items-start gap-2">
868
- <UTextarea
869
- v-model="reRequestNotes[rec.id]"
870
- :rows="1"
871
- autoresize
872
- size="sm"
873
- class="flex-1"
874
- :placeholder="t('requirements.reRequestPlaceholder')"
875
- :disabled="frozen || recommending"
876
- />
877
- <UButton
878
- color="neutral"
879
- variant="soft"
880
- size="xs"
881
- icon="i-lucide-rotate-cw"
882
- :loading="recommending"
883
- :disabled="
884
- !(reRequestNotes[rec.id] ?? '').trim() ||
885
- frozen ||
886
- !access.canExecuteRuns.value
887
- "
888
- :title="
889
- access.canExecuteRuns.value
890
- ? undefined
891
- : t('access.noRunExecute')
892
- "
893
- @click="reRequestRecommendation(rec)"
894
- >
895
- {{ t('requirements.reRequest') }}
896
- </UButton>
897
- </div>
898
- </div>
899
- </template>
900
- <template v-else>
830
+ {{ t('requirements.reject') }}
831
+ </UButton>
832
+ </div>
833
+ <div class="mt-2 flex items-start gap-2">
901
834
  <UTextarea
902
- v-model="guidanceDrafts[item.id]"
903
- :rows="2"
835
+ v-model="reRequestNotes[rec.id]"
836
+ :rows="1"
904
837
  autoresize
905
838
  size="sm"
906
- class="mt-2 w-full"
907
- :placeholder="t('requirements.guidancePlaceholder')"
908
- :disabled="frozen"
839
+ class="flex-1"
840
+ :placeholder="t('requirements.reRequestPlaceholder')"
841
+ :disabled="frozen || recommending"
909
842
  />
910
- <p class="mt-1 flex items-center gap-1 text-[11px] text-indigo-300/80">
911
- <UIcon name="i-lucide-wand-2" class="h-3 w-3 shrink-0" />
912
- {{ t('requirements.guidanceHint') }}
913
- </p>
914
- </template>
915
- </template>
916
-
917
- <!-- DISMISS: nothing to fill in — a short note explains the effect -->
918
- <p v-else class="mt-2 text-[11px] text-slate-500">
919
- {{ t('requirements.dismissedHint') }}
843
+ <UButton
844
+ color="neutral"
845
+ variant="soft"
846
+ size="xs"
847
+ icon="i-lucide-rotate-cw"
848
+ :loading="recommending"
849
+ :disabled="
850
+ !(reRequestNotes[rec.id] ?? '').trim() ||
851
+ frozen ||
852
+ !access.canExecuteRuns.value
853
+ "
854
+ :title="
855
+ access.canExecuteRuns.value ? undefined : t('access.noRunExecute')
856
+ "
857
+ @click="reRequestRecommendation(rec)"
858
+ >
859
+ {{ t('requirements.reRequest') }}
860
+ </UButton>
861
+ </div>
862
+ </div>
863
+ </template>
864
+ <template v-else>
865
+ <UTextarea
866
+ v-model="guidanceDrafts[item.id]"
867
+ :rows="2"
868
+ autoresize
869
+ size="sm"
870
+ class="mt-2 w-full"
871
+ :placeholder="t('requirements.guidancePlaceholder')"
872
+ :disabled="frozen"
873
+ />
874
+ <p class="mt-1 flex items-center gap-1 text-[11px] text-indigo-300/80">
875
+ <UIcon name="i-lucide-wand-2" class="h-3 w-3 shrink-0" />
876
+ {{ t('requirements.guidanceHint') }}
920
877
  </p>
921
878
  </template>
922
- </div>
923
- </div>
879
+ </template>
880
+
881
+ <!-- DISMISS: nothing to fill in — a short note explains the effect -->
882
+ <p v-else class="mt-2 text-[11px] text-slate-500">
883
+ {{ t('requirements.dismissedHint') }}
884
+ </p>
885
+ </template>
924
886
  </div>
925
887
  </div>
888
+ </div>
889
+ </div>
926
890
 
927
- <!-- incorporated document: the standard-format requirements. The whole section
891
+ <!-- incorporated document: the standard-format requirements. The whole section
928
892
  collapses as a unit (a long doc otherwise pushes the findings/recommendations
929
893
  off-screen); the per-heading toggles below still work when it's expanded. -->
930
- <section v-if="outline" class="mt-6 border-t border-slate-800 pt-5">
894
+ <section v-if="outline" class="mt-6 border-t border-slate-800 pt-5">
895
+ <button
896
+ class="mb-3 flex w-full items-center gap-1.5 text-[11px] text-emerald-400"
897
+ @click="toggleDoc"
898
+ >
899
+ <UIcon
900
+ name="i-lucide-chevron-right"
901
+ class="h-3.5 w-3.5 shrink-0 transition-transform"
902
+ :class="docCollapsed ? '' : 'rotate-90'"
903
+ />
904
+ <UIcon name="i-lucide-file-check-2" class="h-3.5 w-3.5" />
905
+ <span class="font-semibold uppercase tracking-wide">
906
+ {{
907
+ incorporated
908
+ ? t('requirements.finalRequirements')
909
+ : t('requirements.incorporatedDraft')
910
+ }}
911
+ </span>
912
+ </button>
913
+ <div v-show="!docCollapsed">
914
+ <div v-for="s in outline.sections" :key="s.id" class="mb-2">
931
915
  <button
932
- class="mb-3 flex w-full items-center gap-1.5 text-[11px] text-emerald-400"
933
- @click="toggleDoc"
916
+ v-if="s.title"
917
+ class="group flex w-full items-center gap-2 text-start"
918
+ @click="toggle(s.id)"
934
919
  >
935
920
  <UIcon
936
921
  name="i-lucide-chevron-right"
937
- class="h-3.5 w-3.5 shrink-0 transition-transform"
938
- :class="docCollapsed ? '' : 'rotate-90'"
922
+ class="h-3.5 w-3.5 shrink-0 text-slate-500 transition-transform"
923
+ :class="collapsed[s.id] ? '' : 'rotate-90'"
924
+ />
925
+ <span
926
+ class="font-semibold text-white"
927
+ :class="s.depth <= 1 ? 'text-base' : s.depth === 2 ? 'text-sm' : 'text-xs'"
928
+ v-html="s.titleHtml"
939
929
  />
940
- <UIcon name="i-lucide-file-check-2" class="h-3.5 w-3.5" />
941
- <span class="font-semibold uppercase tracking-wide">
942
- {{
943
- incorporated
944
- ? t('requirements.finalRequirements')
945
- : t('requirements.incorporatedDraft')
946
- }}
947
- </span>
948
930
  </button>
949
- <div v-show="!docCollapsed">
950
- <div v-for="s in outline.sections" :key="s.id" class="mb-2">
951
- <button
952
- v-if="s.title"
953
- class="group flex w-full items-center gap-2 text-start"
954
- @click="toggle(s.id)"
955
- >
956
- <UIcon
957
- name="i-lucide-chevron-right"
958
- class="h-3.5 w-3.5 shrink-0 text-slate-500 transition-transform"
959
- :class="collapsed[s.id] ? '' : 'rotate-90'"
960
- />
961
- <span
962
- class="font-semibold text-white"
963
- :class="s.depth <= 1 ? 'text-base' : s.depth === 2 ? 'text-sm' : 'text-xs'"
964
- v-html="s.titleHtml"
965
- />
966
- </button>
967
- <div
968
- v-show="!s.title || !collapsed[s.id]"
969
- class="reader-prose mt-1 ps-5.5 text-[13px] leading-relaxed text-slate-300"
970
- v-html="s.bodyHtml"
971
- />
972
- </div>
973
- </div>
974
- </section>
975
- </template>
976
- </div>
931
+ <div
932
+ v-show="!s.title || !collapsed[s.id]"
933
+ class="reader-prose mt-1 ps-5.5 text-[13px] leading-relaxed text-slate-300"
934
+ v-html="s.bodyHtml"
935
+ />
936
+ </div>
937
+ </div>
938
+ </section>
939
+ </template>
940
+ </div>
977
941
 
978
- <!-- action rail: a right-hand column on wide screens, a bottom action bar below `lg`
942
+ <!-- action rail: a right-hand column on wide screens, a bottom action bar below `lg`
979
943
  (never hidden — the gate is otherwise unadvanceable on a laptop split-screen /
980
944
  tablet, UX-32). The informational stats collapse away below `lg` to keep the
981
945
  bottom bar compact; the actions themselves always show. -->
982
- <aside
983
- class="flex w-full shrink-0 flex-col border-t border-slate-800 lg:w-72 lg:border-s lg:border-t-0"
984
- >
985
- <div class="flex flex-col gap-4 px-4 py-5">
986
- <div v-if="review" class="hidden space-y-2 text-xs text-slate-400 lg:block">
987
- <div class="flex items-center justify-between">
988
- <span>{{ t('requirements.stats.findings') }}</span>
989
- <span class="text-slate-300">{{ review.items.length }}</span>
990
- </div>
991
- <div class="flex items-center justify-between">
992
- <span>{{ t('requirements.stats.open') }}</span>
993
- <span class="text-slate-300">{{ openCount }}</span>
994
- </div>
995
- <div class="flex items-center justify-between">
996
- <span>{{ t('requirements.stats.answered') }}</span>
997
- <span class="text-slate-300">{{ answeredCount }}</span>
998
- </div>
999
- <!-- awaited recommendations — kept here (always visible) so the human can see what
946
+ <aside
947
+ class="flex w-full shrink-0 flex-col border-t border-slate-800 lg:w-72 lg:border-s lg:border-t-0"
948
+ >
949
+ <div class="flex flex-col gap-4 px-4 py-5">
950
+ <div v-if="review" class="hidden space-y-2 text-xs text-slate-400 lg:block">
951
+ <div class="flex items-center justify-between">
952
+ <span>{{ t('requirements.stats.findings') }}</span>
953
+ <span class="text-slate-300">{{ review.items.length }}</span>
954
+ </div>
955
+ <div class="flex items-center justify-between">
956
+ <span>{{ t('requirements.stats.open') }}</span>
957
+ <span class="text-slate-300">{{ openCount }}</span>
958
+ </div>
959
+ <div class="flex items-center justify-between">
960
+ <span>{{ t('requirements.stats.answered') }}</span>
961
+ <span class="text-slate-300">{{ answeredCount }}</span>
962
+ </div>
963
+ <!-- awaited recommendations — kept here (always visible) so the human can see what
1000
964
  the Writer is still producing / what's waiting on them even while reading the
1001
965
  incorporated document or acting elsewhere in the window. -->
1002
- <template v-if="generatingRecommendations.length || readyRecommendations.length">
1003
- <div
1004
- class="flex items-center gap-1.5 border-t border-slate-800/60 pt-2 text-indigo-300"
1005
- >
1006
- <UIcon name="i-lucide-wand-2" class="h-3 w-3" />
1007
- <span class="font-medium">{{ t('requirements.stats.recommendations') }}</span>
1008
- </div>
1009
- <div
1010
- v-if="generatingRecommendations.length"
1011
- class="flex items-center justify-between"
1012
- >
1013
- <span>{{ t('requirements.stats.recsGenerating') }}</span>
1014
- <span class="text-indigo-300">{{ generatingRecommendations.length }}</span>
1015
- </div>
1016
- <div v-if="readyRecommendations.length" class="flex items-center justify-between">
1017
- <span>{{ t('requirements.stats.recsToReview') }}</span>
1018
- <span class="text-indigo-300">{{ readyRecommendations.length }}</span>
1019
- </div>
1020
- </template>
1021
- <div v-if="review.model" class="flex items-center justify-between">
1022
- <span>{{ t('requirements.stats.model') }}</span>
1023
- <span class="truncate ps-2 text-slate-500">{{
1024
- models.labelForRef(review.model) ?? review.model
1025
- }}</span>
1026
- </div>
966
+ <template v-if="generatingRecommendations.length || readyRecommendations.length">
967
+ <div
968
+ class="flex items-center gap-1.5 border-t border-slate-800/60 pt-2 text-indigo-300"
969
+ >
970
+ <UIcon name="i-lucide-wand-2" class="h-3 w-3" />
971
+ <span class="font-medium">{{ t('requirements.stats.recommendations') }}</span>
972
+ </div>
973
+ <div
974
+ v-if="generatingRecommendations.length"
975
+ class="flex items-center justify-between"
976
+ >
977
+ <span>{{ t('requirements.stats.recsGenerating') }}</span>
978
+ <span class="text-indigo-300">{{ generatingRecommendations.length }}</span>
979
+ </div>
980
+ <div v-if="readyRecommendations.length" class="flex items-center justify-between">
981
+ <span>{{ t('requirements.stats.recsToReview') }}</span>
982
+ <span class="text-indigo-300">{{ readyRecommendations.length }}</span>
1027
983
  </div>
984
+ </template>
985
+ <div v-if="review.model" class="flex items-center justify-between">
986
+ <span>{{ t('requirements.stats.model') }}</span>
987
+ <span class="truncate ps-2 text-slate-500">{{
988
+ models.labelForRef(review.model) ?? review.model
989
+ }}</span>
990
+ </div>
991
+ </div>
1028
992
 
1029
- <!-- Request the Requirement Writer for the marked findings. Kept OUT of the
993
+ <!-- Request the Requirement Writer for the marked findings. Kept OUT of the
1030
994
  status-scoped blocks below so it's available whenever the review is still
1031
995
  editable — the `ready` first pass AND a `merged` review being reworked — not
1032
996
  only when status is exactly `ready`. Scoped to exactly those two states (NOT a
1033
997
  bare `!frozen`, which would also expose it in `exceeded`, where the run is parked
1034
998
  on the cap decision and a fresh recommendation batch has no path to settle). -->
1035
- <div
1036
- v-if="
1037
- review && pendingRecommendRequests.length > 0 && (status === 'ready' || merged)
1038
- "
1039
- class="border-t border-slate-800 pt-4"
1040
- >
1041
- <UButton
1042
- color="primary"
1043
- variant="soft"
1044
- size="sm"
1045
- block
1046
- icon="i-lucide-wand-2"
1047
- :loading="recommending"
1048
- :disabled="!access.canExecuteRuns.value"
1049
- :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
1050
- @click="requestRecommendations"
1051
- >
1052
- {{
1053
- t(
1054
- 'requirements.actions.requestRecommendations',
1055
- { count: pendingRecommendRequests.length },
1056
- pendingRecommendRequests.length,
1057
- )
1058
- }}
1059
- </UButton>
1060
- </div>
1061
-
1062
- <!-- action: ready (answer → incorporate / proceed) -->
1063
- <div
1064
- v-if="review && status === 'ready'"
1065
- class="space-y-2 border-t border-slate-800 pt-4"
1066
- >
1067
- <UButton
1068
- v-if="canProceed"
1069
- color="primary"
1070
- size="sm"
1071
- block
1072
- icon="i-lucide-arrow-right"
1073
- :ui="{ leadingIcon: 'rtl:-scale-x-100', trailingIcon: 'rtl:-scale-x-100' }"
1074
- :loading="acting"
1075
- :disabled="!access.canExecuteRuns.value"
1076
- :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
1077
- @click="proceed"
1078
- >
1079
- {{ t('requirements.actions.proceedNothing') }}
1080
- </UButton>
1081
- <UButton
1082
- v-else
1083
- color="primary"
1084
- size="sm"
1085
- block
1086
- icon="i-lucide-wand-sparkles"
1087
- :loading="reworking"
1088
- :disabled="!canIncorporate || !access.canExecuteRuns.value"
1089
- :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
1090
- @click="incorporate()"
1091
- >
1092
- {{ t('requirements.actions.incorporateAnswers') }}
1093
- </UButton>
1094
- <p class="text-[11px] leading-relaxed text-slate-500">
1095
- <template v-if="canProceed">
1096
- {{ t('requirements.help.canProceed') }}
1097
- </template>
1098
- <template v-else-if="canIncorporate">
1099
- {{ t('requirements.help.canIncorporate') }}
1100
- </template>
1101
- <template v-else> {{ t('requirements.help.answerAll') }} </template>
1102
- </p>
1103
- </div>
999
+ <div
1000
+ v-if="review && pendingRecommendRequests.length > 0 && (status === 'ready' || merged)"
1001
+ class="border-t border-slate-800 pt-4"
1002
+ >
1003
+ <UButton
1004
+ color="primary"
1005
+ variant="soft"
1006
+ size="sm"
1007
+ block
1008
+ icon="i-lucide-wand-2"
1009
+ :loading="recommending"
1010
+ :disabled="!access.canExecuteRuns.value"
1011
+ :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
1012
+ @click="requestRecommendations"
1013
+ >
1014
+ {{
1015
+ t(
1016
+ 'requirements.actions.requestRecommendations',
1017
+ { count: pendingRecommendRequests.length },
1018
+ pendingRecommendRequests.length,
1019
+ )
1020
+ }}
1021
+ </UButton>
1022
+ </div>
1104
1023
 
1105
- <!-- action: merged (inspectre-review / redo) -->
1106
- <div v-if="review && merged" class="space-y-2 border-t border-slate-800 pt-4">
1107
- <UButton
1108
- color="primary"
1109
- size="sm"
1110
- block
1111
- icon="i-lucide-sparkles"
1112
- :loading="busy"
1113
- :disabled="!access.canExecuteRuns.value"
1114
- :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
1115
- @click="reReview"
1116
- >
1117
- {{
1118
- busy
1119
- ? t('requirements.actions.reReviewing')
1120
- : t('requirements.actions.reReview')
1121
- }}
1122
- </UButton>
1123
- <UButton
1124
- color="neutral"
1125
- variant="soft"
1126
- size="sm"
1127
- block
1128
- icon="i-lucide-pencil"
1129
- @click="
1130
- () => {
1131
- showRedo = !showRedo
1132
- }
1133
- "
1134
- >
1135
- {{ t('requirements.actions.redoIncorporation') }}
1136
- </UButton>
1137
- <div v-if="showRedo" class="space-y-2">
1138
- <UTextarea
1139
- v-model="redoComment"
1140
- :rows="3"
1141
- autoresize
1142
- size="sm"
1143
- class="w-full"
1144
- :placeholder="t('requirements.redoPlaceholder')"
1145
- />
1146
- <UButton
1147
- color="primary"
1148
- variant="soft"
1149
- size="xs"
1150
- block
1151
- icon="i-lucide-wand-sparkles"
1152
- :loading="reworking"
1153
- :disabled="!redoComment.trim() || !access.canExecuteRuns.value"
1154
- :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
1155
- @click="incorporate(redoComment.trim())"
1156
- >
1157
- {{ t('requirements.actions.redoWithDirection') }}
1158
- </UButton>
1159
- </div>
1160
- <p class="text-[11px] leading-relaxed text-slate-500">
1161
- {{ t('requirements.help.merged') }}
1162
- </p>
1163
- </div>
1024
+ <!-- action: ready (answerincorporate / proceed) -->
1025
+ <div v-if="review && status === 'ready'" class="space-y-2 border-t border-slate-800 pt-4">
1026
+ <UButton
1027
+ v-if="canProceed"
1028
+ color="primary"
1029
+ size="sm"
1030
+ block
1031
+ icon="i-lucide-arrow-right"
1032
+ :ui="{ leadingIcon: 'rtl:-scale-x-100', trailingIcon: 'rtl:-scale-x-100' }"
1033
+ :loading="acting"
1034
+ :disabled="!access.canExecuteRuns.value"
1035
+ :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
1036
+ @click="proceed"
1037
+ >
1038
+ {{ t('requirements.actions.proceedNothing') }}
1039
+ </UButton>
1040
+ <UButton
1041
+ v-else
1042
+ color="primary"
1043
+ size="sm"
1044
+ block
1045
+ icon="i-lucide-wand-sparkles"
1046
+ :loading="reworking"
1047
+ :disabled="!canIncorporate || !access.canExecuteRuns.value"
1048
+ :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
1049
+ @click="incorporate()"
1050
+ >
1051
+ {{ t('requirements.actions.incorporateAnswers') }}
1052
+ </UButton>
1053
+ <p class="text-[11px] leading-relaxed text-slate-500">
1054
+ <template v-if="canProceed">
1055
+ {{ t('requirements.help.canProceed') }}
1056
+ </template>
1057
+ <template v-else-if="canIncorporate">
1058
+ {{ t('requirements.help.canIncorporate') }}
1059
+ </template>
1060
+ <template v-else> {{ t('requirements.help.answerAll') }} </template>
1061
+ </p>
1062
+ </div>
1164
1063
 
1165
- <div
1166
- v-if="review && incorporated"
1167
- class="border-t border-slate-800 pt-4 text-[11px] leading-relaxed text-slate-500"
1064
+ <!-- action: merged (inspect → re-review / redo) -->
1065
+ <div v-if="review && merged" class="space-y-2 border-t border-slate-800 pt-4">
1066
+ <UButton
1067
+ color="primary"
1068
+ size="sm"
1069
+ block
1070
+ icon="i-lucide-sparkles"
1071
+ :loading="busy"
1072
+ :disabled="!access.canExecuteRuns.value"
1073
+ :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
1074
+ @click="reReview"
1075
+ >
1076
+ {{
1077
+ busy ? t('requirements.actions.reReviewing') : t('requirements.actions.reReview')
1078
+ }}
1079
+ </UButton>
1080
+ <UButton
1081
+ color="neutral"
1082
+ variant="soft"
1083
+ size="sm"
1084
+ block
1085
+ icon="i-lucide-pencil"
1086
+ @click="
1087
+ () => {
1088
+ showRedo = !showRedo
1089
+ }
1090
+ "
1091
+ >
1092
+ {{ t('requirements.actions.redoIncorporation') }}
1093
+ </UButton>
1094
+ <div v-if="showRedo" class="space-y-2">
1095
+ <UTextarea
1096
+ v-model="redoComment"
1097
+ :rows="3"
1098
+ autoresize
1099
+ size="sm"
1100
+ class="w-full"
1101
+ :placeholder="t('requirements.redoPlaceholder')"
1102
+ />
1103
+ <UButton
1104
+ color="primary"
1105
+ variant="soft"
1106
+ size="xs"
1107
+ block
1108
+ icon="i-lucide-wand-sparkles"
1109
+ :loading="reworking"
1110
+ :disabled="!redoComment.trim() || !access.canExecuteRuns.value"
1111
+ :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
1112
+ @click="incorporate(redoComment.trim())"
1168
1113
  >
1169
- {{ t('requirements.settledFooter') }}
1170
- </div>
1114
+ {{ t('requirements.actions.redoWithDirection') }}
1115
+ </UButton>
1171
1116
  </div>
1172
- </aside>
1117
+ <p class="text-[11px] leading-relaxed text-slate-500">
1118
+ {{ t('requirements.help.merged') }}
1119
+ </p>
1120
+ </div>
1121
+
1122
+ <div
1123
+ v-if="review && incorporated"
1124
+ class="border-t border-slate-800 pt-4 text-[11px] leading-relaxed text-slate-500"
1125
+ >
1126
+ {{ t('requirements.settledFooter') }}
1127
+ </div>
1173
1128
  </div>
1174
- </div>
1129
+ </aside>
1175
1130
  </div>
1176
- </Teleport>
1131
+ </ResultWindowShell>
1177
1132
  </template>
1178
1133
 
1179
1134
  <style scoped>