@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,404 @@
1
+ <script setup lang="ts">
2
+ // Test-report window — the dedicated surface for a `tester` step's structured report
3
+ // (opened via the universal result-view host, the same seam the requirements review
4
+ // uses). It renders the report as a hierarchical tree: the scenarios the Tester chose
5
+ // to exercise (its `tested` areas, which map to the spec's acceptance scenarios) →
6
+ // the per-area outcomes (passed / failed / skipped) → the concerns linked to them,
7
+ // plus the overall greenlight verdict and the Tester→Fixer loop state.
8
+ //
9
+ // The service spec is not exposed to the SPA, so "linked spec elements" are derived
10
+ // from the report itself: each `tested` entry is the scenario the Tester walked, and
11
+ // outcomes / concerns are grouped under it by name. Deeper linkage to the in-repo
12
+ // `spec/features/*.feature` files would need a spec endpoint (a future enhancement).
13
+ import type { TestConcern, TestOutcome, TestReport } from '~/types/domain'
14
+ import StepRestartControl from '~/components/panels/StepRestartControl.vue'
15
+
16
+ const board = useBoardStore()
17
+ const execution = useExecutionStore()
18
+
19
+ // Shared seam contract (open/blockId/close + Escape). No `onOpen` loader: this window reads
20
+ // its report straight off the execution step, so there's nothing to fetch on open.
21
+ const { open, blockId, instanceId, stepIndex, close } = useResultView('tester')
22
+ const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
23
+
24
+ const step = computed(() => {
25
+ if (instanceId.value === null || stepIndex.value === null) return null
26
+ return execution.getInstance(instanceId.value)?.steps[stepIndex.value] ?? null
27
+ })
28
+ const report = computed<TestReport | null>(() => step.value?.test?.lastReport ?? null)
29
+ const testState = computed(() => step.value?.test ?? null)
30
+
31
+ const STATUS_META: Record<TestOutcome['status'], { icon: string; text: string; label: string }> = {
32
+ passed: { icon: 'i-lucide-circle-check', text: 'text-emerald-400', label: 'Passed' },
33
+ failed: { icon: 'i-lucide-circle-x', text: 'text-rose-400', label: 'Failed' },
34
+ skipped: { icon: 'i-lucide-circle-minus', text: 'text-slate-500', label: 'Skipped' },
35
+ }
36
+
37
+ const SEVERITY_META: Record<TestConcern['severity'], { text: string; chip: string; rank: number }> =
38
+ {
39
+ critical: { text: 'text-rose-300', chip: 'bg-rose-500/15 text-rose-300', rank: 0 },
40
+ high: { text: 'text-rose-300', chip: 'bg-rose-500/15 text-rose-300', rank: 1 },
41
+ medium: { text: 'text-amber-300', chip: 'bg-amber-500/15 text-amber-300', rank: 2 },
42
+ low: { text: 'text-slate-300', chip: 'bg-slate-500/15 text-slate-300', rank: 3 },
43
+ }
44
+
45
+ /** Case-insensitive "these two labels refer to the same thing" heuristic. */
46
+ function related(a: string, b: string): boolean {
47
+ const x = a.trim().toLowerCase()
48
+ const y = b.trim().toLowerCase()
49
+ if (!x || !y) return false
50
+ return x.includes(y) || y.includes(x)
51
+ }
52
+
53
+ interface ScenarioGroup {
54
+ key: string
55
+ title: string
56
+ /** true for the synthetic catch-all bucket of unmatched checks. */
57
+ other: boolean
58
+ outcomes: TestOutcome[]
59
+ concerns: TestConcern[]
60
+ status: 'passed' | 'failed' | 'skipped' | 'mixed' | 'empty'
61
+ }
62
+
63
+ /** Roll the per-area outcomes + concerns into one status for the scenario node. */
64
+ function rollUp(outcomes: TestOutcome[], concerns: TestConcern[]): ScenarioGroup['status'] {
65
+ const blocking = concerns.some((c) => c.severity === 'high' || c.severity === 'critical')
66
+ if (outcomes.some((o) => o.status === 'failed') || blocking) return 'failed'
67
+ if (!outcomes.length) return concerns.length ? 'mixed' : 'empty'
68
+ if (outcomes.every((o) => o.status === 'passed')) return 'passed'
69
+ if (outcomes.every((o) => o.status === 'skipped')) return 'skipped'
70
+ return 'mixed'
71
+ }
72
+
73
+ // Group outcomes + concerns under the scenarios the Tester listed in `tested`. An
74
+ // outcome/concern falls under a scenario when their names are related; anything left
75
+ // over lands in a synthetic "Other checks" bucket so nothing is dropped.
76
+ const groups = computed<ScenarioGroup[]>(() => {
77
+ const r = report.value
78
+ if (!r) return []
79
+ const outcomes = r.outcomes ?? []
80
+ const concerns = r.concerns ?? []
81
+ const usedOutcome = new Set<number>()
82
+ const usedConcern = new Set<number>()
83
+ const out: ScenarioGroup[] = []
84
+
85
+ r.tested.forEach((area, i) => {
86
+ const groupOutcomes = outcomes.filter((o, oi) => {
87
+ if (usedOutcome.has(oi)) return false
88
+ if (related(area, o.name)) {
89
+ usedOutcome.add(oi)
90
+ return true
91
+ }
92
+ return false
93
+ })
94
+ const groupConcerns = concerns.filter((c, ci) => {
95
+ if (usedConcern.has(ci)) return false
96
+ if (related(area, c.title) || groupOutcomes.some((o) => related(o.name, c.title))) {
97
+ usedConcern.add(ci)
98
+ return true
99
+ }
100
+ return false
101
+ })
102
+ out.push({
103
+ key: `s${i}`,
104
+ title: area,
105
+ other: false,
106
+ outcomes: groupOutcomes,
107
+ concerns: groupConcerns,
108
+ status: rollUp(groupOutcomes, groupConcerns),
109
+ })
110
+ })
111
+
112
+ const leftoverOutcomes = outcomes.filter((_, oi) => !usedOutcome.has(oi))
113
+ const leftoverConcerns = concerns.filter((_, ci) => !usedConcern.has(ci))
114
+ if (leftoverOutcomes.length || leftoverConcerns.length) {
115
+ out.push({
116
+ key: 'other',
117
+ title: r.tested.length ? 'Other checks' : 'Checks',
118
+ other: true,
119
+ outcomes: leftoverOutcomes,
120
+ concerns: leftoverConcerns,
121
+ status: rollUp(leftoverOutcomes, leftoverConcerns),
122
+ })
123
+ }
124
+ return out
125
+ })
126
+
127
+ const sortedConcerns = computed<TestConcern[]>(() => {
128
+ const r = report.value
129
+ if (!r) return []
130
+ return [...r.concerns].sort(
131
+ (a, b) => SEVERITY_META[a.severity].rank - SEVERITY_META[b.severity].rank,
132
+ )
133
+ })
134
+
135
+ const counts = computed(() => {
136
+ const r = report.value
137
+ const o = r?.outcomes ?? []
138
+ return {
139
+ passed: o.filter((x) => x.status === 'passed').length,
140
+ failed: o.filter((x) => x.status === 'failed').length,
141
+ skipped: o.filter((x) => x.status === 'skipped').length,
142
+ concerns: r?.concerns.length ?? 0,
143
+ blocking: (r?.concerns ?? []).filter((c) => c.severity === 'high' || c.severity === 'critical')
144
+ .length,
145
+ }
146
+ })
147
+
148
+ // Expand/collapse per scenario node; default everything open.
149
+ const collapsed = ref<Set<string>>(new Set())
150
+ function toggle(key: string) {
151
+ const next = new Set(collapsed.value)
152
+ if (next.has(key)) next.delete(key)
153
+ else next.add(key)
154
+ collapsed.value = next
155
+ }
156
+
157
+ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: string }> = {
158
+ passed: { icon: 'i-lucide-circle-check', text: 'text-emerald-400' },
159
+ failed: { icon: 'i-lucide-circle-x', text: 'text-rose-400' },
160
+ skipped: { icon: 'i-lucide-circle-minus', text: 'text-slate-500' },
161
+ mixed: { icon: 'i-lucide-circle-dot', text: 'text-amber-400' },
162
+ empty: { icon: 'i-lucide-circle-dashed', text: 'text-slate-500' },
163
+ }
164
+ </script>
165
+
166
+ <template>
167
+ <Teleport to="body">
168
+ <div
169
+ v-if="open"
170
+ class="fixed inset-0 z-50 flex items-stretch justify-center bg-slate-950/70 backdrop-blur-sm"
171
+ @click.self="close"
172
+ >
173
+ <div
174
+ class="m-4 flex w-full max-w-5xl flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl"
175
+ >
176
+ <!-- Header -->
177
+ <header class="flex items-center gap-3 border-b border-slate-800 px-5 py-3">
178
+ <span
179
+ class="flex h-8 w-8 items-center justify-center rounded-lg bg-amber-500/15 text-amber-300"
180
+ >
181
+ <UIcon name="i-lucide-flask-conical" class="h-4 w-4" />
182
+ </span>
183
+ <div class="min-w-0 flex-1">
184
+ <h2 class="truncate text-sm font-semibold text-slate-100">
185
+ Test report{{ block ? ` — ${block.title}` : '' }}
186
+ </h2>
187
+ <p class="truncate text-[11px] text-slate-400">
188
+ Exploratory + regression testing of the change
189
+ </p>
190
+ </div>
191
+ <UBadge
192
+ v-if="report"
193
+ :color="report.greenlight ? 'success' : 'warning'"
194
+ variant="subtle"
195
+ size="sm"
196
+ >
197
+ {{ report.greenlight ? 'Greenlit' : 'Needs fixes' }}
198
+ </UBadge>
199
+ <span
200
+ v-if="testState && testState.attempts > 0"
201
+ class="text-[11px] text-slate-400"
202
+ :title="'Fixer attempts'"
203
+ >
204
+ {{ testState.attempts }}/{{ testState.maxAttempts }} fix
205
+ <template v-if="testState.phase === 'fixing'"> · fixing…</template>
206
+ </span>
207
+ <StepRestartControl
208
+ :instance-id="instanceId"
209
+ :step-index="stepIndex"
210
+ @restarted="close"
211
+ />
212
+ <button
213
+ class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
214
+ @click="close"
215
+ >
216
+ <UIcon name="i-lucide-x" class="h-4 w-4" />
217
+ </button>
218
+ </header>
219
+
220
+ <div class="flex min-h-0 flex-1">
221
+ <!-- Main: scenarios → outcomes → concerns tree -->
222
+ <div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
223
+ <div
224
+ v-if="!report"
225
+ class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
226
+ >
227
+ <UIcon name="i-lucide-flask-conical" class="h-8 w-8 opacity-40" />
228
+ <p class="text-sm">No test report yet.</p>
229
+ <p class="max-w-sm text-[11px] text-slate-500">
230
+ The report appears once the Tester finishes a pass. While it runs, the step shows
231
+ live progress on the board.
232
+ </p>
233
+ </div>
234
+
235
+ <template v-else>
236
+ <!-- Summary -->
237
+ <p v-if="report.summary" class="mb-4 text-[13px] leading-relaxed text-slate-300">
238
+ {{ report.summary }}
239
+ </p>
240
+
241
+ <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
242
+ Scenarios &amp; outcomes
243
+ </h3>
244
+ <ul class="space-y-2">
245
+ <li
246
+ v-for="g in groups"
247
+ :key="g.key"
248
+ class="overflow-hidden rounded-lg border border-slate-800 bg-slate-900/60"
249
+ >
250
+ <button
251
+ class="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-slate-800/40"
252
+ @click="toggle(g.key)"
253
+ >
254
+ <UIcon
255
+ :name="
256
+ collapsed.has(g.key) ? 'i-lucide-chevron-right' : 'i-lucide-chevron-down'
257
+ "
258
+ class="h-3.5 w-3.5 shrink-0 text-slate-500"
259
+ />
260
+ <UIcon
261
+ :name="GROUP_STATUS_META[g.status].icon"
262
+ class="h-4 w-4 shrink-0"
263
+ :class="GROUP_STATUS_META[g.status].text"
264
+ />
265
+ <span
266
+ class="min-w-0 flex-1 truncate text-[13px]"
267
+ :class="g.other ? 'text-slate-400' : 'font-medium text-slate-200'"
268
+ >
269
+ {{ g.title }}
270
+ </span>
271
+ <span class="shrink-0 text-[11px] text-slate-500">
272
+ {{ g.outcomes.length }} check{{ g.outcomes.length === 1 ? '' : 's' }}
273
+ <template v-if="g.concerns.length">
274
+ · {{ g.concerns.length }} concern{{ g.concerns.length === 1 ? '' : 's' }}
275
+ </template>
276
+ </span>
277
+ </button>
278
+
279
+ <div v-if="!collapsed.has(g.key)" class="space-y-1 px-3 pb-3 pl-9">
280
+ <!-- Outcomes -->
281
+ <div
282
+ v-for="(o, oi) in g.outcomes"
283
+ :key="`o${oi}`"
284
+ class="flex items-start gap-2 py-0.5"
285
+ >
286
+ <UIcon
287
+ :name="STATUS_META[o.status].icon"
288
+ class="mt-0.5 h-3.5 w-3.5 shrink-0"
289
+ :class="STATUS_META[o.status].text"
290
+ />
291
+ <div class="min-w-0">
292
+ <span class="text-[13px] text-slate-200">{{ o.name }}</span>
293
+ <p v-if="o.detail" class="text-[12px] leading-snug text-slate-400">
294
+ {{ o.detail }}
295
+ </p>
296
+ </div>
297
+ </div>
298
+ <p v-if="!g.outcomes.length" class="py-0.5 text-[12px] italic text-slate-500">
299
+ No discrete check recorded for this scenario.
300
+ </p>
301
+
302
+ <!-- Concerns linked to this scenario -->
303
+ <div
304
+ v-for="(c, ci) in g.concerns"
305
+ :key="`c${ci}`"
306
+ class="mt-1 flex items-start gap-2 rounded-md border border-slate-800 bg-slate-950/40 px-2 py-1.5"
307
+ >
308
+ <UIcon
309
+ name="i-lucide-alert-triangle"
310
+ class="mt-0.5 h-3.5 w-3.5 shrink-0"
311
+ :class="SEVERITY_META[c.severity].text"
312
+ />
313
+ <div class="min-w-0">
314
+ <div class="flex items-center gap-1.5">
315
+ <span class="text-[12px] font-medium text-slate-200">{{ c.title }}</span>
316
+ <span
317
+ class="rounded px-1 text-[10px] uppercase"
318
+ :class="SEVERITY_META[c.severity].chip"
319
+ >
320
+ {{ c.severity }}
321
+ </span>
322
+ </div>
323
+ <p v-if="c.detail" class="text-[12px] leading-snug text-slate-400">
324
+ {{ c.detail }}
325
+ </p>
326
+ </div>
327
+ </div>
328
+ </div>
329
+ </li>
330
+ </ul>
331
+ </template>
332
+ </div>
333
+
334
+ <!-- Sidebar: metadata -->
335
+ <aside
336
+ class="hidden w-60 shrink-0 flex-col gap-4 border-l border-slate-800 bg-slate-900/50 px-4 py-4 lg:flex"
337
+ >
338
+ <div v-if="report">
339
+ <h4 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
340
+ Verdict
341
+ </h4>
342
+ <div class="flex items-center gap-2 text-[13px]">
343
+ <UIcon
344
+ :name="report.greenlight ? 'i-lucide-circle-check' : 'i-lucide-circle-x'"
345
+ class="h-4 w-4"
346
+ :class="report.greenlight ? 'text-emerald-400' : 'text-rose-400'"
347
+ />
348
+ <span :class="report.greenlight ? 'text-emerald-300' : 'text-rose-300'">
349
+ {{ report.greenlight ? 'Safe to release' : 'Withheld' }}
350
+ </span>
351
+ </div>
352
+ </div>
353
+
354
+ <div v-if="report">
355
+ <h4 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
356
+ Outcomes
357
+ </h4>
358
+ <dl class="space-y-1 text-[12px]">
359
+ <div class="flex items-center justify-between">
360
+ <dt class="text-slate-400">Passed</dt>
361
+ <dd class="text-emerald-300">{{ counts.passed }}</dd>
362
+ </div>
363
+ <div class="flex items-center justify-between">
364
+ <dt class="text-slate-400">Failed</dt>
365
+ <dd class="text-rose-300">{{ counts.failed }}</dd>
366
+ </div>
367
+ <div class="flex items-center justify-between">
368
+ <dt class="text-slate-400">Skipped</dt>
369
+ <dd class="text-slate-300">{{ counts.skipped }}</dd>
370
+ </div>
371
+ <div class="flex items-center justify-between border-t border-slate-800 pt-1">
372
+ <dt class="text-slate-400">Concerns</dt>
373
+ <dd class="text-amber-300">
374
+ {{ counts.concerns
375
+ }}<template v-if="counts.blocking"> ({{ counts.blocking }} blocking)</template>
376
+ </dd>
377
+ </div>
378
+ </dl>
379
+ </div>
380
+
381
+ <div v-if="report?.environment">
382
+ <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
383
+ Environment
384
+ </h4>
385
+ <p class="text-[12px] capitalize text-slate-300">{{ report.environment }}</p>
386
+ </div>
387
+
388
+ <div v-if="step?.model">
389
+ <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
390
+ Model
391
+ </h4>
392
+ <p class="break-all text-[12px] text-slate-300">{{ step.model }}</p>
393
+ </div>
394
+
395
+ <p class="mt-auto text-[10px] leading-relaxed text-slate-600">
396
+ Scenarios are the areas the Tester chose to exercise (its spec acceptance scenarios).
397
+ Outcomes and concerns are grouped under them by name.
398
+ </p>
399
+ </aside>
400
+ </div>
401
+ </div>
402
+ </div>
403
+ </Teleport>
404
+ </template>
@@ -0,0 +1,81 @@
1
+ import type {
2
+ Account,
3
+ AccountInvitation,
4
+ AccountMember,
5
+ AccountRole,
6
+ AddMemberInput,
7
+ EmailConnection,
8
+ UpdateAccountInput,
9
+ } from '~/types/domain'
10
+ import type { ApiContext } from './context'
11
+
12
+ /** Account (tenancy) management: orgs, members, invitations + the email sender. */
13
+ export function accountsApi({ http }: ApiContext) {
14
+ return {
15
+ // ---- accounts (tenancy) -----------------------------------------------
16
+ // The accounts the user can switch between (personal + orgs), org creation
17
+ // and membership management. Empty when auth is disabled (dev).
18
+ listAccounts: () => http<Account[]>('/accounts'),
19
+
20
+ createAccount: (body: { name: string; githubAccountLogin?: string }) =>
21
+ http<Account>('/accounts', { method: 'POST', body }),
22
+
23
+ updateAccount: (accountId: string, body: UpdateAccountInput) =>
24
+ http<Account>(`/accounts/${encodeURIComponent(accountId)}`, { method: 'PATCH', body }),
25
+
26
+ listAccountMembers: (accountId: string) =>
27
+ http<AccountMember[]>(`/accounts/${encodeURIComponent(accountId)}/members`),
28
+
29
+ addAccountMember: (accountId: string, body: AddMemberInput) =>
30
+ http<AccountMember>(`/accounts/${encodeURIComponent(accountId)}/members`, {
31
+ method: 'POST',
32
+ body,
33
+ }),
34
+
35
+ setMemberRoles: (accountId: string, userId: string, roles: AccountRole[]) =>
36
+ http<AccountMember>(
37
+ `/accounts/${encodeURIComponent(accountId)}/members/${encodeURIComponent(userId)}/roles`,
38
+ { method: 'PATCH', body: { roles } },
39
+ ),
40
+
41
+ // Invitations: invite teammates by email into an org account.
42
+ listInvitations: (accountId: string) =>
43
+ http<AccountInvitation[]>(`/accounts/${encodeURIComponent(accountId)}/invitations`),
44
+
45
+ createInvitation: (accountId: string, body: { email: string; roles?: AccountRole[] }) =>
46
+ http<{ invitation: AccountInvitation; acceptUrl: string | null }>(
47
+ `/accounts/${encodeURIComponent(accountId)}/invitations`,
48
+ { method: 'POST', body },
49
+ ),
50
+
51
+ revokeInvitation: (accountId: string, invitationId: string) =>
52
+ http(
53
+ `/accounts/${encodeURIComponent(accountId)}/invitations/${encodeURIComponent(invitationId)}`,
54
+ { method: 'DELETE' },
55
+ ),
56
+
57
+ // Per-account email sender (UI-onboarded): connect/inspect/disconnect/test.
58
+ getEmailConnection: (accountId: string) =>
59
+ http<{ connection: EmailConnection | null; configured: boolean }>(
60
+ `/accounts/${encodeURIComponent(accountId)}/email-connection`,
61
+ ),
62
+
63
+ connectEmail: (
64
+ accountId: string,
65
+ body: { provider: 'sendgrid' | 'resend'; apiKey: string; fromAddress: string },
66
+ ) =>
67
+ http<EmailConnection>(`/accounts/${encodeURIComponent(accountId)}/email-connection`, {
68
+ method: 'POST',
69
+ body,
70
+ }),
71
+
72
+ disconnectEmail: (accountId: string) =>
73
+ http(`/accounts/${encodeURIComponent(accountId)}/email-connection`, { method: 'DELETE' }),
74
+
75
+ testEmail: (accountId: string, to: string) =>
76
+ http<{ ok: boolean }>(`/accounts/${encodeURIComponent(accountId)}/email-connection/test`, {
77
+ method: 'POST',
78
+ body: { to },
79
+ }),
80
+ }
81
+ }
@@ -0,0 +1,45 @@
1
+ import type { AuthUser } from '~/types/domain'
2
+ import type { ApiContext } from './context'
3
+
4
+ /** Auth/session endpoints + the events-WebSocket ticket mint. */
5
+ export function authApi({ http, ws }: ApiContext) {
6
+ return {
7
+ // ---- auth -------------------------------------------------------------
8
+ getAuthConfig: () =>
9
+ http<{
10
+ enabled: boolean
11
+ providers?: { github: boolean; password: boolean; google: boolean }
12
+ /** Local-mode signals; present only when the backend is the local facade. */
13
+ localMode?: { enabled: boolean; githubPatSetupUrl?: string }
14
+ }>('/auth/config'),
15
+
16
+ getMe: () => http<{ user: AuthUser | null; enabled: boolean }>('/auth/me'),
17
+
18
+ signup: (body: { email: string; password: string; name?: string; invite?: string }) =>
19
+ http<{ token: string; user: AuthUser }>('/auth/signup', { method: 'POST', body }),
20
+
21
+ passwordLogin: (body: { email: string; password: string }) =>
22
+ http<{ token: string; user: AuthUser }>('/auth/password-login', { method: 'POST', body }),
23
+
24
+ peekInvite: (token: string) =>
25
+ http<{ valid: boolean; email?: string; accountName?: string | null }>(
26
+ `/auth/invitations/${encodeURIComponent(token)}`,
27
+ ),
28
+
29
+ acceptInvite: (token: string) =>
30
+ http<{ accountId: string }>(`/auth/invitations/${encodeURIComponent(token)}/accept`, {
31
+ method: 'POST',
32
+ }),
33
+
34
+ logout: () => http('/auth/logout', { method: 'POST' }),
35
+
36
+ // Mint a short-lived, workspace-scoped ticket for the events WebSocket. A
37
+ // browser can't set Authorization on a WS handshake, so the socket auths from
38
+ // this `?ticket=` instead of the long-lived session token. Empty string when
39
+ // auth is disabled (dev) — the handshake is open in that case.
40
+ mintEventsTicket: (workspaceId: string) =>
41
+ http<{ ticket: string; expiresInMs?: number }>(`${ws(workspaceId)}/events/ticket`, {
42
+ method: 'POST',
43
+ }),
44
+ }
45
+ }
@@ -0,0 +1,101 @@
1
+ import type { Block, BlockType, CreateTaskType, Pipeline, TaskTypeFields } from '~/types/domain'
2
+ import type { ConsensusStepConfig, StepGating } from '~/types/consensus'
3
+ import type { ApiContext, Position } from './context'
4
+
5
+ /**
6
+ * Create/update body for a pipeline. `name`+`agentKinds` required on create, all optional on
7
+ * update; the parallel arrays are aligned to `agentKinds` and persisted only when non-default.
8
+ */
9
+ interface PipelineWriteBody {
10
+ name?: string
11
+ agentKinds?: string[]
12
+ gates?: boolean[]
13
+ thresholds?: (number | null)[]
14
+ enabled?: boolean[]
15
+ consensus?: (ConsensusStepConfig | null)[]
16
+ gating?: (StepGating | null)[]
17
+ labels?: string[]
18
+ }
19
+
20
+ /** Board structure: block (frame/module/task) mutations + the pipeline library. */
21
+ export function boardApi({ http, ws }: ApiContext) {
22
+ return {
23
+ // ---- blocks -----------------------------------------------------------
24
+ addFrame: (workspaceId: string, body: { type: BlockType; position: Position }) =>
25
+ http<Block>(`${ws(workspaceId)}/blocks`, { method: 'POST', body }),
26
+
27
+ // Import an existing GitHub repo as a service frame (no bootstrap run).
28
+ addServiceFromRepo: (
29
+ workspaceId: string,
30
+ body: { repoGithubId: number; position?: Position; directory?: string; isMonorepo?: boolean },
31
+ ) => http<Block>(`${ws(workspaceId)}/blocks/from-repo`, { method: 'POST', body }),
32
+
33
+ addTask: (
34
+ workspaceId: string,
35
+ blockId: string,
36
+ body: {
37
+ title: string
38
+ description?: string
39
+ taskType?: CreateTaskType
40
+ taskTypeFields?: TaskTypeFields
41
+ mergePresetId?: string
42
+ pipelineId?: string
43
+ agentConfig?: Record<string, string>
44
+ },
45
+ ) => http<Block>(`${ws(workspaceId)}/blocks/${blockId}/tasks`, { method: 'POST', body }),
46
+
47
+ addModule: (
48
+ workspaceId: string,
49
+ blockId: string,
50
+ body: { name: string; position?: Position },
51
+ ) => http<Block>(`${ws(workspaceId)}/blocks/${blockId}/modules`, { method: 'POST', body }),
52
+
53
+ updateBlock: (workspaceId: string, blockId: string, body: Partial<Block>) =>
54
+ http<Block>(`${ws(workspaceId)}/blocks/${blockId}`, { method: 'PATCH', body }),
55
+
56
+ moveBlock: (workspaceId: string, blockId: string, body: { position: Position }) =>
57
+ http<Block>(`${ws(workspaceId)}/blocks/${blockId}/move`, { method: 'POST', body }),
58
+
59
+ reparentBlock: (
60
+ workspaceId: string,
61
+ blockId: string,
62
+ body: { parentId: string; position: Position },
63
+ ) => http<Block>(`${ws(workspaceId)}/blocks/${blockId}/reparent`, { method: 'POST', body }),
64
+
65
+ removeBlock: (workspaceId: string, blockId: string) =>
66
+ http(`${ws(workspaceId)}/blocks/${blockId}`, { method: 'DELETE' }),
67
+
68
+ toggleDependency: (workspaceId: string, blockId: string, body: { sourceId: string }) =>
69
+ http<Block>(`${ws(workspaceId)}/blocks/${blockId}/dependencies`, { method: 'POST', body }),
70
+
71
+ // ---- pipelines --------------------------------------------------------
72
+ listPipelines: (workspaceId: string) => http<Pipeline[]>(`${ws(workspaceId)}/pipelines`),
73
+
74
+ createPipeline: (workspaceId: string, body: PipelineWriteBody) =>
75
+ http<Pipeline>(`${ws(workspaceId)}/pipelines`, { method: 'POST', body }),
76
+
77
+ updatePipeline: (workspaceId: string, pipelineId: string, body: PipelineWriteBody) =>
78
+ http<Pipeline>(`${ws(workspaceId)}/pipelines/${pipelineId}`, { method: 'PATCH', body }),
79
+
80
+ clonePipeline: (workspaceId: string, pipelineId: string, body: { name?: string } = {}) =>
81
+ http<Pipeline>(`${ws(workspaceId)}/pipelines/${pipelineId}/clone`, {
82
+ method: 'POST',
83
+ body,
84
+ }),
85
+
86
+ // Organize a pipeline in the library (labels / archive). The only mutation a built-in
87
+ // accepts — it touches view metadata, not structure.
88
+ organizePipeline: (
89
+ workspaceId: string,
90
+ pipelineId: string,
91
+ body: { labels?: string[]; archived?: boolean },
92
+ ) =>
93
+ http<Pipeline>(`${ws(workspaceId)}/pipelines/${pipelineId}/organize`, {
94
+ method: 'PATCH',
95
+ body,
96
+ }),
97
+
98
+ removePipeline: (workspaceId: string, pipelineId: string) =>
99
+ http(`${ws(workspaceId)}/pipelines/${pipelineId}`, { method: 'DELETE' }),
100
+ }
101
+ }