@cat-factory/app 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/app/app.config.ts +8 -0
- package/app/app.vue +11 -0
- package/app/assets/css/main.css +100 -0
- package/app/components/auth/AuthGate.vue +24 -0
- package/app/components/auth/LoginScreen.vue +143 -0
- package/app/components/auth/UserMenu.vue +39 -0
- package/app/components/board/AddTaskModal.vue +444 -0
- package/app/components/board/AgentFailureCard.vue +97 -0
- package/app/components/board/AgentStopButton.vue +61 -0
- package/app/components/board/BoardCanvas.vue +183 -0
- package/app/components/board/ContextPicker.vue +367 -0
- package/app/components/board/RecurringPipelineModal.vue +219 -0
- package/app/components/board/TaskDependencyEdges.vue +132 -0
- package/app/components/board/nodes/AgentChip.vue +59 -0
- package/app/components/board/nodes/BlockNode.vue +433 -0
- package/app/components/board/nodes/DecisionBadge.vue +27 -0
- package/app/components/board/nodes/DraggableTask.vue +48 -0
- package/app/components/board/nodes/ModuleFrame.vue +97 -0
- package/app/components/board/nodes/TaskCard.vue +359 -0
- package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
- package/app/components/bootstrap/BootstrapModal.vue +665 -0
- package/app/components/clarity/ClarityReviewWindow.vue +611 -0
- package/app/components/consensus/ConsensusSessionWindow.vue +210 -0
- package/app/components/documents/DocumentImportModal.vue +161 -0
- package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
- package/app/components/documents/SpawnPreviewModal.vue +161 -0
- package/app/components/documents/TaskContextDocs.vue +83 -0
- package/app/components/focus/BlockFocusView.vue +171 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
- package/app/components/gates/GateResultView.vue +282 -0
- package/app/components/github/AddServiceFromRepoModal.vue +354 -0
- package/app/components/github/GitHubConnect.vue +183 -0
- package/app/components/github/GitHubOnboarding.vue +45 -0
- package/app/components/github/GitHubPanel.vue +584 -0
- package/app/components/github/RepoTreeBrowser.vue +171 -0
- package/app/components/layout/AccountTeamSettings.vue +237 -0
- package/app/components/layout/BoardSwitcher.vue +280 -0
- package/app/components/layout/BoardToolbar.vue +156 -0
- package/app/components/layout/CommandBar.vue +336 -0
- package/app/components/layout/GitHubPatBanner.vue +73 -0
- package/app/components/layout/NotificationsInbox.vue +175 -0
- package/app/components/layout/SideBar.vue +314 -0
- package/app/components/layout/SpendWarningBanner.vue +107 -0
- package/app/components/observability/StepMetricsBar.vue +102 -0
- package/app/components/palettes/AgentPalette.vue +86 -0
- package/app/components/panels/AgentStepDetail.vue +737 -0
- package/app/components/panels/DecisionModal.vue +71 -0
- package/app/components/panels/InspectorPanel.vue +465 -0
- package/app/components/panels/ObservabilityPanel.vue +351 -0
- package/app/components/panels/StepMetadataCard.vue +253 -0
- package/app/components/panels/StepRestartControl.vue +90 -0
- package/app/components/panels/StepResultViewHost.vue +40 -0
- package/app/components/panels/StepTestReport.vue +84 -0
- package/app/components/panels/inspector/ContainerSummary.vue +74 -0
- package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
- package/app/components/panels/inspector/ServiceFragments.vue +82 -0
- package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
- package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
- package/app/components/panels/inspector/TaskDependencies.vue +70 -0
- package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
- package/app/components/panels/inspector/TaskExecution.vue +364 -0
- package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
- package/app/components/panels/inspector/TaskStructure.vue +96 -0
- package/app/components/pipeline/AgentKindIcon.vue +30 -0
- package/app/components/pipeline/IterationCapPrompt.vue +70 -0
- package/app/components/pipeline/PipelineBuilder.vue +817 -0
- package/app/components/pipeline/PipelineProgress.vue +484 -0
- package/app/components/providers/ApiKeysSection.vue +273 -0
- package/app/components/providers/PersonalCredentialModal.vue +128 -0
- package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
- package/app/components/providers/VendorCredentialsModal.vue +197 -0
- package/app/components/recurring/RecurrenceEditor.vue +124 -0
- package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
- package/app/components/settings/DatadogPanel.vue +213 -0
- package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
- package/app/components/settings/MergeThresholdsPanel.vue +378 -0
- package/app/components/settings/ModelDefaultsPanel.vue +250 -0
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
- package/app/components/slack/SlackPanel.vue +299 -0
- package/app/components/tasks/TaskContextIssues.vue +88 -0
- package/app/components/tasks/TaskImportModal.vue +207 -0
- package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
- package/app/components/testing/TestReportWindow.vue +404 -0
- package/app/composables/api/accounts.ts +81 -0
- package/app/composables/api/auth.ts +45 -0
- package/app/composables/api/board.ts +101 -0
- package/app/composables/api/bootstrap.ts +62 -0
- package/app/composables/api/context.ts +25 -0
- package/app/composables/api/documents.ts +74 -0
- package/app/composables/api/execution.ts +127 -0
- package/app/composables/api/fragments.ts +71 -0
- package/app/composables/api/github.ts +131 -0
- package/app/composables/api/models.ts +127 -0
- package/app/composables/api/notifications.ts +23 -0
- package/app/composables/api/presets.ts +29 -0
- package/app/composables/api/recurring.ts +68 -0
- package/app/composables/api/releaseHealth.ts +43 -0
- package/app/composables/api/reviews.ts +146 -0
- package/app/composables/api/slack.ts +54 -0
- package/app/composables/api/tasks.ts +72 -0
- package/app/composables/api/workspaces.ts +36 -0
- package/app/composables/useApi.ts +89 -0
- package/app/composables/useBlockDrag.ts +90 -0
- package/app/composables/useBlockQueries.ts +154 -0
- package/app/composables/useBoardFlow.ts +11 -0
- package/app/composables/useContextLinking.ts +65 -0
- package/app/composables/useDepLabels.ts +26 -0
- package/app/composables/useFrameResize.ts +54 -0
- package/app/composables/useResultView.ts +48 -0
- package/app/composables/useReviewStage.ts +40 -0
- package/app/composables/useSemanticZoom.ts +31 -0
- package/app/composables/useStepApproval.ts +233 -0
- package/app/composables/useStepProse.ts +78 -0
- package/app/composables/useStepTimer.ts +63 -0
- package/app/composables/useTaskExpansion.ts +92 -0
- package/app/composables/useWorkspaceStream.ts +155 -0
- package/app/docs/architecture.md +31 -0
- package/app/pages/index.vue +141 -0
- package/app/stores/accounts.ts +152 -0
- package/app/stores/agentConfig.ts +35 -0
- package/app/stores/agentRuns.ts +122 -0
- package/app/stores/agents.ts +40 -0
- package/app/stores/apiKeys.ts +108 -0
- package/app/stores/auth.ts +166 -0
- package/app/stores/board.spec.ts +205 -0
- package/app/stores/board.ts +286 -0
- package/app/stores/bootstrap.ts +97 -0
- package/app/stores/clarity.ts +196 -0
- package/app/stores/consensus.ts +60 -0
- package/app/stores/documents.ts +176 -0
- package/app/stores/execution.ts +273 -0
- package/app/stores/fragmentLibrary.ts +147 -0
- package/app/stores/fragments.ts +40 -0
- package/app/stores/github.ts +305 -0
- package/app/stores/localModels.ts +51 -0
- package/app/stores/mergePresets.ts +58 -0
- package/app/stores/modelDefaults.ts +76 -0
- package/app/stores/models.ts +134 -0
- package/app/stores/notifications.ts +70 -0
- package/app/stores/observability.ts +144 -0
- package/app/stores/personalSubscriptions.ts +215 -0
- package/app/stores/pipelines.ts +327 -0
- package/app/stores/recurringPipelines.ts +112 -0
- package/app/stores/releaseHealth.ts +75 -0
- package/app/stores/requirements.spec.ts +94 -0
- package/app/stores/requirements.ts +208 -0
- package/app/stores/serviceFragmentDefaults.ts +29 -0
- package/app/stores/services.ts +87 -0
- package/app/stores/slack.ts +142 -0
- package/app/stores/taskExpansion.ts +36 -0
- package/app/stores/tasks.spec.ts +71 -0
- package/app/stores/tasks.ts +176 -0
- package/app/stores/tracker.ts +27 -0
- package/app/stores/ui.ts +434 -0
- package/app/stores/vendorCredentials.ts +54 -0
- package/app/stores/workspace.ts +215 -0
- package/app/stores/workspaceSettings.ts +36 -0
- package/app/types/accounts.ts +77 -0
- package/app/types/bootstrap.ts +83 -0
- package/app/types/clarity.ts +59 -0
- package/app/types/consensus.ts +91 -0
- package/app/types/documents.ts +104 -0
- package/app/types/domain.ts +495 -0
- package/app/types/execution.ts +383 -0
- package/app/types/fragments.ts +72 -0
- package/app/types/github.ts +173 -0
- package/app/types/localModels.ts +73 -0
- package/app/types/merge.ts +71 -0
- package/app/types/models.ts +157 -0
- package/app/types/notifications.ts +74 -0
- package/app/types/recurring.ts +69 -0
- package/app/types/releaseHealth.ts +31 -0
- package/app/types/requirements.ts +61 -0
- package/app/types/services.ts +27 -0
- package/app/types/slack.ts +57 -0
- package/app/types/tasks.ts +82 -0
- package/app/types/tracker.ts +18 -0
- package/app/utils/agentOutput.spec.ts +128 -0
- package/app/utils/agentOutput.ts +173 -0
- package/app/utils/catalog.spec.ts +112 -0
- package/app/utils/catalog.ts +455 -0
- package/app/utils/dnd.ts +29 -0
- package/app/utils/observability.ts +52 -0
- package/app/utils/pipelineRender.ts +151 -0
- package/nuxt.config.ts +55 -0
- package/package.json +45 -0
|
@@ -0,0 +1,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 & 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
|
+
}
|