@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.
Files changed (189) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/app/app.config.ts +8 -0
  4. package/app/app.vue +11 -0
  5. package/app/assets/css/main.css +100 -0
  6. package/app/components/auth/AuthGate.vue +24 -0
  7. package/app/components/auth/LoginScreen.vue +143 -0
  8. package/app/components/auth/UserMenu.vue +39 -0
  9. package/app/components/board/AddTaskModal.vue +444 -0
  10. package/app/components/board/AgentFailureCard.vue +97 -0
  11. package/app/components/board/AgentStopButton.vue +61 -0
  12. package/app/components/board/BoardCanvas.vue +183 -0
  13. package/app/components/board/ContextPicker.vue +367 -0
  14. package/app/components/board/RecurringPipelineModal.vue +219 -0
  15. package/app/components/board/TaskDependencyEdges.vue +132 -0
  16. package/app/components/board/nodes/AgentChip.vue +59 -0
  17. package/app/components/board/nodes/BlockNode.vue +433 -0
  18. package/app/components/board/nodes/DecisionBadge.vue +27 -0
  19. package/app/components/board/nodes/DraggableTask.vue +48 -0
  20. package/app/components/board/nodes/ModuleFrame.vue +97 -0
  21. package/app/components/board/nodes/TaskCard.vue +359 -0
  22. package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
  23. package/app/components/bootstrap/BootstrapModal.vue +665 -0
  24. package/app/components/clarity/ClarityReviewWindow.vue +611 -0
  25. package/app/components/consensus/ConsensusSessionWindow.vue +210 -0
  26. package/app/components/documents/DocumentImportModal.vue +161 -0
  27. package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
  28. package/app/components/documents/SpawnPreviewModal.vue +161 -0
  29. package/app/components/documents/TaskContextDocs.vue +83 -0
  30. package/app/components/focus/BlockFocusView.vue +171 -0
  31. package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
  32. package/app/components/gates/GateResultView.vue +282 -0
  33. package/app/components/github/AddServiceFromRepoModal.vue +354 -0
  34. package/app/components/github/GitHubConnect.vue +183 -0
  35. package/app/components/github/GitHubOnboarding.vue +45 -0
  36. package/app/components/github/GitHubPanel.vue +584 -0
  37. package/app/components/github/RepoTreeBrowser.vue +171 -0
  38. package/app/components/layout/AccountTeamSettings.vue +237 -0
  39. package/app/components/layout/BoardSwitcher.vue +280 -0
  40. package/app/components/layout/BoardToolbar.vue +156 -0
  41. package/app/components/layout/CommandBar.vue +336 -0
  42. package/app/components/layout/GitHubPatBanner.vue +73 -0
  43. package/app/components/layout/NotificationsInbox.vue +175 -0
  44. package/app/components/layout/SideBar.vue +314 -0
  45. package/app/components/layout/SpendWarningBanner.vue +107 -0
  46. package/app/components/observability/StepMetricsBar.vue +102 -0
  47. package/app/components/palettes/AgentPalette.vue +86 -0
  48. package/app/components/panels/AgentStepDetail.vue +737 -0
  49. package/app/components/panels/DecisionModal.vue +71 -0
  50. package/app/components/panels/InspectorPanel.vue +465 -0
  51. package/app/components/panels/ObservabilityPanel.vue +351 -0
  52. package/app/components/panels/StepMetadataCard.vue +253 -0
  53. package/app/components/panels/StepRestartControl.vue +90 -0
  54. package/app/components/panels/StepResultViewHost.vue +40 -0
  55. package/app/components/panels/StepTestReport.vue +84 -0
  56. package/app/components/panels/inspector/ContainerSummary.vue +74 -0
  57. package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
  58. package/app/components/panels/inspector/ServiceFragments.vue +82 -0
  59. package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
  60. package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
  61. package/app/components/panels/inspector/TaskDependencies.vue +70 -0
  62. package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
  63. package/app/components/panels/inspector/TaskExecution.vue +364 -0
  64. package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
  65. package/app/components/panels/inspector/TaskStructure.vue +96 -0
  66. package/app/components/pipeline/AgentKindIcon.vue +30 -0
  67. package/app/components/pipeline/IterationCapPrompt.vue +70 -0
  68. package/app/components/pipeline/PipelineBuilder.vue +817 -0
  69. package/app/components/pipeline/PipelineProgress.vue +484 -0
  70. package/app/components/providers/ApiKeysSection.vue +273 -0
  71. package/app/components/providers/PersonalCredentialModal.vue +128 -0
  72. package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
  73. package/app/components/providers/VendorCredentialsModal.vue +197 -0
  74. package/app/components/recurring/RecurrenceEditor.vue +124 -0
  75. package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
  76. package/app/components/settings/DatadogPanel.vue +213 -0
  77. package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
  78. package/app/components/settings/MergeThresholdsPanel.vue +378 -0
  79. package/app/components/settings/ModelDefaultsPanel.vue +250 -0
  80. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
  81. package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
  82. package/app/components/slack/SlackPanel.vue +299 -0
  83. package/app/components/tasks/TaskContextIssues.vue +88 -0
  84. package/app/components/tasks/TaskImportModal.vue +207 -0
  85. package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
  86. package/app/components/testing/TestReportWindow.vue +404 -0
  87. package/app/composables/api/accounts.ts +81 -0
  88. package/app/composables/api/auth.ts +45 -0
  89. package/app/composables/api/board.ts +101 -0
  90. package/app/composables/api/bootstrap.ts +62 -0
  91. package/app/composables/api/context.ts +25 -0
  92. package/app/composables/api/documents.ts +74 -0
  93. package/app/composables/api/execution.ts +127 -0
  94. package/app/composables/api/fragments.ts +71 -0
  95. package/app/composables/api/github.ts +131 -0
  96. package/app/composables/api/models.ts +127 -0
  97. package/app/composables/api/notifications.ts +23 -0
  98. package/app/composables/api/presets.ts +29 -0
  99. package/app/composables/api/recurring.ts +68 -0
  100. package/app/composables/api/releaseHealth.ts +43 -0
  101. package/app/composables/api/reviews.ts +146 -0
  102. package/app/composables/api/slack.ts +54 -0
  103. package/app/composables/api/tasks.ts +72 -0
  104. package/app/composables/api/workspaces.ts +36 -0
  105. package/app/composables/useApi.ts +89 -0
  106. package/app/composables/useBlockDrag.ts +90 -0
  107. package/app/composables/useBlockQueries.ts +154 -0
  108. package/app/composables/useBoardFlow.ts +11 -0
  109. package/app/composables/useContextLinking.ts +65 -0
  110. package/app/composables/useDepLabels.ts +26 -0
  111. package/app/composables/useFrameResize.ts +54 -0
  112. package/app/composables/useResultView.ts +48 -0
  113. package/app/composables/useReviewStage.ts +40 -0
  114. package/app/composables/useSemanticZoom.ts +31 -0
  115. package/app/composables/useStepApproval.ts +233 -0
  116. package/app/composables/useStepProse.ts +78 -0
  117. package/app/composables/useStepTimer.ts +63 -0
  118. package/app/composables/useTaskExpansion.ts +92 -0
  119. package/app/composables/useWorkspaceStream.ts +155 -0
  120. package/app/docs/architecture.md +31 -0
  121. package/app/pages/index.vue +141 -0
  122. package/app/stores/accounts.ts +152 -0
  123. package/app/stores/agentConfig.ts +35 -0
  124. package/app/stores/agentRuns.ts +122 -0
  125. package/app/stores/agents.ts +40 -0
  126. package/app/stores/apiKeys.ts +108 -0
  127. package/app/stores/auth.ts +166 -0
  128. package/app/stores/board.spec.ts +205 -0
  129. package/app/stores/board.ts +286 -0
  130. package/app/stores/bootstrap.ts +97 -0
  131. package/app/stores/clarity.ts +196 -0
  132. package/app/stores/consensus.ts +60 -0
  133. package/app/stores/documents.ts +176 -0
  134. package/app/stores/execution.ts +273 -0
  135. package/app/stores/fragmentLibrary.ts +147 -0
  136. package/app/stores/fragments.ts +40 -0
  137. package/app/stores/github.ts +305 -0
  138. package/app/stores/localModels.ts +51 -0
  139. package/app/stores/mergePresets.ts +58 -0
  140. package/app/stores/modelDefaults.ts +76 -0
  141. package/app/stores/models.ts +134 -0
  142. package/app/stores/notifications.ts +70 -0
  143. package/app/stores/observability.ts +144 -0
  144. package/app/stores/personalSubscriptions.ts +215 -0
  145. package/app/stores/pipelines.ts +327 -0
  146. package/app/stores/recurringPipelines.ts +112 -0
  147. package/app/stores/releaseHealth.ts +75 -0
  148. package/app/stores/requirements.spec.ts +94 -0
  149. package/app/stores/requirements.ts +208 -0
  150. package/app/stores/serviceFragmentDefaults.ts +29 -0
  151. package/app/stores/services.ts +87 -0
  152. package/app/stores/slack.ts +142 -0
  153. package/app/stores/taskExpansion.ts +36 -0
  154. package/app/stores/tasks.spec.ts +71 -0
  155. package/app/stores/tasks.ts +176 -0
  156. package/app/stores/tracker.ts +27 -0
  157. package/app/stores/ui.ts +434 -0
  158. package/app/stores/vendorCredentials.ts +54 -0
  159. package/app/stores/workspace.ts +215 -0
  160. package/app/stores/workspaceSettings.ts +36 -0
  161. package/app/types/accounts.ts +77 -0
  162. package/app/types/bootstrap.ts +83 -0
  163. package/app/types/clarity.ts +59 -0
  164. package/app/types/consensus.ts +91 -0
  165. package/app/types/documents.ts +104 -0
  166. package/app/types/domain.ts +495 -0
  167. package/app/types/execution.ts +383 -0
  168. package/app/types/fragments.ts +72 -0
  169. package/app/types/github.ts +173 -0
  170. package/app/types/localModels.ts +73 -0
  171. package/app/types/merge.ts +71 -0
  172. package/app/types/models.ts +157 -0
  173. package/app/types/notifications.ts +74 -0
  174. package/app/types/recurring.ts +69 -0
  175. package/app/types/releaseHealth.ts +31 -0
  176. package/app/types/requirements.ts +61 -0
  177. package/app/types/services.ts +27 -0
  178. package/app/types/slack.ts +57 -0
  179. package/app/types/tasks.ts +82 -0
  180. package/app/types/tracker.ts +18 -0
  181. package/app/utils/agentOutput.spec.ts +128 -0
  182. package/app/utils/agentOutput.ts +173 -0
  183. package/app/utils/catalog.spec.ts +112 -0
  184. package/app/utils/catalog.ts +455 -0
  185. package/app/utils/dnd.ts +29 -0
  186. package/app/utils/observability.ts +52 -0
  187. package/app/utils/pipelineRender.ts +151 -0
  188. package/nuxt.config.ts +55 -0
  189. package/package.json +45 -0
@@ -0,0 +1,737 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, watch, onMounted } from 'vue'
3
+ import { onKeyStroke } from '@vueuse/core'
4
+ import type { IterationCapChoice } from '~/types/execution'
5
+ import { agentKindMeta } from '~/utils/catalog'
6
+ import StepRestartControl from '~/components/panels/StepRestartControl.vue'
7
+ import StepMetadataCard from '~/components/panels/StepMetadataCard.vue'
8
+ import StepTestReport from '~/components/panels/StepTestReport.vue'
9
+ import { useStepTimer } from '~/composables/useStepTimer'
10
+ import { useStepProse } from '~/composables/useStepProse'
11
+ import { useStepApproval } from '~/composables/useStepApproval'
12
+
13
+ // Detail overlay for a single pipeline step. Opened by clicking an agent in the
14
+ // inspector list (TaskExecution) or the focus-view pipeline (PipelineProgress) via
15
+ // `ui.openStepDetail(instanceId, stepIndex)`. It resolves the step from the
16
+ // execution store so it stays live while open, and shows the step's metadata
17
+ // (state, timing, model, subtasks, fragments, decision/approval). When the agent
18
+ // produced prose (architect, researcher, reviewer, …) it also renders that output
19
+ // as markdown, split into collapsible sections with an auto-generated ToC sidebar.
20
+ // This component is orchestration only: the metadata card + the tester report are
21
+ // child components, and the live clock / prose reader / approval-review state machine
22
+ // live in the `useStepTimer` / `useStepProse` / `useStepApproval` composables.
23
+ const ui = useUiStore()
24
+ const execution = useExecutionStore()
25
+ const board = useBoardStore()
26
+ const models = useModelsStore()
27
+ const workspace = useWorkspaceStore()
28
+
29
+ onMounted(() => models.ensureLoaded(workspace.workspaceId ?? undefined))
30
+
31
+ const ctx = computed(() => ui.stepDetail)
32
+ const instance = computed(() => execution.getInstance(ctx.value?.instanceId))
33
+ const step = computed(() =>
34
+ ctx.value ? (instance.value?.steps[ctx.value.stepIndex] ?? null) : null,
35
+ )
36
+ const block = computed(() => (instance.value ? board.getBlock(instance.value.blockId) : undefined))
37
+ const agent = computed(() => (step.value ? agentKindMeta(step.value.agentKind) : null))
38
+ const open = computed(() => !!ctx.value && !!step.value)
39
+
40
+ const stepNumber = computed(() => (ctx.value ? ctx.value.stepIndex + 1 : 0))
41
+ const totalSteps = computed(() => instance.value?.steps.length ?? 0)
42
+
43
+ // Companion verdicts for a companion step: the full sequence of correction cycles.
44
+ const companionVerdicts = computed(() => step.value?.companion?.verdicts ?? [])
45
+ const latestVerdict = computed(() => companionVerdicts.value.at(-1) ?? null)
46
+ const pctOf = (n: number) => `${Math.round(n * 100)}%`
47
+
48
+ // A tester step's latest structured report (what was tested, outcomes, concerns,
49
+ // greenlight) + its loop phase/attempts, surfaced when this is a `tester` step.
50
+ const testReport = computed(() => step.value?.test?.lastReport ?? null)
51
+ const testPhase = computed(() => step.value?.test ?? null)
52
+
53
+ // A failed run is no longer executing: a step left mid-flight (state still
54
+ // `working`, no `finishedAt`) must stop looking live — no ticking clock, no
55
+ // "spinning up" phase, no spinner.
56
+ const runFailed = computed(() => instance.value?.status === 'failed')
57
+
58
+ // Live elapsed-time clock for the open step.
59
+ const { isRunning, durationLabel } = useStepTimer({
60
+ step: () => step.value,
61
+ runFailed: () => runFailed.value,
62
+ failureAt: () => instance.value?.failure?.occurredAt,
63
+ })
64
+
65
+ // The prose reader: heading outline, collapse state, scroll-spy + scroll refs.
66
+ const prose = useStepProse(() => step.value?.output ?? '')
67
+ const {
68
+ outline,
69
+ tocSections,
70
+ hasOutput,
71
+ collapsed,
72
+ activeId,
73
+ scrollEl,
74
+ sectionEls,
75
+ toggle,
76
+ setAll,
77
+ allCollapsed,
78
+ goTo,
79
+ onScroll,
80
+ } = prose
81
+
82
+ const approvalPending = computed(() => step.value?.approval?.status === 'pending')
83
+ const approvalId = computed(() => step.value?.approval?.id ?? null)
84
+ // A companion step parked at its automatic-rework cap: instead of the generic
85
+ // approve/request-changes/reject rail, it shows the shared iteration-cap prompt
86
+ // (one more round / proceed / stop & reset), resolved through its own endpoint.
87
+ const companionExceeded = computed(() => approvalPending.value && !!step.value?.companion?.exceeded)
88
+
89
+ function close() {
90
+ // Reset the approval-mode sub-states so reopening the same step is clean
91
+ // (the step-change watch only fires when the step key actually changes).
92
+ approval.resetForClose()
93
+ ui.closeStepDetail()
94
+ }
95
+
96
+ // The GitHub-style approval/review state machine for a pending gate step.
97
+ const approval = useStepApproval({
98
+ step: () => step.value,
99
+ scrollEl: () => scrollEl.value,
100
+ instanceId: () => ctx.value?.instanceId,
101
+ approvalId: () => approvalId.value,
102
+ approvalPending: () => approvalPending.value,
103
+ companionExceeded: () => companionExceeded.value,
104
+ close,
105
+ })
106
+ const {
107
+ reviewComments,
108
+ feedback,
109
+ submitting,
110
+ draftTarget,
111
+ draftBody,
112
+ editing,
113
+ draftProposal,
114
+ rejectArmed,
115
+ canRequestChanges,
116
+ onProseClick,
117
+ addDraftComment,
118
+ cancelDraft,
119
+ removeComment,
120
+ approve,
121
+ startEditing,
122
+ cancelEditing,
123
+ approveWithEdits,
124
+ requestChanges,
125
+ armReject,
126
+ disarmReject,
127
+ reject,
128
+ } = approval
129
+
130
+ const resolvingCap = ref(false)
131
+ async function resolveCompanionCap(choice: IterationCapChoice) {
132
+ if (!ctx.value || !approvalId.value || resolvingCap.value) return
133
+ resolvingCap.value = true
134
+ try {
135
+ await execution.resolveCompanionExceeded(ctx.value.instanceId, approvalId.value, choice)
136
+ close()
137
+ } finally {
138
+ resolvingCap.value = false
139
+ }
140
+ }
141
+
142
+ // Re-seed the reader (all sections expanded, scrolled to top) + reset the review
143
+ // drafts whenever a different step opens.
144
+ watch(
145
+ () => ctx.value && `${ctx.value.instanceId}:${ctx.value.stepIndex}`,
146
+ () => {
147
+ prose.reset()
148
+ approval.resetForStep()
149
+ },
150
+ )
151
+
152
+ onKeyStroke('Escape', () => {
153
+ if (open.value) close()
154
+ })
155
+
156
+ async function copyOutput() {
157
+ if (step.value?.output) await navigator.clipboard?.writeText(step.value.output)
158
+ }
159
+ </script>
160
+
161
+ <template>
162
+ <Teleport to="body">
163
+ <Transition name="reader-fade">
164
+ <div
165
+ v-if="open && step && agent"
166
+ class="fixed inset-0 z-50 flex bg-slate-950/96 backdrop-blur-sm"
167
+ role="dialog"
168
+ aria-modal="true"
169
+ >
170
+ <!-- ToC sidebar (only meaningful when there are prose headings) -->
171
+ <aside
172
+ v-if="outline.hasToc"
173
+ class="hidden w-72 shrink-0 flex-col border-r border-slate-800 bg-slate-900/60 md:flex"
174
+ >
175
+ <div class="border-b border-slate-800 px-4 py-3">
176
+ <div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
177
+ Contents
178
+ </div>
179
+ </div>
180
+ <nav class="flex-1 space-y-0.5 overflow-auto px-2 py-3">
181
+ <button
182
+ class="block w-full truncate rounded-md px-2 py-1 text-left text-[13px] transition"
183
+ :class="
184
+ activeId === 'step-details'
185
+ ? 'bg-indigo-500/15 font-medium text-indigo-200'
186
+ : 'text-slate-400 hover:bg-slate-800/60 hover:text-slate-200'
187
+ "
188
+ @click="goTo('step-details')"
189
+ >
190
+ Details
191
+ </button>
192
+ <button
193
+ v-for="s in tocSections"
194
+ :key="s.id"
195
+ class="block w-full truncate rounded-md px-2 py-1 text-left text-[13px] transition"
196
+ :class="
197
+ activeId === s.id
198
+ ? 'bg-indigo-500/15 font-medium text-indigo-200'
199
+ : 'text-slate-400 hover:bg-slate-800/60 hover:text-slate-200'
200
+ "
201
+ :style="{ paddingLeft: `${(s.depth - outline.minDepth) * 0.85 + 0.5}rem` }"
202
+ :title="s.title"
203
+ @click="goTo(s.id)"
204
+ >
205
+ {{ s.title }}
206
+ </button>
207
+ </nav>
208
+ </aside>
209
+
210
+ <!-- main column -->
211
+ <div class="flex min-w-0 flex-1 flex-col">
212
+ <header class="flex items-center gap-3 border-b border-slate-800 px-6 py-4">
213
+ <div
214
+ class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg"
215
+ :style="{ backgroundColor: agent.color + '22' }"
216
+ >
217
+ <UIcon :name="agent.icon" class="h-5 w-5" :style="{ color: agent.color }" />
218
+ </div>
219
+ <div class="min-w-0">
220
+ <h1 class="truncate text-base font-semibold text-white">{{ agent.label }}</h1>
221
+ <p v-if="block" class="truncate text-xs text-slate-500">{{ block.title }}</p>
222
+ </div>
223
+ <div class="ml-auto flex items-center gap-1.5">
224
+ <UBadge
225
+ v-if="approvalPending && !companionExceeded"
226
+ color="warning"
227
+ variant="subtle"
228
+ size="sm"
229
+ class="mr-1"
230
+ >
231
+ <UIcon name="i-lucide-shield-check" class="mr-1 h-3 w-3" />
232
+ Approval required
233
+ </UBadge>
234
+ <UBadge
235
+ v-else-if="companionExceeded"
236
+ color="warning"
237
+ variant="subtle"
238
+ size="sm"
239
+ class="mr-1"
240
+ >
241
+ <UIcon name="i-lucide-alert-triangle" class="mr-1 h-3 w-3" />
242
+ Decision required
243
+ </UBadge>
244
+ <UButton
245
+ v-if="outline.sections.length"
246
+ :icon="allCollapsed ? 'i-lucide-unfold-vertical' : 'i-lucide-fold-vertical'"
247
+ color="neutral"
248
+ variant="ghost"
249
+ size="sm"
250
+ :title="allCollapsed ? 'Expand all sections' : 'Collapse all sections'"
251
+ @click="setAll(!allCollapsed)"
252
+ />
253
+ <UButton
254
+ v-if="hasOutput"
255
+ icon="i-lucide-copy"
256
+ color="neutral"
257
+ variant="ghost"
258
+ size="sm"
259
+ title="Copy raw output"
260
+ @click="copyOutput"
261
+ />
262
+ <!-- Restart the pipeline from this step (shared two-click confirm; resetting
263
+ later steps is destructive). Keyed on the step so its armed state resets
264
+ when a different step opens within this overlay. -->
265
+ <StepRestartControl
266
+ :key="`${ctx?.instanceId}:${ctx?.stepIndex}`"
267
+ :instance-id="ctx?.instanceId ?? null"
268
+ :step-index="ctx?.stepIndex ?? null"
269
+ @restarted="close"
270
+ />
271
+ <UButton
272
+ icon="i-lucide-x"
273
+ color="neutral"
274
+ variant="ghost"
275
+ size="sm"
276
+ title="Close (Esc)"
277
+ @click="close"
278
+ />
279
+ </div>
280
+ </header>
281
+
282
+ <div ref="scrollEl" class="flex-1 overflow-auto px-6 py-6" @scroll="onScroll">
283
+ <div class="mx-auto max-w-3xl space-y-5">
284
+ <!-- metadata card (always shown) -->
285
+ <section
286
+ id="step-details"
287
+ :ref="(el) => (sectionEls['step-details'] = el as HTMLElement | null)"
288
+ class="scroll-mt-4 rounded-xl border border-slate-800 bg-slate-900/50 p-4"
289
+ >
290
+ <StepMetadataCard
291
+ :step="step"
292
+ :run-failed="runFailed"
293
+ :duration-label="durationLabel"
294
+ :is-running="isRunning"
295
+ :step-number="stepNumber"
296
+ :total-steps="totalSteps"
297
+ :instance-id="instance?.id"
298
+ :companion-verdicts="companionVerdicts"
299
+ :latest-verdict="latestVerdict"
300
+ />
301
+ </section>
302
+
303
+ <!-- companion rework budget spent: the shared iteration-cap decision
304
+ (one more round / proceed with the current output / stop & reset) -->
305
+ <IterationCapPrompt
306
+ v-if="companionExceeded"
307
+ :heading="`${agent.label} hit its ${step.companion?.maxAttempts}-attempt rework limit, still below the ${pctOf(latestVerdict?.threshold ?? 0)} bar.`"
308
+ detail="Do one more automatic rework round, proceed to the next step accepting the current output, or stop and reset the task so you can edit the inputs and resubmit."
309
+ :loading="resolvingCap"
310
+ @resolve="resolveCompanionCap"
311
+ />
312
+
313
+ <!-- tester report: what was tested, the per-area outcomes, the concerns
314
+ it raised and the greenlight verdict; plus the fixer-loop phase -->
315
+ <StepTestReport v-if="testReport" :report="testReport" :phase="testPhase" />
316
+
317
+ <!-- edit-then-approve: a direct editor over the raw conclusions; the
318
+ edits become the approved proposal that flows to the next step -->
319
+ <section v-if="editing" class="scroll-mt-4">
320
+ <div class="mb-2 flex items-center gap-1.5 text-[11px] text-amber-400">
321
+ <UIcon name="i-lucide-pencil" class="h-3.5 w-3.5" />
322
+ <span class="font-semibold uppercase tracking-wide">Editing the conclusions</span>
323
+ </div>
324
+ <UTextarea
325
+ v-model="draftProposal"
326
+ :rows="22"
327
+ autoresize
328
+ size="sm"
329
+ class="w-full"
330
+ :ui="{ base: 'font-mono text-[12px] leading-relaxed' }"
331
+ placeholder="Edit the agent's conclusions; your edits are saved when you approve…"
332
+ />
333
+ </section>
334
+
335
+ <!-- the agent's prose output, sectioned + collapsible -->
336
+ <template v-else-if="hasOutput">
337
+ <section
338
+ v-for="s in outline.sections"
339
+ :id="s.id"
340
+ :key="s.id"
341
+ :ref="(el) => (sectionEls[s.id] = el as HTMLElement | null)"
342
+ class="scroll-mt-4"
343
+ >
344
+ <button
345
+ v-if="s.depth > 0"
346
+ class="group flex w-full items-center gap-2 rounded-md py-1 text-left transition hover:text-white"
347
+ @click="toggle(s.id)"
348
+ >
349
+ <UIcon
350
+ name="i-lucide-chevron-right"
351
+ class="h-4 w-4 shrink-0 text-slate-500 transition-transform group-hover:text-slate-300"
352
+ :class="collapsed[s.id] ? '' : 'rotate-90'"
353
+ />
354
+ <span
355
+ class="font-semibold text-slate-100"
356
+ :class="s.depth <= 1 ? 'text-lg' : s.depth === 2 ? 'text-base' : 'text-sm'"
357
+ v-html="s.titleHtml"
358
+ />
359
+ </button>
360
+ <!-- eslint-disable-next-line vue/no-v-html -->
361
+ <div
362
+ v-show="!collapsed[s.id]"
363
+ class="reader-prose mt-1 text-[13px] leading-relaxed text-slate-300"
364
+ :class="[
365
+ s.depth > 0 ? 'pl-6' : '',
366
+ approvalPending && !editing && !companionExceeded ? 'review-mode' : '',
367
+ ]"
368
+ @click="onProseClick"
369
+ v-html="s.bodyHtml"
370
+ />
371
+ </section>
372
+ </template>
373
+
374
+ <p
375
+ v-else
376
+ class="rounded-lg border border-dashed border-slate-800 py-6 text-center text-sm text-slate-500"
377
+ >
378
+ This agent produced no prose output.
379
+ </p>
380
+ </div>
381
+ </div>
382
+ </div>
383
+
384
+ <!-- review rail (approval mode): per-block comments + overall feedback +
385
+ Approve / Request changes / Reject. A right-side rail on wide screens; a
386
+ bottom sheet (still reachable) below lg, so the gate is always actionable. -->
387
+ <aside
388
+ v-if="approvalPending && !companionExceeded"
389
+ class="absolute inset-x-0 bottom-0 z-10 flex max-h-[70vh] 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-l lg:border-t-0 lg:border-slate-800 lg:bg-slate-900/60 lg:shadow-none lg:backdrop-blur-none"
390
+ >
391
+ <div class="border-b border-slate-800 px-4 py-3">
392
+ <div class="text-[11px] font-semibold uppercase tracking-wide text-amber-400">
393
+ {{ editing ? 'Approve with corrections' : 'Review & approve' }}
394
+ </div>
395
+ <p class="mt-1 text-[12px] text-slate-400">
396
+ {{
397
+ editing
398
+ ? 'Edit the conclusions on the left; your edits are saved when you approve.'
399
+ : 'Click any block in the output to comment on it, or leave overall feedback below.'
400
+ }}
401
+ </p>
402
+ </div>
403
+
404
+ <div class="flex-1 space-y-3 overflow-auto px-4 py-3">
405
+ <p
406
+ v-if="editing"
407
+ class="rounded-lg border border-amber-500/30 bg-amber-500/5 p-3 text-[12px] leading-relaxed text-amber-200/90"
408
+ >
409
+ You're editing the conclusions directly. Manual edits can't be combined with per-block
410
+ comments — approve to save them, or cancel to return to review.
411
+ </p>
412
+ <template v-else>
413
+ <!-- composer for the block the human just clicked -->
414
+ <div
415
+ v-if="draftTarget"
416
+ class="rounded-lg border border-indigo-500/40 bg-indigo-500/5 p-3"
417
+ >
418
+ <div class="mb-1 text-[10px] uppercase tracking-wide text-indigo-300">
419
+ Commenting on
420
+ </div>
421
+ <pre
422
+ class="mb-2 max-h-24 overflow-auto whitespace-pre-wrap rounded bg-slate-950/60 p-2 text-[11px] text-slate-300"
423
+ >{{ draftTarget.quotedSource }}</pre
424
+ >
425
+ <UTextarea
426
+ v-model="draftBody"
427
+ :rows="3"
428
+ autoresize
429
+ size="sm"
430
+ class="w-full"
431
+ placeholder="Leave a comment on this block…"
432
+ />
433
+ <div class="mt-2 flex justify-end gap-2">
434
+ <UButton color="neutral" variant="ghost" size="xs" @click="cancelDraft">
435
+ Cancel
436
+ </UButton>
437
+ <UButton
438
+ color="primary"
439
+ size="xs"
440
+ :disabled="!draftBody.trim()"
441
+ @click="addDraftComment"
442
+ >
443
+ Add comment
444
+ </UButton>
445
+ </div>
446
+ </div>
447
+
448
+ <!-- comments added so far -->
449
+ <div
450
+ v-for="(c, idx) in reviewComments"
451
+ :key="idx"
452
+ class="rounded-lg border border-slate-800 bg-slate-900/50 p-3"
453
+ >
454
+ <div class="mb-1 flex items-start justify-between gap-2">
455
+ <div class="text-[10px] uppercase tracking-wide text-slate-500">
456
+ Comment {{ idx + 1 }}
457
+ </div>
458
+ <button
459
+ class="text-slate-500 transition hover:text-rose-400"
460
+ title="Remove comment"
461
+ @click="removeComment(idx)"
462
+ >
463
+ <UIcon name="i-lucide-x" class="h-3.5 w-3.5" />
464
+ </button>
465
+ </div>
466
+ <pre
467
+ class="mb-1 max-h-20 overflow-auto whitespace-pre-wrap rounded bg-slate-950/50 p-1.5 text-[10px] text-slate-400"
468
+ >{{ c.quotedSource }}</pre
469
+ >
470
+ <p class="text-[12px] text-slate-200">{{ c.body }}</p>
471
+ </div>
472
+
473
+ <div>
474
+ <label
475
+ class="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-slate-400"
476
+ >
477
+ Overall feedback / reject reason
478
+ </label>
479
+ <UTextarea
480
+ v-model="feedback"
481
+ :rows="3"
482
+ autoresize
483
+ size="sm"
484
+ class="w-full"
485
+ placeholder="Describe the changes the agent should make (optional if you left per-block comments)…"
486
+ />
487
+ </div>
488
+ </template>
489
+ </div>
490
+
491
+ <!-- edit-then-approve actions -->
492
+ <div v-if="editing" class="space-y-2 border-t border-slate-800 px-4 py-3">
493
+ <UButton
494
+ color="primary"
495
+ size="sm"
496
+ icon="i-lucide-check"
497
+ block
498
+ :loading="submitting"
499
+ @click="approveWithEdits"
500
+ >
501
+ Approve with these edits
502
+ </UButton>
503
+ <UButton
504
+ color="neutral"
505
+ variant="ghost"
506
+ size="sm"
507
+ block
508
+ :disabled="submitting"
509
+ @click="cancelEditing"
510
+ >
511
+ Cancel edits
512
+ </UButton>
513
+ </div>
514
+
515
+ <div v-else class="space-y-2 border-t border-slate-800 px-4 py-3">
516
+ <UButton
517
+ color="primary"
518
+ size="sm"
519
+ icon="i-lucide-check"
520
+ block
521
+ :disabled="rejectArmed"
522
+ :loading="submitting"
523
+ @click="approve"
524
+ >
525
+ Approve &amp; proceed
526
+ </UButton>
527
+ <UButton
528
+ color="primary"
529
+ variant="soft"
530
+ size="sm"
531
+ icon="i-lucide-pencil"
532
+ block
533
+ :disabled="rejectArmed || submitting"
534
+ @click="startEditing"
535
+ >
536
+ Approve with corrections
537
+ </UButton>
538
+
539
+ <!-- destructive: a two-step inline confirm instead of a native dialog -->
540
+ <div
541
+ v-if="rejectArmed"
542
+ class="rounded-lg border border-rose-500/40 bg-rose-500/5 p-2.5"
543
+ >
544
+ <p class="mb-2 text-[11px] text-rose-200">
545
+ Reject this proposal and stop the run entirely?
546
+ </p>
547
+ <div class="flex gap-2">
548
+ <UButton
549
+ color="neutral"
550
+ variant="ghost"
551
+ size="xs"
552
+ class="flex-1"
553
+ :disabled="submitting"
554
+ @click="disarmReject"
555
+ >
556
+ Cancel
557
+ </UButton>
558
+ <UButton
559
+ color="error"
560
+ size="xs"
561
+ icon="i-lucide-ban"
562
+ class="flex-1"
563
+ :loading="submitting"
564
+ @click="reject"
565
+ >
566
+ Confirm reject
567
+ </UButton>
568
+ </div>
569
+ </div>
570
+ <div v-else class="flex gap-2">
571
+ <UButton
572
+ color="warning"
573
+ variant="soft"
574
+ size="sm"
575
+ icon="i-lucide-rotate-ccw"
576
+ class="flex-1"
577
+ :disabled="!canRequestChanges"
578
+ :loading="submitting"
579
+ @click="requestChanges"
580
+ >
581
+ Request changes
582
+ </UButton>
583
+ <UButton
584
+ color="error"
585
+ variant="soft"
586
+ size="sm"
587
+ icon="i-lucide-ban"
588
+ class="flex-1"
589
+ :disabled="submitting"
590
+ @click="armReject"
591
+ >
592
+ Reject
593
+ </UButton>
594
+ </div>
595
+ <p class="text-[10px] text-slate-500">
596
+ Request changes re-runs this step with your feedback &amp; comments. Reject stops the
597
+ run entirely.
598
+ </p>
599
+ </div>
600
+ </aside>
601
+ </div>
602
+ </Transition>
603
+ </Teleport>
604
+ </template>
605
+
606
+ <style scoped>
607
+ .reader-fade-enter-active,
608
+ .reader-fade-leave-active {
609
+ transition: opacity 0.18s ease;
610
+ }
611
+ .reader-fade-enter-from,
612
+ .reader-fade-leave-to {
613
+ opacity: 0;
614
+ }
615
+
616
+ /* Approval mode: each source-mapped block becomes a comment target — a hover
617
+ highlight + a "+" gutter affordance, GitHub-review style. */
618
+ .reader-prose.review-mode :deep([data-src-start]) {
619
+ position: relative;
620
+ cursor: pointer;
621
+ border-radius: 0.375rem;
622
+ transition: background 0.12s ease;
623
+ }
624
+ .reader-prose.review-mode :deep([data-src-start]:hover) {
625
+ background: rgb(99 102 241 / 0.08);
626
+ box-shadow: inset 2px 0 0 rgb(99 102 241 / 0.5);
627
+ }
628
+ .reader-prose.review-mode :deep([data-src-start])::before {
629
+ content: '+';
630
+ position: absolute;
631
+ left: -1.4rem;
632
+ top: 0.1rem;
633
+ display: none;
634
+ height: 1.1rem;
635
+ width: 1.1rem;
636
+ align-items: center;
637
+ justify-content: center;
638
+ border-radius: 0.25rem;
639
+ background: rgb(99 102 241);
640
+ color: white;
641
+ font-size: 0.8rem;
642
+ line-height: 1;
643
+ }
644
+ .reader-prose.review-mode :deep([data-src-start]:hover)::before {
645
+ display: flex;
646
+ }
647
+ /* Persistent markers: amber for a block that already has a comment, indigo for
648
+ the block whose composer is currently open. */
649
+ .reader-prose :deep(.cf-commented) {
650
+ background: rgb(234 179 8 / 0.1);
651
+ box-shadow: inset 2px 0 0 rgb(234 179 8 / 0.6);
652
+ }
653
+ .reader-prose :deep(.cf-selected) {
654
+ background: rgb(99 102 241 / 0.12);
655
+ box-shadow: inset 2px 0 0 rgb(99 102 241 / 0.8);
656
+ }
657
+
658
+ /* Styling for the markdown HTML injected via v-html (out of scoped reach without
659
+ :deep), kept close to the inspector's existing prose styling. */
660
+ .reader-prose :deep(p) {
661
+ margin: 0.5rem 0;
662
+ }
663
+ .reader-prose :deep(ul),
664
+ .reader-prose :deep(ol) {
665
+ margin: 0.5rem 0;
666
+ padding-left: 1.25rem;
667
+ }
668
+ .reader-prose :deep(ul) {
669
+ list-style: disc;
670
+ }
671
+ .reader-prose :deep(ol) {
672
+ list-style: decimal;
673
+ }
674
+ .reader-prose :deep(li) {
675
+ margin: 0.2rem 0;
676
+ }
677
+ .reader-prose :deep(strong) {
678
+ font-weight: 600;
679
+ color: rgb(226 232 240);
680
+ }
681
+ .reader-prose :deep(em) {
682
+ font-style: italic;
683
+ }
684
+ .reader-prose :deep(code) {
685
+ border-radius: 0.25rem;
686
+ background: rgb(30 41 59 / 0.8);
687
+ padding: 0.1rem 0.3rem;
688
+ font-family: ui-monospace, monospace;
689
+ font-size: 0.85em;
690
+ color: rgb(199 210 254);
691
+ }
692
+ .reader-prose :deep(pre) {
693
+ margin: 0.6rem 0;
694
+ overflow: auto;
695
+ border-radius: 0.5rem;
696
+ background: rgb(2 6 23 / 0.6);
697
+ padding: 0.75rem 0.9rem;
698
+ }
699
+ .reader-prose :deep(pre code) {
700
+ background: transparent;
701
+ padding: 0;
702
+ color: rgb(203 213 225);
703
+ }
704
+ .reader-prose :deep(blockquote) {
705
+ margin: 0.6rem 0;
706
+ border-left: 3px solid rgb(99 102 241 / 0.5);
707
+ padding-left: 0.75rem;
708
+ color: rgb(148 163 184);
709
+ }
710
+ .reader-prose :deep(table) {
711
+ margin: 0.6rem 0;
712
+ border-collapse: collapse;
713
+ font-size: 0.95em;
714
+ }
715
+ .reader-prose :deep(th),
716
+ .reader-prose :deep(td) {
717
+ border: 1px solid rgb(51 65 85);
718
+ padding: 0.3rem 0.6rem;
719
+ }
720
+ .reader-prose :deep(th) {
721
+ background: rgb(30 41 59 / 0.6);
722
+ font-weight: 600;
723
+ }
724
+ .reader-prose :deep(hr) {
725
+ margin: 1rem 0;
726
+ border: none;
727
+ border-top: 1px solid rgb(51 65 85);
728
+ }
729
+ .reader-prose :deep(h1),
730
+ .reader-prose :deep(h2),
731
+ .reader-prose :deep(h3),
732
+ .reader-prose :deep(h4) {
733
+ margin: 0.6rem 0 0.3rem;
734
+ font-weight: 600;
735
+ color: rgb(226 232 240);
736
+ }
737
+ </style>