@cat-factory/app 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/app/app.config.ts +8 -0
- package/app/app.vue +11 -0
- package/app/assets/css/main.css +100 -0
- package/app/components/auth/AuthGate.vue +24 -0
- package/app/components/auth/LoginScreen.vue +143 -0
- package/app/components/auth/UserMenu.vue +39 -0
- package/app/components/board/AddTaskModal.vue +444 -0
- package/app/components/board/AgentFailureCard.vue +97 -0
- package/app/components/board/AgentStopButton.vue +61 -0
- package/app/components/board/BoardCanvas.vue +183 -0
- package/app/components/board/ContextPicker.vue +367 -0
- package/app/components/board/RecurringPipelineModal.vue +219 -0
- package/app/components/board/TaskDependencyEdges.vue +132 -0
- package/app/components/board/nodes/AgentChip.vue +59 -0
- package/app/components/board/nodes/BlockNode.vue +433 -0
- package/app/components/board/nodes/DecisionBadge.vue +27 -0
- package/app/components/board/nodes/DraggableTask.vue +48 -0
- package/app/components/board/nodes/ModuleFrame.vue +97 -0
- package/app/components/board/nodes/TaskCard.vue +359 -0
- package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
- package/app/components/bootstrap/BootstrapModal.vue +665 -0
- package/app/components/clarity/ClarityReviewWindow.vue +611 -0
- package/app/components/consensus/ConsensusSessionWindow.vue +210 -0
- package/app/components/documents/DocumentImportModal.vue +161 -0
- package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
- package/app/components/documents/SpawnPreviewModal.vue +161 -0
- package/app/components/documents/TaskContextDocs.vue +83 -0
- package/app/components/focus/BlockFocusView.vue +171 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
- package/app/components/gates/GateResultView.vue +282 -0
- package/app/components/github/AddServiceFromRepoModal.vue +354 -0
- package/app/components/github/GitHubConnect.vue +183 -0
- package/app/components/github/GitHubOnboarding.vue +45 -0
- package/app/components/github/GitHubPanel.vue +584 -0
- package/app/components/github/RepoTreeBrowser.vue +171 -0
- package/app/components/layout/AccountTeamSettings.vue +237 -0
- package/app/components/layout/BoardSwitcher.vue +280 -0
- package/app/components/layout/BoardToolbar.vue +156 -0
- package/app/components/layout/CommandBar.vue +336 -0
- package/app/components/layout/GitHubPatBanner.vue +73 -0
- package/app/components/layout/NotificationsInbox.vue +175 -0
- package/app/components/layout/SideBar.vue +314 -0
- package/app/components/layout/SpendWarningBanner.vue +107 -0
- package/app/components/observability/StepMetricsBar.vue +102 -0
- package/app/components/palettes/AgentPalette.vue +86 -0
- package/app/components/panels/AgentStepDetail.vue +737 -0
- package/app/components/panels/DecisionModal.vue +71 -0
- package/app/components/panels/InspectorPanel.vue +465 -0
- package/app/components/panels/ObservabilityPanel.vue +351 -0
- package/app/components/panels/StepMetadataCard.vue +253 -0
- package/app/components/panels/StepRestartControl.vue +90 -0
- package/app/components/panels/StepResultViewHost.vue +40 -0
- package/app/components/panels/StepTestReport.vue +84 -0
- package/app/components/panels/inspector/ContainerSummary.vue +74 -0
- package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
- package/app/components/panels/inspector/ServiceFragments.vue +82 -0
- package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
- package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
- package/app/components/panels/inspector/TaskDependencies.vue +70 -0
- package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
- package/app/components/panels/inspector/TaskExecution.vue +364 -0
- package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
- package/app/components/panels/inspector/TaskStructure.vue +96 -0
- package/app/components/pipeline/AgentKindIcon.vue +30 -0
- package/app/components/pipeline/IterationCapPrompt.vue +70 -0
- package/app/components/pipeline/PipelineBuilder.vue +817 -0
- package/app/components/pipeline/PipelineProgress.vue +484 -0
- package/app/components/providers/ApiKeysSection.vue +273 -0
- package/app/components/providers/PersonalCredentialModal.vue +128 -0
- package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
- package/app/components/providers/VendorCredentialsModal.vue +197 -0
- package/app/components/recurring/RecurrenceEditor.vue +124 -0
- package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
- package/app/components/settings/DatadogPanel.vue +213 -0
- package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
- package/app/components/settings/MergeThresholdsPanel.vue +378 -0
- package/app/components/settings/ModelDefaultsPanel.vue +250 -0
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
- package/app/components/slack/SlackPanel.vue +299 -0
- package/app/components/tasks/TaskContextIssues.vue +88 -0
- package/app/components/tasks/TaskImportModal.vue +207 -0
- package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
- package/app/components/testing/TestReportWindow.vue +404 -0
- package/app/composables/api/accounts.ts +81 -0
- package/app/composables/api/auth.ts +45 -0
- package/app/composables/api/board.ts +101 -0
- package/app/composables/api/bootstrap.ts +62 -0
- package/app/composables/api/context.ts +25 -0
- package/app/composables/api/documents.ts +74 -0
- package/app/composables/api/execution.ts +127 -0
- package/app/composables/api/fragments.ts +71 -0
- package/app/composables/api/github.ts +131 -0
- package/app/composables/api/models.ts +127 -0
- package/app/composables/api/notifications.ts +23 -0
- package/app/composables/api/presets.ts +29 -0
- package/app/composables/api/recurring.ts +68 -0
- package/app/composables/api/releaseHealth.ts +43 -0
- package/app/composables/api/reviews.ts +146 -0
- package/app/composables/api/slack.ts +54 -0
- package/app/composables/api/tasks.ts +72 -0
- package/app/composables/api/workspaces.ts +36 -0
- package/app/composables/useApi.ts +89 -0
- package/app/composables/useBlockDrag.ts +90 -0
- package/app/composables/useBlockQueries.ts +154 -0
- package/app/composables/useBoardFlow.ts +11 -0
- package/app/composables/useContextLinking.ts +65 -0
- package/app/composables/useDepLabels.ts +26 -0
- package/app/composables/useFrameResize.ts +54 -0
- package/app/composables/useResultView.ts +48 -0
- package/app/composables/useReviewStage.ts +40 -0
- package/app/composables/useSemanticZoom.ts +31 -0
- package/app/composables/useStepApproval.ts +233 -0
- package/app/composables/useStepProse.ts +78 -0
- package/app/composables/useStepTimer.ts +63 -0
- package/app/composables/useTaskExpansion.ts +92 -0
- package/app/composables/useWorkspaceStream.ts +155 -0
- package/app/docs/architecture.md +31 -0
- package/app/pages/index.vue +141 -0
- package/app/stores/accounts.ts +152 -0
- package/app/stores/agentConfig.ts +35 -0
- package/app/stores/agentRuns.ts +122 -0
- package/app/stores/agents.ts +40 -0
- package/app/stores/apiKeys.ts +108 -0
- package/app/stores/auth.ts +166 -0
- package/app/stores/board.spec.ts +205 -0
- package/app/stores/board.ts +286 -0
- package/app/stores/bootstrap.ts +97 -0
- package/app/stores/clarity.ts +196 -0
- package/app/stores/consensus.ts +60 -0
- package/app/stores/documents.ts +176 -0
- package/app/stores/execution.ts +273 -0
- package/app/stores/fragmentLibrary.ts +147 -0
- package/app/stores/fragments.ts +40 -0
- package/app/stores/github.ts +305 -0
- package/app/stores/localModels.ts +51 -0
- package/app/stores/mergePresets.ts +58 -0
- package/app/stores/modelDefaults.ts +76 -0
- package/app/stores/models.ts +134 -0
- package/app/stores/notifications.ts +70 -0
- package/app/stores/observability.ts +144 -0
- package/app/stores/personalSubscriptions.ts +215 -0
- package/app/stores/pipelines.ts +327 -0
- package/app/stores/recurringPipelines.ts +112 -0
- package/app/stores/releaseHealth.ts +75 -0
- package/app/stores/requirements.spec.ts +94 -0
- package/app/stores/requirements.ts +208 -0
- package/app/stores/serviceFragmentDefaults.ts +29 -0
- package/app/stores/services.ts +87 -0
- package/app/stores/slack.ts +142 -0
- package/app/stores/taskExpansion.ts +36 -0
- package/app/stores/tasks.spec.ts +71 -0
- package/app/stores/tasks.ts +176 -0
- package/app/stores/tracker.ts +27 -0
- package/app/stores/ui.ts +434 -0
- package/app/stores/vendorCredentials.ts +54 -0
- package/app/stores/workspace.ts +215 -0
- package/app/stores/workspaceSettings.ts +36 -0
- package/app/types/accounts.ts +77 -0
- package/app/types/bootstrap.ts +83 -0
- package/app/types/clarity.ts +59 -0
- package/app/types/consensus.ts +91 -0
- package/app/types/documents.ts +104 -0
- package/app/types/domain.ts +495 -0
- package/app/types/execution.ts +383 -0
- package/app/types/fragments.ts +72 -0
- package/app/types/github.ts +173 -0
- package/app/types/localModels.ts +73 -0
- package/app/types/merge.ts +71 -0
- package/app/types/models.ts +157 -0
- package/app/types/notifications.ts +74 -0
- package/app/types/recurring.ts +69 -0
- package/app/types/releaseHealth.ts +31 -0
- package/app/types/requirements.ts +61 -0
- package/app/types/services.ts +27 -0
- package/app/types/slack.ts +57 -0
- package/app/types/tasks.ts +82 -0
- package/app/types/tracker.ts +18 -0
- package/app/utils/agentOutput.spec.ts +128 -0
- package/app/utils/agentOutput.ts +173 -0
- package/app/utils/catalog.spec.ts +112 -0
- package/app/utils/catalog.ts +455 -0
- package/app/utils/dnd.ts +29 -0
- package/app/utils/observability.ts +52 -0
- package/app/utils/pipelineRender.ts +151 -0
- package/nuxt.config.ts +55 -0
- package/package.json +45 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Requirements review window — the dedicated surface for the `requirements-review` gate
|
|
3
|
+
// step (opened via the universal result-view host). The human reacts to the reviewer's
|
|
4
|
+
// structured findings (answer the relevant, dismiss the irrelevant), then asks to
|
|
5
|
+
// incorporate. Incorporation + the re-review run ASYNCHRONOUSLY in the durable driver: the
|
|
6
|
+
// window closes and the user returns to the board, and is summoned back (a notification)
|
|
7
|
+
// only if the re-review raises new findings or hits the iteration cap. The incorporated
|
|
8
|
+
// document — not the original description + linked docs/tasks — is what every downstream
|
|
9
|
+
// agent step and the spec-writer consume.
|
|
10
|
+
import { parseOutputOutline } from '~/utils/agentOutput'
|
|
11
|
+
import StepRestartControl from '~/components/panels/StepRestartControl.vue'
|
|
12
|
+
import type {
|
|
13
|
+
RequirementReview,
|
|
14
|
+
RequirementReviewItem,
|
|
15
|
+
ReviewItemCategory,
|
|
16
|
+
ReviewItemSeverity,
|
|
17
|
+
ReviewItemStatus,
|
|
18
|
+
} from '~/types/requirements'
|
|
19
|
+
|
|
20
|
+
const board = useBoardStore()
|
|
21
|
+
const requirements = useRequirementsStore()
|
|
22
|
+
const toast = useToast()
|
|
23
|
+
|
|
24
|
+
// Draft replies, keyed by item id, so editing one item doesn't disturb others.
|
|
25
|
+
const drafts = ref<Record<string, string>>({})
|
|
26
|
+
// Freeform "do it differently" comment when redoing a merge the human was unhappy with.
|
|
27
|
+
const redoComment = ref('')
|
|
28
|
+
const showRedo = ref(false)
|
|
29
|
+
|
|
30
|
+
// The seam contract (open/blockId/close + Escape handling + load-on-open) lives in
|
|
31
|
+
// `useResultView`, so this window can't drift from the others. Declaring `onOpen` makes the
|
|
32
|
+
// review load on EVERY open regardless of navigation route: the host mounts this window
|
|
33
|
+
// fresh each open, so a non-immediate per-window watch used to leave it empty for whichever
|
|
34
|
+
// route (a pipeline step / "Review & approve") didn't warm the cache by selecting the block.
|
|
35
|
+
const { open, blockId, instanceId, stepIndex, close } = useResultView('requirements-review', {
|
|
36
|
+
onOpen: (id) => {
|
|
37
|
+
drafts.value = {}
|
|
38
|
+
redoComment.value = ''
|
|
39
|
+
showRedo.value = false
|
|
40
|
+
void requirements.load(id)
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
44
|
+
const review = computed<RequirementReview | null>(() =>
|
|
45
|
+
blockId.value ? requirements.reviewFor(blockId.value) : null,
|
|
46
|
+
)
|
|
47
|
+
const busy = computed(() => (blockId.value ? requirements.isReviewing(blockId.value) : false))
|
|
48
|
+
// True while the initial fetch of an existing review is in flight (opening the window),
|
|
49
|
+
// before the cache is populated — so we show a spinner instead of the empty state.
|
|
50
|
+
const loading = computed(() => (blockId.value ? requirements.isLoading(blockId.value) : false))
|
|
51
|
+
const reworking = computed(() =>
|
|
52
|
+
review.value ? requirements.isIncorporating(review.value.id) : false,
|
|
53
|
+
)
|
|
54
|
+
const acting = ref(false)
|
|
55
|
+
|
|
56
|
+
const SEVERITY_RANK: Record<ReviewItemSeverity, number> = { high: 0, medium: 1, low: 2 }
|
|
57
|
+
const sortedItems = computed<RequirementReviewItem[]>(() => {
|
|
58
|
+
if (!review.value) return []
|
|
59
|
+
return [...review.value.items].sort(
|
|
60
|
+
(a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity],
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const openCount = computed(() => (review.value ? requirements.openCount(review.value) : 0))
|
|
65
|
+
const answeredCount = computed(() => (review.value ? requirements.answeredCount(review.value) : 0))
|
|
66
|
+
const status = computed(() => review.value?.status ?? null)
|
|
67
|
+
const merged = computed(() => status.value === 'merged')
|
|
68
|
+
const exceeded = computed(() => status.value === 'exceeded')
|
|
69
|
+
const incorporated = computed(() => status.value === 'incorporated')
|
|
70
|
+
// The async cycle runs in the driver in two stages — folding the answers (`incorporating`)
|
|
71
|
+
// then re-reviewing the document (`reviewing`). The window normally closes the moment
|
|
72
|
+
// incorporation is requested; these states only show if it's later re-opened mid-cycle.
|
|
73
|
+
const incorporating = computed(() => status.value === 'incorporating')
|
|
74
|
+
const reReviewing = computed(() => status.value === 'reviewing')
|
|
75
|
+
const working = computed(() => incorporating.value || reReviewing.value)
|
|
76
|
+
// No edits while the requirements are settled or a cycle is running in the background.
|
|
77
|
+
const frozen = computed(() => incorporated.value || working.value)
|
|
78
|
+
const canIncorporate = computed(() => !!review.value && requirements.canIncorporate(review.value))
|
|
79
|
+
const canProceed = computed(() => !!review.value && requirements.canProceed(review.value))
|
|
80
|
+
const iteration = computed(() => review.value?.iteration ?? 1)
|
|
81
|
+
const maxIterations = computed(() => review.value?.maxIterations ?? 1)
|
|
82
|
+
|
|
83
|
+
// The incorporated document rendered as collapsible markdown (same reader the prose
|
|
84
|
+
// review window uses), shown once the companion has produced one.
|
|
85
|
+
const outline = computed(() =>
|
|
86
|
+
review.value?.incorporatedRequirements
|
|
87
|
+
? parseOutputOutline(review.value.incorporatedRequirements)
|
|
88
|
+
: null,
|
|
89
|
+
)
|
|
90
|
+
const collapsed = ref<Record<string, boolean>>({})
|
|
91
|
+
function toggle(id: string) {
|
|
92
|
+
collapsed.value = { ...collapsed.value, [id]: !collapsed.value[id] }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const SEVERITY_COLOR = {
|
|
96
|
+
high: 'error',
|
|
97
|
+
medium: 'warning',
|
|
98
|
+
low: 'neutral',
|
|
99
|
+
} as const satisfies Record<ReviewItemSeverity, string>
|
|
100
|
+
const CATEGORY_ICON: Record<ReviewItemCategory, string> = {
|
|
101
|
+
gap: 'i-lucide-puzzle',
|
|
102
|
+
clarification: 'i-lucide-help-circle',
|
|
103
|
+
assumption: 'i-lucide-lightbulb',
|
|
104
|
+
risk: 'i-lucide-shield-alert',
|
|
105
|
+
question: 'i-lucide-message-circle-question',
|
|
106
|
+
}
|
|
107
|
+
const STATUS_COLOR = {
|
|
108
|
+
open: 'warning',
|
|
109
|
+
answered: 'info',
|
|
110
|
+
resolved: 'success',
|
|
111
|
+
dismissed: 'neutral',
|
|
112
|
+
} as const satisfies Record<ReviewItemStatus, string>
|
|
113
|
+
|
|
114
|
+
function notifyError(title: string, e: unknown) {
|
|
115
|
+
toast.add({
|
|
116
|
+
title,
|
|
117
|
+
description: e instanceof Error ? e.message : String(e),
|
|
118
|
+
icon: 'i-lucide-triangle-alert',
|
|
119
|
+
color: 'error',
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function submitReply(item: RequirementReviewItem) {
|
|
124
|
+
if (!review.value) return
|
|
125
|
+
const text = (drafts.value[item.id] ?? '').trim()
|
|
126
|
+
if (!text) return
|
|
127
|
+
try {
|
|
128
|
+
await requirements.reply(review.value, item.id, text)
|
|
129
|
+
drafts.value = { ...drafts.value, [item.id]: '' }
|
|
130
|
+
} catch (e) {
|
|
131
|
+
notifyError('Could not save the answer', e)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function setStatus(item: RequirementReviewItem, itemStatus: ReviewItemStatus) {
|
|
136
|
+
if (!review.value) return
|
|
137
|
+
try {
|
|
138
|
+
await requirements.setItemStatus(review.value, item.id, itemStatus)
|
|
139
|
+
} catch (e) {
|
|
140
|
+
notifyError('Could not update the finding', e)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function incorporate(feedback?: string) {
|
|
145
|
+
if (!review.value || !blockId.value) return
|
|
146
|
+
try {
|
|
147
|
+
await requirements.incorporate(review.value, feedback)
|
|
148
|
+
} catch (e) {
|
|
149
|
+
notifyError('Could not incorporate the answers', e)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
redoComment.value = ''
|
|
153
|
+
showRedo.value = false
|
|
154
|
+
// The fold + re-review now run in the durable driver. Hand the user back to the board;
|
|
155
|
+
// a notification calls them back only if the re-review needs more input.
|
|
156
|
+
toast.add({
|
|
157
|
+
title: 'Incorporating your answers in the background',
|
|
158
|
+
description: "You're back on the board — we'll notify you only if more input is needed.",
|
|
159
|
+
icon: 'i-lucide-wand-sparkles',
|
|
160
|
+
})
|
|
161
|
+
close()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function reReview() {
|
|
165
|
+
if (!blockId.value) return
|
|
166
|
+
try {
|
|
167
|
+
const updated = await requirements.reReview(blockId.value)
|
|
168
|
+
toast.add({
|
|
169
|
+
title:
|
|
170
|
+
updated.status === 'incorporated'
|
|
171
|
+
? 'Reviewer is satisfied — continuing the pipeline'
|
|
172
|
+
: updated.status === 'exceeded'
|
|
173
|
+
? 'Iteration limit reached — choose how to proceed'
|
|
174
|
+
: `${requirements.openCount(updated)} new finding(s) to react to`,
|
|
175
|
+
icon: 'i-lucide-sparkles',
|
|
176
|
+
})
|
|
177
|
+
} catch (e) {
|
|
178
|
+
notifyError('Could not re-review the requirements', e)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function proceed() {
|
|
183
|
+
if (!blockId.value) return
|
|
184
|
+
acting.value = true
|
|
185
|
+
try {
|
|
186
|
+
await requirements.proceed(blockId.value)
|
|
187
|
+
toast.add({ title: 'Proceeding to the next phase', icon: 'i-lucide-arrow-right' })
|
|
188
|
+
} catch (e) {
|
|
189
|
+
notifyError('Could not proceed', e)
|
|
190
|
+
} finally {
|
|
191
|
+
acting.value = false
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset') {
|
|
196
|
+
if (!blockId.value) return
|
|
197
|
+
acting.value = true
|
|
198
|
+
try {
|
|
199
|
+
await requirements.resolveExceeded(blockId.value, choice)
|
|
200
|
+
if (choice === 'stop-reset') {
|
|
201
|
+
toast.add({ title: 'Task reset — edit the requirements and resubmit', icon: 'i-lucide-undo' })
|
|
202
|
+
close()
|
|
203
|
+
} else if (choice === 'proceed') {
|
|
204
|
+
toast.add({ title: 'Proceeding to the next phase', icon: 'i-lucide-arrow-right' })
|
|
205
|
+
} else {
|
|
206
|
+
toast.add({ title: 'One more review round granted', icon: 'i-lucide-rotate-cw' })
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
notifyError('Could not resolve the review', e)
|
|
210
|
+
} finally {
|
|
211
|
+
acting.value = false
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
</script>
|
|
215
|
+
|
|
216
|
+
<template>
|
|
217
|
+
<Teleport to="body">
|
|
218
|
+
<div
|
|
219
|
+
v-if="open"
|
|
220
|
+
class="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/70 p-4 backdrop-blur-sm"
|
|
221
|
+
@click.self="close"
|
|
222
|
+
>
|
|
223
|
+
<div
|
|
224
|
+
class="flex h-[90vh] w-full max-w-5xl flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl"
|
|
225
|
+
>
|
|
226
|
+
<!-- header -->
|
|
227
|
+
<header class="flex items-center gap-3 border-b border-slate-800 px-6 py-4">
|
|
228
|
+
<div
|
|
229
|
+
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-indigo-500/15"
|
|
230
|
+
>
|
|
231
|
+
<UIcon name="i-lucide-clipboard-check" class="h-5 w-5 text-indigo-300" />
|
|
232
|
+
</div>
|
|
233
|
+
<div class="min-w-0">
|
|
234
|
+
<h1 class="truncate text-base font-semibold text-white">Requirements review</h1>
|
|
235
|
+
<p v-if="block" class="truncate text-xs text-slate-500">{{ block.title }}</p>
|
|
236
|
+
</div>
|
|
237
|
+
<div class="ml-auto flex items-center gap-1.5">
|
|
238
|
+
<UBadge v-if="review" color="neutral" variant="subtle" size="sm">
|
|
239
|
+
Iteration {{ iteration }} / {{ maxIterations }}
|
|
240
|
+
</UBadge>
|
|
241
|
+
<StepRestartControl
|
|
242
|
+
:instance-id="instanceId"
|
|
243
|
+
:step-index="stepIndex"
|
|
244
|
+
@restarted="close"
|
|
245
|
+
/>
|
|
246
|
+
<UButton icon="i-lucide-x" color="neutral" variant="ghost" size="sm" @click="close" />
|
|
247
|
+
</div>
|
|
248
|
+
</header>
|
|
249
|
+
|
|
250
|
+
<div class="flex min-h-0 flex-1">
|
|
251
|
+
<!-- main column -->
|
|
252
|
+
<div class="min-w-0 flex-1 overflow-y-auto px-6 py-5">
|
|
253
|
+
<p class="mb-4 text-sm text-slate-400">
|
|
254
|
+
An AI reviewer inspected this {{ block?.level ?? 'item' }}’s collected requirements —
|
|
255
|
+
its description plus any linked PRDs and tracker issues — and raised the findings
|
|
256
|
+
below. <span class="text-slate-300">Answer</span> the relevant ones and
|
|
257
|
+
<span class="text-slate-300">dismiss</span> the irrelevant, then incorporate them; the
|
|
258
|
+
reviewer re-reviews until the requirements are clear.
|
|
259
|
+
</p>
|
|
260
|
+
|
|
261
|
+
<!-- empty state — the reviewer runs automatically as the first pipeline
|
|
262
|
+
gate step, so there's nothing to do here until then -->
|
|
263
|
+
<div
|
|
264
|
+
v-if="!review && !busy && !loading"
|
|
265
|
+
class="rounded-lg border border-dashed border-slate-700 p-8 text-center text-sm text-slate-500"
|
|
266
|
+
>
|
|
267
|
+
No review yet. The reviewer runs automatically as the first step when this task's
|
|
268
|
+
pipeline starts.
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<!-- working state (initial fetch on open, or a reviewer pass running) -->
|
|
272
|
+
<div
|
|
273
|
+
v-else-if="(busy || loading) && !review"
|
|
274
|
+
class="flex items-center justify-center gap-2 p-8 text-sm text-slate-400"
|
|
275
|
+
>
|
|
276
|
+
<UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
|
|
277
|
+
{{ loading && !busy ? 'Loading the review…' : 'Reviewing the requirements…' }}
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
<template v-else-if="review">
|
|
281
|
+
<!-- converged: reviewer satisfied -->
|
|
282
|
+
<div
|
|
283
|
+
v-if="incorporated"
|
|
284
|
+
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"
|
|
285
|
+
>
|
|
286
|
+
<UIcon name="i-lucide-circle-check" class="h-5 w-5 shrink-0" />
|
|
287
|
+
The requirements are settled. The document below is what every downstream agent step
|
|
288
|
+
uses.
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<!-- iteration cap hit -->
|
|
292
|
+
<IterationCapPrompt
|
|
293
|
+
v-else-if="exceeded"
|
|
294
|
+
class="mb-4"
|
|
295
|
+
:heading="`Reached the ${maxIterations}-iteration limit with findings still open.`"
|
|
296
|
+
detail="Do one more review round, proceed to the next phase with the last incorporated requirements anyway, or stop and reset the task so you can rework the requirements and resubmit."
|
|
297
|
+
:loading="acting"
|
|
298
|
+
@resolve="resolveExceeded"
|
|
299
|
+
/>
|
|
300
|
+
|
|
301
|
+
<!-- working: the async cycle is running in the driver. Two distinct stages so
|
|
302
|
+
the human can see which of the two LLM calls is currently in progress. -->
|
|
303
|
+
<div
|
|
304
|
+
v-else-if="working"
|
|
305
|
+
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"
|
|
306
|
+
>
|
|
307
|
+
<UIcon name="i-lucide-loader-circle" class="h-5 w-5 shrink-0 animate-spin" />
|
|
308
|
+
<span v-if="incorporating">
|
|
309
|
+
Incorporating your answers into a requirements document… You can close this —
|
|
310
|
+
we’ll notify you only if more input is needed.
|
|
311
|
+
</span>
|
|
312
|
+
<span v-else>
|
|
313
|
+
Re-reviewing the updated requirements… You can close this — we’ll notify you only
|
|
314
|
+
if more input is needed.
|
|
315
|
+
</span>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
<!-- findings to react to -->
|
|
319
|
+
<div v-if="review.items.length" class="flex flex-col gap-3">
|
|
320
|
+
<div
|
|
321
|
+
v-for="item in sortedItems"
|
|
322
|
+
:key="item.id"
|
|
323
|
+
class="rounded-lg border border-slate-800 bg-slate-900/60 p-3"
|
|
324
|
+
:class="{ 'opacity-60': item.status === 'dismissed' }"
|
|
325
|
+
>
|
|
326
|
+
<div class="flex items-start gap-2">
|
|
327
|
+
<UIcon
|
|
328
|
+
:name="CATEGORY_ICON[item.category]"
|
|
329
|
+
class="mt-0.5 h-4 w-4 shrink-0 text-slate-400"
|
|
330
|
+
/>
|
|
331
|
+
<div class="min-w-0 flex-1">
|
|
332
|
+
<div class="flex flex-wrap items-center gap-1.5">
|
|
333
|
+
<span class="text-sm font-medium text-white">{{ item.title }}</span>
|
|
334
|
+
<UBadge size="xs" variant="subtle" :color="SEVERITY_COLOR[item.severity]">
|
|
335
|
+
{{ item.severity }}
|
|
336
|
+
</UBadge>
|
|
337
|
+
<UBadge size="xs" variant="outline" color="neutral">
|
|
338
|
+
{{ item.category }}
|
|
339
|
+
</UBadge>
|
|
340
|
+
<UBadge
|
|
341
|
+
size="xs"
|
|
342
|
+
variant="soft"
|
|
343
|
+
:color="STATUS_COLOR[item.status]"
|
|
344
|
+
class="ml-auto"
|
|
345
|
+
>
|
|
346
|
+
{{ item.status }}
|
|
347
|
+
</UBadge>
|
|
348
|
+
</div>
|
|
349
|
+
<p class="mt-1 whitespace-pre-line text-sm text-slate-400">
|
|
350
|
+
{{ item.detail }}
|
|
351
|
+
</p>
|
|
352
|
+
|
|
353
|
+
<!-- recorded answer -->
|
|
354
|
+
<div
|
|
355
|
+
v-if="item.reply"
|
|
356
|
+
class="mt-2 rounded-md border-l-2 border-slate-700 bg-slate-950/40 px-3 py-1.5 text-sm text-slate-300"
|
|
357
|
+
>
|
|
358
|
+
<span class="text-[10px] uppercase tracking-wide text-slate-500">
|
|
359
|
+
Answer
|
|
360
|
+
</span>
|
|
361
|
+
<p class="whitespace-pre-line">{{ item.reply }}</p>
|
|
362
|
+
</div>
|
|
363
|
+
|
|
364
|
+
<!-- react: answer (relevant) or dismiss (irrelevant). Disabled once the
|
|
365
|
+
requirements are settled / awaiting a higher-level decision. -->
|
|
366
|
+
<template v-if="item.status === 'open' || item.status === 'answered'">
|
|
367
|
+
<UTextarea
|
|
368
|
+
v-model="drafts[item.id]"
|
|
369
|
+
:rows="2"
|
|
370
|
+
autoresize
|
|
371
|
+
size="sm"
|
|
372
|
+
class="mt-2 w-full"
|
|
373
|
+
:placeholder="item.reply ? 'Refine your answer…' : 'Answer this finding…'"
|
|
374
|
+
:disabled="frozen"
|
|
375
|
+
/>
|
|
376
|
+
<div class="mt-2 flex flex-wrap items-center gap-2">
|
|
377
|
+
<UButton
|
|
378
|
+
color="primary"
|
|
379
|
+
variant="soft"
|
|
380
|
+
size="xs"
|
|
381
|
+
icon="i-lucide-corner-down-left"
|
|
382
|
+
:disabled="!(drafts[item.id] ?? '').trim() || frozen"
|
|
383
|
+
@click="submitReply(item)"
|
|
384
|
+
>
|
|
385
|
+
Save answer
|
|
386
|
+
</UButton>
|
|
387
|
+
<UButton
|
|
388
|
+
color="neutral"
|
|
389
|
+
variant="ghost"
|
|
390
|
+
size="xs"
|
|
391
|
+
icon="i-lucide-x"
|
|
392
|
+
:disabled="frozen"
|
|
393
|
+
@click="setStatus(item, 'dismissed')"
|
|
394
|
+
>
|
|
395
|
+
Dismiss as irrelevant
|
|
396
|
+
</UButton>
|
|
397
|
+
</div>
|
|
398
|
+
</template>
|
|
399
|
+
|
|
400
|
+
<!-- reopen a dismissed finding -->
|
|
401
|
+
<div v-else-if="item.status === 'dismissed'" class="mt-2">
|
|
402
|
+
<UButton
|
|
403
|
+
color="neutral"
|
|
404
|
+
variant="ghost"
|
|
405
|
+
size="xs"
|
|
406
|
+
icon="i-lucide-rotate-ccw"
|
|
407
|
+
:disabled="frozen"
|
|
408
|
+
@click="setStatus(item, 'open')"
|
|
409
|
+
>
|
|
410
|
+
Reopen
|
|
411
|
+
</UButton>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
|
|
418
|
+
<!-- incorporated document: the standard-format requirements -->
|
|
419
|
+
<section v-if="outline" class="mt-6 border-t border-slate-800 pt-5">
|
|
420
|
+
<div class="mb-3 flex items-center gap-1.5 text-[11px] text-emerald-400">
|
|
421
|
+
<UIcon name="i-lucide-file-check-2" class="h-3.5 w-3.5" />
|
|
422
|
+
<span class="font-semibold uppercase tracking-wide">
|
|
423
|
+
{{ incorporated ? 'Final requirements' : 'Incorporated requirements (draft)' }}
|
|
424
|
+
</span>
|
|
425
|
+
</div>
|
|
426
|
+
<div v-for="s in outline.sections" :key="s.id" class="mb-2">
|
|
427
|
+
<button
|
|
428
|
+
v-if="s.title"
|
|
429
|
+
class="group flex w-full items-center gap-2 text-left"
|
|
430
|
+
@click="toggle(s.id)"
|
|
431
|
+
>
|
|
432
|
+
<UIcon
|
|
433
|
+
name="i-lucide-chevron-right"
|
|
434
|
+
class="h-3.5 w-3.5 shrink-0 text-slate-500 transition-transform"
|
|
435
|
+
:class="collapsed[s.id] ? '' : 'rotate-90'"
|
|
436
|
+
/>
|
|
437
|
+
<span
|
|
438
|
+
class="font-semibold text-white"
|
|
439
|
+
:class="s.depth <= 1 ? 'text-base' : s.depth === 2 ? 'text-sm' : 'text-xs'"
|
|
440
|
+
v-html="s.titleHtml"
|
|
441
|
+
/>
|
|
442
|
+
</button>
|
|
443
|
+
<div
|
|
444
|
+
v-show="!s.title || !collapsed[s.id]"
|
|
445
|
+
class="reader-prose mt-1 pl-5.5 text-[13px] leading-relaxed text-slate-300"
|
|
446
|
+
v-html="s.bodyHtml"
|
|
447
|
+
/>
|
|
448
|
+
</div>
|
|
449
|
+
</section>
|
|
450
|
+
</template>
|
|
451
|
+
</div>
|
|
452
|
+
|
|
453
|
+
<!-- right action rail -->
|
|
454
|
+
<aside class="hidden w-72 shrink-0 flex-col border-l border-slate-800 lg:flex">
|
|
455
|
+
<div class="flex flex-col gap-4 px-4 py-5">
|
|
456
|
+
<div v-if="review" class="space-y-2 text-xs text-slate-400">
|
|
457
|
+
<div class="flex items-center justify-between">
|
|
458
|
+
<span>Findings</span>
|
|
459
|
+
<span class="text-slate-300">{{ review.items.length }}</span>
|
|
460
|
+
</div>
|
|
461
|
+
<div class="flex items-center justify-between">
|
|
462
|
+
<span>Open</span>
|
|
463
|
+
<span class="text-slate-300">{{ openCount }}</span>
|
|
464
|
+
</div>
|
|
465
|
+
<div class="flex items-center justify-between">
|
|
466
|
+
<span>Answered</span>
|
|
467
|
+
<span class="text-slate-300">{{ answeredCount }}</span>
|
|
468
|
+
</div>
|
|
469
|
+
<div v-if="review.model" class="flex items-center justify-between">
|
|
470
|
+
<span>Model</span>
|
|
471
|
+
<span class="truncate pl-2 text-slate-500">{{ review.model }}</span>
|
|
472
|
+
</div>
|
|
473
|
+
</div>
|
|
474
|
+
|
|
475
|
+
<!-- action: ready (answer → incorporate / proceed) -->
|
|
476
|
+
<div
|
|
477
|
+
v-if="review && status === 'ready'"
|
|
478
|
+
class="space-y-2 border-t border-slate-800 pt-4"
|
|
479
|
+
>
|
|
480
|
+
<UButton
|
|
481
|
+
v-if="canProceed"
|
|
482
|
+
color="primary"
|
|
483
|
+
size="sm"
|
|
484
|
+
block
|
|
485
|
+
icon="i-lucide-arrow-right"
|
|
486
|
+
:loading="acting"
|
|
487
|
+
@click="proceed"
|
|
488
|
+
>
|
|
489
|
+
Proceed (nothing to incorporate)
|
|
490
|
+
</UButton>
|
|
491
|
+
<UButton
|
|
492
|
+
v-else
|
|
493
|
+
color="primary"
|
|
494
|
+
size="sm"
|
|
495
|
+
block
|
|
496
|
+
icon="i-lucide-wand-sparkles"
|
|
497
|
+
:loading="reworking"
|
|
498
|
+
:disabled="!canIncorporate"
|
|
499
|
+
@click="incorporate()"
|
|
500
|
+
>
|
|
501
|
+
Incorporate answers
|
|
502
|
+
</UButton>
|
|
503
|
+
<p class="text-[11px] leading-relaxed text-slate-500">
|
|
504
|
+
<template v-if="canProceed">
|
|
505
|
+
Every finding is dismissed — proceed to the next phase without reworking.
|
|
506
|
+
</template>
|
|
507
|
+
<template v-else-if="canIncorporate">
|
|
508
|
+
Folds your answers into one standard-format document, then re-reviews it
|
|
509
|
+
automatically.
|
|
510
|
+
</template>
|
|
511
|
+
<template v-else> Answer or dismiss every finding to continue. </template>
|
|
512
|
+
</p>
|
|
513
|
+
</div>
|
|
514
|
+
|
|
515
|
+
<!-- action: merged (inspect → re-review / redo) -->
|
|
516
|
+
<div v-if="review && merged" class="space-y-2 border-t border-slate-800 pt-4">
|
|
517
|
+
<UButton
|
|
518
|
+
color="primary"
|
|
519
|
+
size="sm"
|
|
520
|
+
block
|
|
521
|
+
icon="i-lucide-sparkles"
|
|
522
|
+
:loading="busy"
|
|
523
|
+
@click="reReview"
|
|
524
|
+
>
|
|
525
|
+
{{ busy ? 'Re-reviewing…' : 'Looks good — re-review' }}
|
|
526
|
+
</UButton>
|
|
527
|
+
<UButton
|
|
528
|
+
color="neutral"
|
|
529
|
+
variant="soft"
|
|
530
|
+
size="sm"
|
|
531
|
+
block
|
|
532
|
+
icon="i-lucide-pencil"
|
|
533
|
+
@click="showRedo = !showRedo"
|
|
534
|
+
>
|
|
535
|
+
Redo incorporation
|
|
536
|
+
</UButton>
|
|
537
|
+
<div v-if="showRedo" class="space-y-2">
|
|
538
|
+
<UTextarea
|
|
539
|
+
v-model="redoComment"
|
|
540
|
+
:rows="3"
|
|
541
|
+
autoresize
|
|
542
|
+
size="sm"
|
|
543
|
+
class="w-full"
|
|
544
|
+
placeholder="What should the merge do differently?"
|
|
545
|
+
/>
|
|
546
|
+
<UButton
|
|
547
|
+
color="primary"
|
|
548
|
+
variant="soft"
|
|
549
|
+
size="xs"
|
|
550
|
+
block
|
|
551
|
+
icon="i-lucide-wand-sparkles"
|
|
552
|
+
:loading="reworking"
|
|
553
|
+
:disabled="!redoComment.trim()"
|
|
554
|
+
@click="incorporate(redoComment.trim())"
|
|
555
|
+
>
|
|
556
|
+
Redo with this direction
|
|
557
|
+
</UButton>
|
|
558
|
+
</div>
|
|
559
|
+
<p class="text-[11px] leading-relaxed text-slate-500">
|
|
560
|
+
Re-review runs the reviewer against this document. If you’re unhappy with how it
|
|
561
|
+
was merged, redo it with a comment instead.
|
|
562
|
+
</p>
|
|
563
|
+
</div>
|
|
564
|
+
|
|
565
|
+
<div
|
|
566
|
+
v-if="review && incorporated"
|
|
567
|
+
class="border-t border-slate-800 pt-4 text-[11px] leading-relaxed text-slate-500"
|
|
568
|
+
>
|
|
569
|
+
Requirements settled — the pipeline is continuing with the document on the left.
|
|
570
|
+
</div>
|
|
571
|
+
</div>
|
|
572
|
+
</aside>
|
|
573
|
+
</div>
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
</Teleport>
|
|
577
|
+
</template>
|
|
578
|
+
|
|
579
|
+
<style scoped>
|
|
580
|
+
.pl-5\.5 {
|
|
581
|
+
padding-left: 1.375rem;
|
|
582
|
+
}
|
|
583
|
+
/* Minimal CommonMark styling for the incorporated requirements reader (mirrors the
|
|
584
|
+
prose review window's reader-prose). */
|
|
585
|
+
.reader-prose :deep(p) {
|
|
586
|
+
margin: 0.4rem 0;
|
|
587
|
+
}
|
|
588
|
+
.reader-prose :deep(ul),
|
|
589
|
+
.reader-prose :deep(ol) {
|
|
590
|
+
margin: 0.4rem 0;
|
|
591
|
+
padding-left: 1.25rem;
|
|
592
|
+
list-style: revert;
|
|
593
|
+
}
|
|
594
|
+
.reader-prose :deep(li) {
|
|
595
|
+
margin: 0.2rem 0;
|
|
596
|
+
}
|
|
597
|
+
.reader-prose :deep(strong) {
|
|
598
|
+
color: rgb(226 232 240);
|
|
599
|
+
font-weight: 600;
|
|
600
|
+
}
|
|
601
|
+
.reader-prose :deep(code) {
|
|
602
|
+
border-radius: 0.25rem;
|
|
603
|
+
background: rgb(2 6 23 / 0.6);
|
|
604
|
+
padding: 0.05rem 0.3rem;
|
|
605
|
+
font-size: 0.85em;
|
|
606
|
+
}
|
|
607
|
+
.reader-prose :deep(pre) {
|
|
608
|
+
margin: 0.5rem 0;
|
|
609
|
+
overflow-x: auto;
|
|
610
|
+
border-radius: 0.5rem;
|
|
611
|
+
background: rgb(2 6 23 / 0.6);
|
|
612
|
+
padding: 0.75rem;
|
|
613
|
+
}
|
|
614
|
+
.reader-prose :deep(blockquote) {
|
|
615
|
+
margin: 0.5rem 0;
|
|
616
|
+
border-left: 2px solid rgb(51 65 85);
|
|
617
|
+
padding-left: 0.75rem;
|
|
618
|
+
color: rgb(148 163 184);
|
|
619
|
+
}
|
|
620
|
+
</style>
|