@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,817 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from 'vue'
3
+ import type { AgentKind, Pipeline } from '~/types/domain'
4
+ import AgentPalette from '~/components/palettes/AgentPalette.vue'
5
+ import AgentKindIcon from '~/components/pipeline/AgentKindIcon.vue'
6
+ import { agentKindMeta, companionForProducer, isConsensusEligibleKind } from '~/utils/catalog'
7
+ import type { ConsensusStrategy } from '~/types/consensus'
8
+
9
+ type DraftUnit = { index: number; kind: AgentKind; companionIndex: number | null }
10
+
11
+ const pipelines = usePipelinesStore()
12
+
13
+ const CONSENSUS_STRATEGIES: { value: ConsensusStrategy; label: string }[] = [
14
+ { value: 'specialist-panel', label: 'Specialist panel' },
15
+ { value: 'debate', label: 'Debate' },
16
+ { value: 'ranked-voting', label: 'Ranked voting' },
17
+ ]
18
+
19
+ /** Add a blank participant to the draft step's consensus config. */
20
+ function addParticipant(i: number) {
21
+ const cfg = pipelines.draftConsensus[i]
22
+ if (!cfg) return
23
+ cfg.participants.push({ id: `cp_${Math.random().toString(36).slice(2, 9)}`, role: 'Reviewer' })
24
+ }
25
+ function removeParticipant(i: number, pIdx: number) {
26
+ pipelines.draftConsensus[i]?.participants.splice(pIdx, 1)
27
+ }
28
+ /** Toggle gating on/off for a draft step's consensus config. */
29
+ function toggleGating(i: number) {
30
+ const cfg = pipelines.draftConsensus[i]
31
+ if (!cfg) return
32
+ cfg.gating = cfg.gating?.enabled
33
+ ? { ...cfg.gating, enabled: false }
34
+ : { enabled: true, minRisk: 0.6, minImpact: 0.6 }
35
+ }
36
+ const agents = useAgentsStore()
37
+ const ui = useUiStore()
38
+ const releaseHealth = useReleaseHealthStore()
39
+
40
+ const open = computed({
41
+ get: () => ui.builderOpen,
42
+ set: (v: boolean) => (ui.builderOpen = v),
43
+ })
44
+
45
+ // Refresh the observability-integration state whenever the builder opens so the palette
46
+ // knows whether to offer the post-release-health gate (it's loaded on demand, not from
47
+ // the snapshot). Best-effort: a failure just leaves the gate hidden.
48
+ watch(open, (isOpen) => {
49
+ if (isOpen) releaseHealth.load().catch(() => {})
50
+ })
51
+
52
+ function add(kind: AgentKind) {
53
+ pipelines.addToDraft(kind)
54
+ }
55
+
56
+ // Saved pipelines render collapsed (name + step count); a click expands the full
57
+ // ordered step list. Tracking expansion as a Set keyed by pipeline id keeps the
58
+ // icon row from overflowing the narrow panel the way an always-on inline list did.
59
+ const expandedSaved = ref<Set<string>>(new Set())
60
+ function toggleSaved(id: string) {
61
+ const next = new Set(expandedSaved.value)
62
+ if (next.has(id)) next.delete(id)
63
+ else next.add(id)
64
+ expandedSaved.value = next
65
+ }
66
+
67
+ const toast = useToast()
68
+
69
+ // ---- "Add agent" mini-form -------------------------------------------------
70
+ const addAgentOpen = ref(false)
71
+ const newAgentName = ref('')
72
+ const newAgentDesc = ref('')
73
+
74
+ function openAddAgent() {
75
+ newAgentName.value = ''
76
+ newAgentDesc.value = ''
77
+ addAgentOpen.value = true
78
+ }
79
+
80
+ function createAgent() {
81
+ const agent = agents.addAgent({
82
+ label: newAgentName.value,
83
+ description: newAgentDesc.value,
84
+ })
85
+ toast.add({ title: `Added agent “${agent.label}”`, color: 'success', icon: 'i-lucide-check' })
86
+ addAgentOpen.value = false
87
+ }
88
+
89
+ function placeholder(what: string) {
90
+ toast.add({ title: 'Placeholder', description: what, icon: 'i-lucide-construction' })
91
+ }
92
+
93
+ async function save() {
94
+ const wasEditing = pipelines.editingId !== null
95
+ try {
96
+ const saved = await pipelines.saveDraft()
97
+ if (saved) {
98
+ toast.add({
99
+ title: wasEditing ? `Updated “${saved.name}”` : `Saved “${saved.name}”`,
100
+ color: 'success',
101
+ icon: 'i-lucide-check',
102
+ })
103
+ ui.builderOpen = false
104
+ } else {
105
+ toast.add({ title: 'Add at least one agent first', color: 'warning' })
106
+ }
107
+ } catch (e) {
108
+ // Surface the backend reason (e.g. post-release-health rejected without an
109
+ // observability integration) rather than a generic failure.
110
+ toast.add({
111
+ title: 'Could not save pipeline',
112
+ description: e instanceof Error ? e.message : undefined,
113
+ color: 'error',
114
+ })
115
+ }
116
+ }
117
+
118
+ /** Remove a producer unit, taking its attached companion with it (companion index first). */
119
+ function removeUnit(unit: DraftUnit) {
120
+ if (unit.companionIndex !== null) pipelines.removeFromDraft(unit.companionIndex)
121
+ pipelines.removeFromDraft(unit.index)
122
+ }
123
+
124
+ /** Enable/disable a producer unit, keeping its attached companion's enable flag in sync. */
125
+ function toggleEnabled(unit: DraftUnit) {
126
+ pipelines.toggleDraftEnabled(unit.index)
127
+ if (unit.companionIndex !== null) {
128
+ pipelines.draftEnabled[unit.companionIndex] = pipelines.draftEnabled[unit.index] !== false
129
+ }
130
+ }
131
+
132
+ function companionLabel(kind: string): string | null {
133
+ const companion = companionForProducer(kind)
134
+ return companion ? agentKindMeta(companion).label : null
135
+ }
136
+
137
+ // Surfaced as an inline hint: a gated step needs a task-estimator before it (mirrors the
138
+ // backend validation, which also rejects the save/start).
139
+ const gatingNeedsEstimator = computed(() => {
140
+ const kinds = pipelines.draft
141
+ for (let i = 0; i < kinds.length; i++) {
142
+ if (!pipelines.draftGating[i]?.enabled || pipelines.draftEnabled[i] === false) continue
143
+ const hasEstimator = kinds
144
+ .slice(0, i)
145
+ .some((k, j) => k === 'task-estimator' && pipelines.draftEnabled[j] !== false)
146
+ if (!hasEstimator) return true
147
+ }
148
+ return false
149
+ })
150
+
151
+ // ---- draft labels ----------------------------------------------------------
152
+ const newLabel = ref('')
153
+ function addLabel() {
154
+ const v = newLabel.value.trim()
155
+ if (v && !pipelines.draftLabels.includes(v)) pipelines.draftLabels.push(v)
156
+ newLabel.value = ''
157
+ }
158
+ function removeLabel(label: string) {
159
+ pipelines.draftLabels = pipelines.draftLabels.filter((l) => l !== label)
160
+ }
161
+
162
+ // ---- saved-pipeline library filtering --------------------------------------
163
+ const labelFilter = ref<string | null>(null)
164
+ const showArchived = ref(false)
165
+ const allLabels = computed(() =>
166
+ [...new Set(pipelines.pipelines.flatMap((p) => p.labels ?? []))].sort(),
167
+ )
168
+ const archivedCount = computed(() => pipelines.pipelines.filter((p) => p.archived).length)
169
+ const visiblePipelines = computed(() =>
170
+ pipelines.pipelines.filter((p) => {
171
+ if (!showArchived.value && p.archived) return false
172
+ if (labelFilter.value && !(p.labels ?? []).includes(labelFilter.value)) return false
173
+ return true
174
+ }),
175
+ )
176
+ async function toggleArchive(p: Pipeline) {
177
+ try {
178
+ if (p.archived) await pipelines.unarchive(p.id)
179
+ else await pipelines.archive(p.id)
180
+ } catch {
181
+ toast.add({ title: 'Could not update pipeline', color: 'error' })
182
+ }
183
+ }
184
+
185
+ /** Load a custom pipeline into the draft for in-place editing. */
186
+ function edit(p: Pipeline) {
187
+ pipelines.loadForEdit(p)
188
+ }
189
+
190
+ /** Clone any pipeline (incl. a read-only built-in) into an editable copy, then edit it. */
191
+ async function clone(p: Pipeline) {
192
+ try {
193
+ const copy = await pipelines.clonePipeline(p.id)
194
+ toast.add({
195
+ title: `Cloned “${p.name}” — now editing “${copy.name}”`,
196
+ color: 'success',
197
+ icon: 'i-lucide-copy',
198
+ })
199
+ } catch {
200
+ toast.add({ title: 'Could not clone pipeline', color: 'error' })
201
+ }
202
+ }
203
+ </script>
204
+
205
+ <template>
206
+ <USlideover v-model:open="open" title="Pipeline builder" side="left">
207
+ <template #body>
208
+ <div class="grid h-full grid-cols-2 gap-4">
209
+ <!-- agent palette -->
210
+ <div class="overflow-y-auto pr-1">
211
+ <div class="mb-2 flex items-center justify-between gap-2">
212
+ <h3 class="text-xs font-semibold uppercase tracking-wide text-slate-400">
213
+ Agent palette
214
+ </h3>
215
+ <UButton
216
+ color="primary"
217
+ variant="soft"
218
+ size="xs"
219
+ icon="i-lucide-plus"
220
+ @click="openAddAgent"
221
+ >
222
+ Add agent
223
+ </UButton>
224
+ </div>
225
+ <AgentPalette @add="add" />
226
+ </div>
227
+
228
+ <!-- draft chain -->
229
+ <div class="flex flex-col">
230
+ <div class="mb-2 flex items-center justify-between gap-2">
231
+ <h3 class="text-xs font-semibold uppercase tracking-wide text-slate-400">Pipeline</h3>
232
+ <UButton
233
+ color="neutral"
234
+ variant="soft"
235
+ size="xs"
236
+ icon="i-lucide-cpu"
237
+ title="Pick which model each agent kind runs on"
238
+ @click="ui.openModelDefaults()"
239
+ >
240
+ Configure models
241
+ </UButton>
242
+ </div>
243
+ <UInput
244
+ v-model="pipelines.draftName"
245
+ placeholder="Pipeline name"
246
+ size="sm"
247
+ class="mb-2"
248
+ />
249
+
250
+ <!-- Labels: organize the pipeline in the library (filter/search). -->
251
+ <div class="mb-3 flex flex-wrap items-center gap-1.5">
252
+ <UBadge
253
+ v-for="l in pipelines.draftLabels"
254
+ :key="l"
255
+ color="neutral"
256
+ variant="soft"
257
+ size="xs"
258
+ class="gap-1"
259
+ >
260
+ {{ l }}
261
+ <button type="button" class="hover:text-rose-400" @click="removeLabel(l)">
262
+ <UIcon name="i-lucide-x" class="h-3 w-3" />
263
+ </button>
264
+ </UBadge>
265
+ <input
266
+ v-model="newLabel"
267
+ placeholder="+ label"
268
+ class="w-20 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-[11px] text-slate-200 focus:w-28"
269
+ @keydown.enter.prevent="addLabel"
270
+ @blur="addLabel"
271
+ />
272
+ </div>
273
+
274
+ <p
275
+ v-if="gatingNeedsEstimator"
276
+ class="mb-2 flex items-center gap-1.5 rounded-md border border-amber-800/50 bg-amber-950/30 px-2 py-1 text-[11px] text-amber-300"
277
+ >
278
+ <UIcon name="i-lucide-alert-triangle" class="h-3.5 w-3.5 shrink-0" />
279
+ A gated step needs a Task Estimator before it — add one or the pipeline won't save.
280
+ </p>
281
+
282
+ <div
283
+ v-if="pipelines.draft.length === 0"
284
+ class="flex flex-1 items-center justify-center rounded-lg border border-dashed border-slate-700 p-4 text-center text-xs text-slate-500"
285
+ >
286
+ Click agents on the left to assemble a linear pipeline.
287
+ </div>
288
+
289
+ <ol v-else class="flex-1 space-y-2 overflow-y-auto">
290
+ <li
291
+ v-for="(unit, vi) in pipelines.units"
292
+ :key="unit.index"
293
+ class="flex flex-col gap-2 rounded-lg border border-slate-700 bg-slate-800/60 p-2"
294
+ :class="{ 'opacity-50': pipelines.draftEnabled[unit.index] === false }"
295
+ >
296
+ <div class="flex items-center gap-1.5">
297
+ <span class="w-4 shrink-0 text-center text-[10px] text-slate-500">{{
298
+ vi + 1
299
+ }}</span>
300
+ <AgentKindIcon :kind="unit.kind" icon-class="h-4 w-4" />
301
+ <span
302
+ class="min-w-0 flex-1 truncate text-xs text-slate-100"
303
+ :class="{ 'line-through': pipelines.draftEnabled[unit.index] === false }"
304
+ :title="agentKindMeta(unit.kind).description"
305
+ >
306
+ {{ agentKindMeta(unit.kind).label }}
307
+ </span>
308
+ <div class="flex shrink-0 items-center">
309
+ <!-- Companion toggle: attach/detach the dependent reviewer for this producer. -->
310
+ <UButton
311
+ v-if="companionForProducer(unit.kind)"
312
+ :icon="
313
+ unit.companionIndex !== null ? 'i-lucide-scan-eye' : 'i-lucide-scan-search'
314
+ "
315
+ :color="unit.companionIndex !== null ? 'secondary' : 'neutral'"
316
+ variant="ghost"
317
+ size="xs"
318
+ :title="
319
+ unit.companionIndex !== null
320
+ ? `Remove the ${companionLabel(unit.kind)} for this step`
321
+ : `Add the ${companionLabel(unit.kind)} (reviews this step, loops it back below threshold)`
322
+ "
323
+ @click="pipelines.toggleCompanion(unit.index)"
324
+ />
325
+ <!-- Enable/disable: keep the step in the pipeline but skip it at run. -->
326
+ <UButton
327
+ :icon="
328
+ pipelines.draftEnabled[unit.index] === false
329
+ ? 'i-lucide-eye-off'
330
+ : 'i-lucide-eye'
331
+ "
332
+ :color="pipelines.draftEnabled[unit.index] === false ? 'neutral' : 'primary'"
333
+ variant="ghost"
334
+ size="xs"
335
+ :title="
336
+ pipelines.draftEnabled[unit.index] === false
337
+ ? 'Step disabled (skipped at run) — click to enable'
338
+ : 'Disable this step (kept in the pipeline but skipped at run)'
339
+ "
340
+ @click="toggleEnabled(unit)"
341
+ />
342
+ <!-- Approval gate: pause after this step so a human reviews (and
343
+ can edit) its proposal before the next step runs. -->
344
+ <UButton
345
+ :icon="
346
+ pipelines.draftGates[unit.index] ? 'i-lucide-shield-check' : 'i-lucide-shield'
347
+ "
348
+ :color="pipelines.draftGates[unit.index] ? 'warning' : 'neutral'"
349
+ variant="ghost"
350
+ size="xs"
351
+ :title="
352
+ pipelines.draftGates[unit.index]
353
+ ? 'Approval required after this step — click to remove the gate'
354
+ : 'Require human approval after this step'
355
+ "
356
+ @click="pipelines.toggleDraftGate(unit.index)"
357
+ />
358
+ <!-- Consensus: run this step through the multi-model mechanism (eligible
359
+ kinds only — architect/analysis/task-estimator). -->
360
+ <UButton
361
+ v-if="isConsensusEligibleKind(unit.kind)"
362
+ :icon="
363
+ pipelines.draftConsensus[unit.index]?.enabled
364
+ ? 'i-lucide-users-round'
365
+ : 'i-lucide-user'
366
+ "
367
+ :color="pipelines.draftConsensus[unit.index]?.enabled ? 'success' : 'neutral'"
368
+ variant="ghost"
369
+ size="xs"
370
+ :title="
371
+ pipelines.draftConsensus[unit.index]?.enabled
372
+ ? 'Consensus enabled — click to revert to a single agent'
373
+ : 'Enable consensus (multi-model panel/debate/voting) for this step'
374
+ "
375
+ @click="pipelines.toggleDraftConsensus(unit.index)"
376
+ />
377
+ <UButton
378
+ icon="i-lucide-chevron-up"
379
+ color="neutral"
380
+ variant="ghost"
381
+ size="xs"
382
+ :disabled="vi === 0"
383
+ @click="pipelines.moveUnit(vi, vi - 1)"
384
+ />
385
+ <UButton
386
+ icon="i-lucide-chevron-down"
387
+ color="neutral"
388
+ variant="ghost"
389
+ size="xs"
390
+ :disabled="vi === pipelines.units.length - 1"
391
+ @click="pipelines.moveUnit(vi, vi + 1)"
392
+ />
393
+ <UButton
394
+ icon="i-lucide-x"
395
+ color="error"
396
+ variant="ghost"
397
+ size="xs"
398
+ title="Remove this step from the pipeline"
399
+ @click="removeUnit(unit)"
400
+ />
401
+ </div>
402
+ </div>
403
+
404
+ <!-- Attached companion: a dependent reviewer for this producer, optionally
405
+ gated on the task estimate. -->
406
+ <div
407
+ v-if="unit.companionIndex !== null"
408
+ class="ml-6 space-y-2 rounded-md border border-fuchsia-800/40 bg-fuchsia-950/20 p-2 text-xs"
409
+ >
410
+ <div class="flex items-center gap-1.5">
411
+ <UIcon name="i-lucide-corner-down-right" class="h-3.5 w-3.5 text-slate-500" />
412
+ <AgentKindIcon
413
+ :kind="pipelines.draft[unit.companionIndex]!"
414
+ icon-class="h-4 w-4"
415
+ />
416
+ <span class="min-w-0 flex-1 truncate text-slate-200">
417
+ {{ agentKindMeta(pipelines.draft[unit.companionIndex]!).label }}
418
+ </span>
419
+ <UButton
420
+ :icon="
421
+ pipelines.draftGating[unit.companionIndex]?.enabled
422
+ ? 'i-lucide-toggle-right'
423
+ : 'i-lucide-toggle-left'
424
+ "
425
+ :color="
426
+ pipelines.draftGating[unit.companionIndex]?.enabled ? 'success' : 'neutral'
427
+ "
428
+ variant="ghost"
429
+ size="xs"
430
+ label="Gate on estimate"
431
+ title="Only run this companion when the task estimate clears a threshold (needs a Task Estimator earlier)"
432
+ @click="pipelines.toggleDraftGating(unit.companionIndex)"
433
+ />
434
+ </div>
435
+ <div
436
+ v-if="pipelines.draftGating[unit.companionIndex]?.enabled"
437
+ class="flex flex-wrap items-center gap-2 border-t border-slate-800 pt-2"
438
+ >
439
+ <span class="text-[10px] text-slate-500">run when (any):</span>
440
+ <label class="text-slate-400">complexity ≥</label>
441
+ <input
442
+ v-model.number="pipelines.draftGating[unit.companionIndex]!.minComplexity"
443
+ type="number"
444
+ min="0"
445
+ max="1"
446
+ step="0.1"
447
+ class="w-14 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
448
+ />
449
+ <label class="text-slate-400">risk ≥</label>
450
+ <input
451
+ v-model.number="pipelines.draftGating[unit.companionIndex]!.minRisk"
452
+ type="number"
453
+ min="0"
454
+ max="1"
455
+ step="0.1"
456
+ class="w-14 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
457
+ />
458
+ <label class="text-slate-400">impact ≥</label>
459
+ <input
460
+ v-model.number="pipelines.draftGating[unit.companionIndex]!.minImpact"
461
+ type="number"
462
+ min="0"
463
+ max="1"
464
+ step="0.1"
465
+ class="w-14 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
466
+ />
467
+ </div>
468
+ </div>
469
+
470
+ <!-- Consensus config (shown when the step is consensus-enabled). -->
471
+ <div
472
+ v-if="pipelines.draftConsensus[unit.index]?.enabled"
473
+ class="ml-6 space-y-2 rounded-md border border-emerald-800/40 bg-emerald-950/20 p-2 text-xs"
474
+ >
475
+ <div class="flex items-center gap-2">
476
+ <label class="text-slate-400">Strategy</label>
477
+ <select
478
+ v-model="pipelines.draftConsensus[unit.index]!.strategy"
479
+ class="rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
480
+ >
481
+ <option v-for="s in CONSENSUS_STRATEGIES" :key="s.value" :value="s.value">
482
+ {{ s.label }}
483
+ </option>
484
+ </select>
485
+ <label
486
+ v-if="pipelines.draftConsensus[unit.index]!.strategy === 'debate'"
487
+ class="ml-2 text-slate-400"
488
+ >Rounds</label
489
+ >
490
+ <input
491
+ v-if="pipelines.draftConsensus[unit.index]!.strategy === 'debate'"
492
+ v-model.number="pipelines.draftConsensus[unit.index]!.rounds"
493
+ type="number"
494
+ min="1"
495
+ max="5"
496
+ placeholder="2"
497
+ class="w-12 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
498
+ />
499
+ </div>
500
+
501
+ <!-- participants -->
502
+ <div class="space-y-1">
503
+ <div
504
+ v-for="(p, pIdx) in pipelines.draftConsensus[unit.index]!.participants"
505
+ :key="p.id"
506
+ class="flex items-center gap-1.5"
507
+ >
508
+ <input
509
+ v-model="p.role"
510
+ placeholder="Role"
511
+ class="w-28 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
512
+ />
513
+ <input
514
+ v-model="p.modelId"
515
+ placeholder="Model id (optional)"
516
+ class="flex-1 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-300"
517
+ />
518
+ <UButton
519
+ icon="i-lucide-x"
520
+ color="error"
521
+ variant="ghost"
522
+ size="xs"
523
+ :disabled="pipelines.draftConsensus[unit.index]!.participants.length <= 2"
524
+ title="Remove participant (min 2)"
525
+ @click="removeParticipant(unit.index, pIdx)"
526
+ />
527
+ </div>
528
+ <UButton
529
+ icon="i-lucide-plus"
530
+ color="neutral"
531
+ variant="ghost"
532
+ size="xs"
533
+ label="Add participant"
534
+ @click="addParticipant(unit.index)"
535
+ />
536
+ </div>
537
+
538
+ <!-- gating -->
539
+ <div class="flex flex-wrap items-center gap-2 border-t border-slate-800 pt-2">
540
+ <UButton
541
+ :icon="
542
+ pipelines.draftConsensus[unit.index]!.gating?.enabled
543
+ ? 'i-lucide-toggle-right'
544
+ : 'i-lucide-toggle-left'
545
+ "
546
+ :color="
547
+ pipelines.draftConsensus[unit.index]!.gating?.enabled ? 'success' : 'neutral'
548
+ "
549
+ variant="ghost"
550
+ size="xs"
551
+ label="Gate on estimate"
552
+ title="Only run consensus when the task estimate clears a threshold (else the standard agent runs)"
553
+ @click="toggleGating(unit.index)"
554
+ />
555
+ <template v-if="pipelines.draftConsensus[unit.index]!.gating?.enabled">
556
+ <label class="text-slate-400">risk ≥</label>
557
+ <input
558
+ v-model.number="pipelines.draftConsensus[unit.index]!.gating!.minRisk"
559
+ type="number"
560
+ min="0"
561
+ max="1"
562
+ step="0.1"
563
+ class="w-14 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
564
+ />
565
+ <label class="text-slate-400">impact ≥</label>
566
+ <input
567
+ v-model.number="pipelines.draftConsensus[unit.index]!.gating!.minImpact"
568
+ type="number"
569
+ min="0"
570
+ max="1"
571
+ step="0.1"
572
+ class="w-14 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
573
+ />
574
+ </template>
575
+ </div>
576
+ </div>
577
+ </li>
578
+ </ol>
579
+
580
+ <!-- Saved pipelines: review the library + delete (the run affordance
581
+ moved to the task card / inspector when the palettes were removed). -->
582
+ <div v-if="pipelines.pipelines.length" class="mt-4 border-t border-slate-800 pt-3">
583
+ <div class="mb-2 flex items-center justify-between gap-2">
584
+ <h3 class="text-xs font-semibold uppercase tracking-wide text-slate-400">
585
+ Saved pipelines
586
+ </h3>
587
+ <UButton
588
+ v-if="archivedCount"
589
+ :icon="showArchived ? 'i-lucide-archive-restore' : 'i-lucide-archive'"
590
+ :color="showArchived ? 'primary' : 'neutral'"
591
+ variant="ghost"
592
+ size="xs"
593
+ @click="showArchived = !showArchived"
594
+ >
595
+ {{ showArchived ? 'Hide archived' : `Archived (${archivedCount})` }}
596
+ </UButton>
597
+ </div>
598
+
599
+ <!-- Label filter chips. -->
600
+ <div v-if="allLabels.length" class="mb-2 flex flex-wrap items-center gap-1">
601
+ <UBadge
602
+ :color="labelFilter === null ? 'primary' : 'neutral'"
603
+ variant="soft"
604
+ size="xs"
605
+ class="cursor-pointer"
606
+ @click="labelFilter = null"
607
+ >
608
+ All
609
+ </UBadge>
610
+ <UBadge
611
+ v-for="l in allLabels"
612
+ :key="l"
613
+ :color="labelFilter === l ? 'primary' : 'neutral'"
614
+ variant="soft"
615
+ size="xs"
616
+ class="cursor-pointer"
617
+ @click="labelFilter = labelFilter === l ? null : l"
618
+ >
619
+ {{ l }}
620
+ </UBadge>
621
+ </div>
622
+
623
+ <ul class="space-y-1.5">
624
+ <li
625
+ v-for="p in visiblePipelines"
626
+ :key="p.id"
627
+ class="group rounded-lg border border-slate-700 bg-slate-800/40"
628
+ :class="{ 'opacity-60': p.archived }"
629
+ >
630
+ <div class="flex items-center gap-2 px-2 py-1.5">
631
+ <button
632
+ type="button"
633
+ class="flex min-w-0 flex-1 items-center gap-2 text-left"
634
+ @click="toggleSaved(p.id)"
635
+ >
636
+ <UIcon
637
+ :name="
638
+ expandedSaved.has(p.id) ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'
639
+ "
640
+ class="h-3.5 w-3.5 shrink-0 text-slate-500"
641
+ />
642
+ <span class="min-w-0 flex-1 truncate text-xs text-slate-200">{{ p.name }}</span>
643
+ <UBadge
644
+ v-for="l in p.labels ?? []"
645
+ :key="l"
646
+ color="info"
647
+ variant="soft"
648
+ size="xs"
649
+ class="shrink-0"
650
+ >
651
+ {{ l }}
652
+ </UBadge>
653
+ <UBadge
654
+ v-if="p.builtin"
655
+ color="neutral"
656
+ variant="soft"
657
+ size="xs"
658
+ class="shrink-0"
659
+ >
660
+ default
661
+ </UBadge>
662
+ <span class="shrink-0 text-[10px] text-slate-500">
663
+ {{ p.agentKinds.length }} {{ p.agentKinds.length === 1 ? 'step' : 'steps' }}
664
+ </span>
665
+ </button>
666
+ <div
667
+ class="flex shrink-0 items-center opacity-0 transition group-hover:opacity-100"
668
+ >
669
+ <!-- Archive/unarchive: organize the library without deleting. Works on
670
+ built-ins too (view metadata, not structure). -->
671
+ <UButton
672
+ :icon="p.archived ? 'i-lucide-archive-restore' : 'i-lucide-archive'"
673
+ color="neutral"
674
+ variant="ghost"
675
+ size="xs"
676
+ :title="p.archived ? 'Unarchive' : 'Archive (hide from the default view)'"
677
+ @click="toggleArchive(p)"
678
+ />
679
+ <!-- Clone is available on every pipeline — it's how a read-only
680
+ built-in template becomes an editable copy. -->
681
+ <UButton
682
+ icon="i-lucide-copy"
683
+ color="neutral"
684
+ variant="ghost"
685
+ size="xs"
686
+ :title="p.builtin ? 'Clone this default into an editable copy' : 'Clone'"
687
+ @click="clone(p)"
688
+ />
689
+ <!-- Built-in templates are read-only; only custom pipelines edit in place. -->
690
+ <UButton
691
+ v-if="!p.builtin"
692
+ icon="i-lucide-pencil"
693
+ color="neutral"
694
+ variant="ghost"
695
+ size="xs"
696
+ title="Edit this pipeline"
697
+ @click="edit(p)"
698
+ />
699
+ <!-- Built-in templates are read-only — they can be cloned but not
700
+ deleted (the backend rejects it too); only custom ones delete. -->
701
+ <UButton
702
+ v-if="!p.builtin"
703
+ icon="i-lucide-trash-2"
704
+ color="neutral"
705
+ variant="ghost"
706
+ size="xs"
707
+ @click="pipelines.removePipeline(p.id)"
708
+ />
709
+ </div>
710
+ </div>
711
+
712
+ <!-- Full ordered step list, revealed on click. -->
713
+ <ol
714
+ v-if="expandedSaved.has(p.id)"
715
+ class="space-y-1 border-t border-slate-800 px-2 py-2 pl-7"
716
+ >
717
+ <li
718
+ v-for="(k, i) in p.agentKinds"
719
+ :key="i"
720
+ class="flex items-center gap-2"
721
+ :class="{ 'opacity-50 line-through': p.enabled?.[i] === false }"
722
+ :title="p.enabled?.[i] === false ? 'Disabled — skipped at run' : undefined"
723
+ >
724
+ <span class="w-4 shrink-0 text-center text-[10px] text-slate-500">{{
725
+ i + 1
726
+ }}</span>
727
+ <AgentKindIcon :kind="k" show-label />
728
+ </li>
729
+ </ol>
730
+ </li>
731
+ </ul>
732
+ </div>
733
+ </div>
734
+ </div>
735
+ </template>
736
+
737
+ <template #footer>
738
+ <div class="flex w-full items-center justify-between">
739
+ <UButton color="neutral" variant="ghost" size="sm" @click="pipelines.clearDraft()">
740
+ {{ pipelines.editingId ? 'Cancel edit' : 'Clear' }}
741
+ </UButton>
742
+ <UButton
743
+ color="primary"
744
+ icon="i-lucide-save"
745
+ size="sm"
746
+ :disabled="pipelines.draft.length === 0"
747
+ @click="save"
748
+ >
749
+ {{ pipelines.editingId ? 'Update pipeline' : 'Save pipeline' }}
750
+ </UButton>
751
+ </div>
752
+ </template>
753
+ </USlideover>
754
+
755
+ <!-- Add-agent form -->
756
+ <UModal v-model:open="addAgentOpen" title="Add agent">
757
+ <template #body>
758
+ <div class="space-y-3">
759
+ <div>
760
+ <label
761
+ class="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-slate-400"
762
+ >
763
+ Name
764
+ </label>
765
+ <UInput
766
+ v-model="newAgentName"
767
+ placeholder="e.g. Security Auditor"
768
+ size="sm"
769
+ class="w-full"
770
+ />
771
+ </div>
772
+ <div>
773
+ <label
774
+ class="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-slate-400"
775
+ >
776
+ Description
777
+ </label>
778
+ <UTextarea
779
+ v-model="newAgentDesc"
780
+ :rows="2"
781
+ autoresize
782
+ size="sm"
783
+ class="w-full"
784
+ placeholder="What does this agent do?"
785
+ />
786
+ </div>
787
+ <UButton
788
+ color="neutral"
789
+ variant="soft"
790
+ size="xs"
791
+ icon="i-lucide-file-text"
792
+ block
793
+ @click="placeholder('Link context document')"
794
+ >
795
+ Link context document
796
+ </UButton>
797
+ </div>
798
+ </template>
799
+
800
+ <template #footer>
801
+ <div class="flex w-full items-center justify-end gap-2">
802
+ <UButton color="neutral" variant="ghost" size="sm" @click="addAgentOpen = false">
803
+ Cancel
804
+ </UButton>
805
+ <UButton
806
+ color="primary"
807
+ icon="i-lucide-plus"
808
+ size="sm"
809
+ :disabled="!newAgentName.trim()"
810
+ @click="createAgent"
811
+ >
812
+ Create agent
813
+ </UButton>
814
+ </div>
815
+ </template>
816
+ </UModal>
817
+ </template>