@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,282 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Gate window — the dedicated surface for a polling gate step (`ci` / `conflicts`),
|
|
3
|
+
// opened via the universal result-view host (the same seam the test report and the
|
|
4
|
+
// requirements review use). It surfaces the gate's conclusion that the backend now
|
|
5
|
+
// persists on `step.gate`: the precheck verdict, the helper attempt budget, the gated
|
|
6
|
+
// commit, and — for CI — the failing checks behind the failure. One window serves both
|
|
7
|
+
// gates; it branches on the step's `agentKind` for the copy and the failure detail.
|
|
8
|
+
import { computed } from 'vue'
|
|
9
|
+
import { agentKindMeta } from '~/utils/catalog'
|
|
10
|
+
import type { GateStepState } from '~/types/execution'
|
|
11
|
+
import StepRestartControl from '~/components/panels/StepRestartControl.vue'
|
|
12
|
+
|
|
13
|
+
const board = useBoardStore()
|
|
14
|
+
const execution = useExecutionStore()
|
|
15
|
+
|
|
16
|
+
// Synchronous window: it reads its state straight off the execution step, so there's
|
|
17
|
+
// nothing to fetch on open (no `onOpen` loader).
|
|
18
|
+
const { open, blockId, instanceId, stepIndex, close } = useResultView('gate')
|
|
19
|
+
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
20
|
+
|
|
21
|
+
const instance = computed(() =>
|
|
22
|
+
instanceId.value === null ? null : (execution.getInstance(instanceId.value) ?? null),
|
|
23
|
+
)
|
|
24
|
+
const step = computed(() => {
|
|
25
|
+
if (instance.value === null || stepIndex.value === null) return null
|
|
26
|
+
return instance.value.steps[stepIndex.value] ?? null
|
|
27
|
+
})
|
|
28
|
+
const gate = computed<GateStepState | null>(() => step.value?.gate ?? null)
|
|
29
|
+
|
|
30
|
+
const isCi = computed(() => step.value?.agentKind === 'ci')
|
|
31
|
+
const meta = computed(() => agentKindMeta(step.value?.agentKind ?? 'ci'))
|
|
32
|
+
const helperKind = computed(() => (isCi.value ? 'ci-fixer' : 'conflict-resolver'))
|
|
33
|
+
const helperMeta = computed(() => agentKindMeta(helperKind.value))
|
|
34
|
+
|
|
35
|
+
const failingChecks = computed(() => gate.value?.failingChecks ?? [])
|
|
36
|
+
const shortSha = computed(() => (gate.value?.headSha ? gate.value.headSha.slice(0, 7) : null))
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The display status — a roll-up of the persisted gate state + the run's status, so the
|
|
40
|
+
* window reads as a conclusion rather than raw fields:
|
|
41
|
+
* - `passed` — the step finished (the precheck went green; the helper was never needed
|
|
42
|
+
* or fixed it);
|
|
43
|
+
* - `gave-up` — the run failed at this gate (attempt budget spent);
|
|
44
|
+
* - `fixing` — a helper agent is in flight on a failed precheck;
|
|
45
|
+
* - `failing` — the precheck failed and a helper is about to run;
|
|
46
|
+
* - `pending` — the provider is still computing;
|
|
47
|
+
* - `checking` — running the precheck.
|
|
48
|
+
*/
|
|
49
|
+
type GateDisplayStatus = 'passed' | 'gave-up' | 'fixing' | 'failing' | 'pending' | 'checking'
|
|
50
|
+
const status = computed<GateDisplayStatus>(() => {
|
|
51
|
+
const s = step.value
|
|
52
|
+
if (!s) return 'checking'
|
|
53
|
+
if (s.state === 'done') return 'passed'
|
|
54
|
+
if (instance.value?.status === 'failed') return 'gave-up'
|
|
55
|
+
if (gate.value?.phase === 'working') return 'fixing'
|
|
56
|
+
if (gate.value?.lastVerdict === 'fail') return 'failing'
|
|
57
|
+
if (gate.value?.lastVerdict === 'pending') return 'pending'
|
|
58
|
+
return 'checking'
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const STATUS_META: Record<
|
|
62
|
+
GateDisplayStatus,
|
|
63
|
+
{ label: string; badge: 'success' | 'warning' | 'error' | 'neutral'; icon: string; text: string }
|
|
64
|
+
> = {
|
|
65
|
+
passed: {
|
|
66
|
+
label: 'Passed',
|
|
67
|
+
badge: 'success',
|
|
68
|
+
icon: 'i-lucide-circle-check',
|
|
69
|
+
text: 'text-emerald-300',
|
|
70
|
+
},
|
|
71
|
+
'gave-up': { label: 'Gave up', badge: 'error', icon: 'i-lucide-circle-x', text: 'text-rose-300' },
|
|
72
|
+
fixing: { label: 'Fixing', badge: 'warning', icon: 'i-lucide-loader', text: 'text-amber-300' },
|
|
73
|
+
failing: {
|
|
74
|
+
label: 'Failing',
|
|
75
|
+
badge: 'error',
|
|
76
|
+
icon: 'i-lucide-circle-x',
|
|
77
|
+
text: 'text-rose-300',
|
|
78
|
+
},
|
|
79
|
+
pending: {
|
|
80
|
+
label: 'Pending',
|
|
81
|
+
badge: 'neutral',
|
|
82
|
+
icon: 'i-lucide-clock',
|
|
83
|
+
text: 'text-slate-300',
|
|
84
|
+
},
|
|
85
|
+
checking: {
|
|
86
|
+
label: 'Checking',
|
|
87
|
+
badge: 'neutral',
|
|
88
|
+
icon: 'i-lucide-loader',
|
|
89
|
+
text: 'text-slate-300',
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// The conflicts gate has no structured detail (GitHub reports mergeability as a single
|
|
94
|
+
// verdict, no file list), so the window shows the verdict + a note rather than a list.
|
|
95
|
+
const conflictVerdict = computed(() => {
|
|
96
|
+
if (status.value === 'passed') return 'Mergeable'
|
|
97
|
+
if (gate.value?.lastVerdict === 'pending') return 'Computing mergeability…'
|
|
98
|
+
if (gate.value?.lastVerdict === 'fail') return 'Conflicts with base'
|
|
99
|
+
return 'Unknown'
|
|
100
|
+
})
|
|
101
|
+
</script>
|
|
102
|
+
|
|
103
|
+
<template>
|
|
104
|
+
<Teleport to="body">
|
|
105
|
+
<div
|
|
106
|
+
v-if="open"
|
|
107
|
+
class="fixed inset-0 z-50 flex items-stretch justify-center bg-slate-950/70 backdrop-blur-sm"
|
|
108
|
+
@click.self="close"
|
|
109
|
+
>
|
|
110
|
+
<div
|
|
111
|
+
class="m-4 flex w-full max-w-3xl flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl"
|
|
112
|
+
>
|
|
113
|
+
<!-- Header -->
|
|
114
|
+
<header class="flex items-center gap-3 border-b border-slate-800 px-5 py-3">
|
|
115
|
+
<span
|
|
116
|
+
class="flex h-8 w-8 items-center justify-center rounded-lg bg-sky-500/15 text-sky-300"
|
|
117
|
+
>
|
|
118
|
+
<UIcon :name="meta.icon" class="h-4 w-4" />
|
|
119
|
+
</span>
|
|
120
|
+
<div class="min-w-0 flex-1">
|
|
121
|
+
<h2 class="truncate text-sm font-semibold text-slate-100">
|
|
122
|
+
{{ meta.label }}{{ block ? ` — ${block.title}` : '' }}
|
|
123
|
+
</h2>
|
|
124
|
+
<p class="truncate text-[11px] text-slate-400">
|
|
125
|
+
{{
|
|
126
|
+
isCi
|
|
127
|
+
? 'Gates the PR on green CI, looping the CI fixer on failure'
|
|
128
|
+
: 'Gates the PR on a clean merge, looping the resolver on conflicts'
|
|
129
|
+
}}
|
|
130
|
+
</p>
|
|
131
|
+
</div>
|
|
132
|
+
<UBadge :color="STATUS_META[status].badge" variant="subtle" size="sm">
|
|
133
|
+
{{ STATUS_META[status].label }}
|
|
134
|
+
</UBadge>
|
|
135
|
+
<StepRestartControl
|
|
136
|
+
:instance-id="instanceId"
|
|
137
|
+
:step-index="stepIndex"
|
|
138
|
+
@restarted="close"
|
|
139
|
+
/>
|
|
140
|
+
<button
|
|
141
|
+
class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
|
|
142
|
+
@click="close"
|
|
143
|
+
>
|
|
144
|
+
<UIcon name="i-lucide-x" class="h-4 w-4" />
|
|
145
|
+
</button>
|
|
146
|
+
</header>
|
|
147
|
+
|
|
148
|
+
<div class="flex min-h-0 flex-1">
|
|
149
|
+
<!-- Main: the conclusion -->
|
|
150
|
+
<div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
|
|
151
|
+
<div
|
|
152
|
+
v-if="!gate"
|
|
153
|
+
class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
|
|
154
|
+
>
|
|
155
|
+
<UIcon :name="meta.icon" class="h-8 w-8 opacity-40" />
|
|
156
|
+
<p class="text-sm">No gate activity yet.</p>
|
|
157
|
+
<p class="max-w-sm text-[11px] text-slate-500">
|
|
158
|
+
The precheck runs once the PR is open. While it polls, the step shows live state on
|
|
159
|
+
the board.
|
|
160
|
+
</p>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<template v-else>
|
|
164
|
+
<!-- Passed -->
|
|
165
|
+
<div
|
|
166
|
+
v-if="status === 'passed'"
|
|
167
|
+
class="flex items-start gap-2 rounded-lg border border-emerald-500/30 bg-emerald-500/10 px-3 py-2.5"
|
|
168
|
+
>
|
|
169
|
+
<UIcon
|
|
170
|
+
name="i-lucide-circle-check"
|
|
171
|
+
class="mt-0.5 h-4 w-4 shrink-0 text-emerald-400"
|
|
172
|
+
/>
|
|
173
|
+
<p class="text-[13px] leading-relaxed text-emerald-200">
|
|
174
|
+
{{
|
|
175
|
+
step?.output || (isCi ? 'CI is green.' : 'The PR merges cleanly with its base.')
|
|
176
|
+
}}
|
|
177
|
+
</p>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<!-- CI: failing checks -->
|
|
181
|
+
<template v-else-if="isCi">
|
|
182
|
+
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
183
|
+
Failing checks
|
|
184
|
+
</h3>
|
|
185
|
+
<ul v-if="failingChecks.length" class="space-y-1">
|
|
186
|
+
<li
|
|
187
|
+
v-for="(c, i) in failingChecks"
|
|
188
|
+
:key="`${c.name}-${i}`"
|
|
189
|
+
class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-950/40 px-3 py-1.5"
|
|
190
|
+
>
|
|
191
|
+
<UIcon name="i-lucide-circle-x" class="h-3.5 w-3.5 shrink-0 text-rose-400" />
|
|
192
|
+
<span class="min-w-0 flex-1 truncate text-[13px] text-slate-200">{{
|
|
193
|
+
c.name
|
|
194
|
+
}}</span>
|
|
195
|
+
<span class="shrink-0 text-[11px] uppercase text-rose-300">
|
|
196
|
+
{{ c.conclusion ?? 'failure' }}
|
|
197
|
+
</span>
|
|
198
|
+
</li>
|
|
199
|
+
</ul>
|
|
200
|
+
<p v-else class="text-[13px] leading-relaxed text-slate-300">
|
|
201
|
+
{{ gate.lastFailureSummary || 'CI has not reported a failure on this commit.' }}
|
|
202
|
+
</p>
|
|
203
|
+
</template>
|
|
204
|
+
|
|
205
|
+
<!-- Conflicts: verdict + note (no file-level detail from GitHub) -->
|
|
206
|
+
<template v-else>
|
|
207
|
+
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
208
|
+
Mergeability
|
|
209
|
+
</h3>
|
|
210
|
+
<div
|
|
211
|
+
class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
|
|
212
|
+
>
|
|
213
|
+
<UIcon
|
|
214
|
+
:name="STATUS_META[status].icon"
|
|
215
|
+
class="h-4 w-4 shrink-0"
|
|
216
|
+
:class="STATUS_META[status].text"
|
|
217
|
+
/>
|
|
218
|
+
<span class="text-[13px] text-slate-200">{{ conflictVerdict }}</span>
|
|
219
|
+
</div>
|
|
220
|
+
<p class="mt-2 text-[11px] leading-relaxed text-slate-500">
|
|
221
|
+
GitHub reports mergeability as a single verdict, so there's no file-level conflict
|
|
222
|
+
list here. The conflict resolver inspects the branch directly.
|
|
223
|
+
</p>
|
|
224
|
+
</template>
|
|
225
|
+
</template>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<!-- Sidebar: gate state -->
|
|
229
|
+
<aside
|
|
230
|
+
class="hidden w-56 shrink-0 flex-col gap-4 border-l border-slate-800 bg-slate-900/50 px-4 py-4 lg:flex"
|
|
231
|
+
>
|
|
232
|
+
<div v-if="gate">
|
|
233
|
+
<h4 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
234
|
+
State
|
|
235
|
+
</h4>
|
|
236
|
+
<div class="flex items-center gap-2 text-[13px]">
|
|
237
|
+
<UIcon
|
|
238
|
+
:name="STATUS_META[status].icon"
|
|
239
|
+
class="h-4 w-4"
|
|
240
|
+
:class="STATUS_META[status].text"
|
|
241
|
+
/>
|
|
242
|
+
<span :class="STATUS_META[status].text">{{ STATUS_META[status].label }}</span>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<div v-if="gate">
|
|
247
|
+
<h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
248
|
+
{{ helperMeta.label }}
|
|
249
|
+
</h4>
|
|
250
|
+
<p class="text-[12px] text-slate-300">
|
|
251
|
+
{{ gate.attempts }}/{{ gate.maxAttempts }} attempt{{
|
|
252
|
+
gate.maxAttempts === 1 ? '' : 's'
|
|
253
|
+
}}
|
|
254
|
+
<template v-if="gate.phase === 'working'"> · running…</template>
|
|
255
|
+
<template v-else-if="gate.attempts === 0"> · not needed yet</template>
|
|
256
|
+
</p>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<div v-if="shortSha">
|
|
260
|
+
<h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
261
|
+
Gated commit
|
|
262
|
+
</h4>
|
|
263
|
+
<p class="font-mono text-[12px] text-slate-300">{{ shortSha }}</p>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div v-if="step?.model">
|
|
267
|
+
<h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
268
|
+
Model
|
|
269
|
+
</h4>
|
|
270
|
+
<p class="break-all text-[12px] text-slate-300">{{ step.model }}</p>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<p class="mt-auto text-[10px] leading-relaxed text-slate-600">
|
|
274
|
+
A gate runs a programmatic precheck and only spins up the
|
|
275
|
+
{{ helperMeta.label }} when it fails — a green check advances with nothing spun up.
|
|
276
|
+
</p>
|
|
277
|
+
</aside>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</Teleport>
|
|
282
|
+
</template>
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Add a board service backed by an EXISTING GitHub repository — no bootstrap
|
|
3
|
+
// run. Unlike the bootstrap modal (which creates a repo and has an agent adapt
|
|
4
|
+
// it in a container), this just links a repo the App can access to a fresh,
|
|
5
|
+
// `ready` service frame. The workspace need not track the repo yet: the backend
|
|
6
|
+
// links + syncs it on import. If the App can't see the wanted repo, the user
|
|
7
|
+
// grants it access from here, then refreshes the list.
|
|
8
|
+
//
|
|
9
|
+
// MONOREPO support: a repo flagged a monorepo can back SEVERAL services, each
|
|
10
|
+
// pinned to a subdirectory. When the selected repo is a monorepo, the user
|
|
11
|
+
// browses its tree and picks the service's directory before adding (and may add
|
|
12
|
+
// more than one, a subset of the repo's services).
|
|
13
|
+
import GitHubConnect from '~/components/github/GitHubConnect.vue'
|
|
14
|
+
import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
|
|
15
|
+
import ServiceTestConfig from '~/components/panels/inspector/ServiceTestConfig.vue'
|
|
16
|
+
import ServiceFragments from '~/components/panels/inspector/ServiceFragments.vue'
|
|
17
|
+
|
|
18
|
+
const ui = useUiStore()
|
|
19
|
+
const github = useGitHubStore()
|
|
20
|
+
const board = useBoardStore()
|
|
21
|
+
const toast = useToast()
|
|
22
|
+
|
|
23
|
+
const open = computed({
|
|
24
|
+
get: () => ui.addServiceOpen,
|
|
25
|
+
set: (v: boolean) => {
|
|
26
|
+
if (!v) ui.closeAddService()
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const selectedRepoId = ref<number | undefined>(undefined)
|
|
31
|
+
const adding = ref(false)
|
|
32
|
+
|
|
33
|
+
async function loadRepos() {
|
|
34
|
+
try {
|
|
35
|
+
await github.probe()
|
|
36
|
+
if (github.connected) await Promise.all([github.load(), github.loadAvailableRepos()])
|
|
37
|
+
} catch {
|
|
38
|
+
// Integration off / unreachable → the picker stays empty, GitHubConnect shows.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// On open: ensure we know the connection + which repos the App can access, and
|
|
43
|
+
// the workspace's already-tracked repos (to flag ones already on the board).
|
|
44
|
+
watch(open, (isOpen) => {
|
|
45
|
+
if (!isOpen) return
|
|
46
|
+
resetSelection()
|
|
47
|
+
void loadRepos()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// If the user connects from inside the modal (the not-connected prompt), pull the
|
|
51
|
+
// repo list as soon as the connection is bound.
|
|
52
|
+
watch(
|
|
53
|
+
() => github.connected,
|
|
54
|
+
(isConnected) => {
|
|
55
|
+
if (isConnected && open.value) void loadRepos()
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
// The integration is on but this workspace isn't bound yet — connect first.
|
|
60
|
+
const needsGitHub = computed(() => github.available === true && !github.connected)
|
|
61
|
+
|
|
62
|
+
// Repos already backing a board service can't be added again — UNLESS they're a
|
|
63
|
+
// monorepo, which can host several services (each at its own subdirectory).
|
|
64
|
+
const onBoardIds = computed(
|
|
65
|
+
() => new Set(github.repos.filter((r) => r.blockId).map((r) => r.githubId)),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
const repoItems = computed(() =>
|
|
69
|
+
github.availableRepos.map((r) => {
|
|
70
|
+
const onBoard = onBoardIds.value.has(r.githubId) && !r.isMonorepo
|
|
71
|
+
const mono = r.isMonorepo ? ' · monorepo' : ''
|
|
72
|
+
return {
|
|
73
|
+
label: `${r.owner}/${r.name}${r.private ? ' (private)' : ''}${mono}${onBoard ? ' · already on board' : ''}`,
|
|
74
|
+
// Searched on (lowercased once) — the owner/name, so the filter matches either.
|
|
75
|
+
search: `${r.owner}/${r.name}`.toLowerCase(),
|
|
76
|
+
value: r.githubId,
|
|
77
|
+
disabled: onBoard,
|
|
78
|
+
}
|
|
79
|
+
}),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
// The PAT (or a wide App install) can expose hundreds of repos, too many for a plain
|
|
83
|
+
// dropdown — filter by owner/name. The currently selected repo is always kept in the
|
|
84
|
+
// list so a selection doesn't vanish when the query no longer matches it.
|
|
85
|
+
const repoSearch = ref('')
|
|
86
|
+
const filteredRepoItems = computed(() => {
|
|
87
|
+
const q = repoSearch.value.trim().toLowerCase()
|
|
88
|
+
if (!q) return repoItems.value
|
|
89
|
+
return repoItems.value.filter((r) => r.search.includes(q) || r.value === selectedRepoId.value)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const hasRepos = computed(() => github.availableRepos.length > 0)
|
|
93
|
+
const selectedRepo = computed(() =>
|
|
94
|
+
github.availableRepos.find((r) => r.githubId === selectedRepoId.value),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
// ---- monorepo flag + directory picker ------------------------------------
|
|
98
|
+
|
|
99
|
+
// The monorepo flag is MODAL-LOCAL state, sent as part of the add-service request
|
|
100
|
+
// rather than persisted up-front on a toggle: there's no need to round-trip a PATCH
|
|
101
|
+
// before adding (browsing the tree needs only the repo id, and the backend flags the
|
|
102
|
+
// repo + requires a directory when it creates the service). A repo already flagged a
|
|
103
|
+
// monorepo (it backs other services) seeds the toggle on when selected.
|
|
104
|
+
const isMonorepo = ref(false)
|
|
105
|
+
const selectedDirectory = ref<string | undefined>(undefined)
|
|
106
|
+
|
|
107
|
+
function toggleMonorepo(value: boolean) {
|
|
108
|
+
isMonorepo.value = value
|
|
109
|
+
selectedDirectory.value = undefined
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// On repo change, seed the toggle from the repo's persisted flag and clear the rest.
|
|
113
|
+
watch(selectedRepoId, () => {
|
|
114
|
+
isMonorepo.value = selectedRepo.value?.isMonorepo === true
|
|
115
|
+
selectedDirectory.value = undefined
|
|
116
|
+
configuredBlockId.value = undefined
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
function resetSelection() {
|
|
120
|
+
selectedRepoId.value = undefined
|
|
121
|
+
selectedDirectory.value = undefined
|
|
122
|
+
isMonorepo.value = false
|
|
123
|
+
configuredBlockId.value = undefined
|
|
124
|
+
repoSearch.value = ''
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// The App's installation settings page — where the user grants it access to a
|
|
128
|
+
// repo it can't see yet (mirrors the bootstrap modal's "grant access" link).
|
|
129
|
+
const manageInstallUrl = computed(() => {
|
|
130
|
+
const conn = github.connection
|
|
131
|
+
if (!conn) return undefined
|
|
132
|
+
return conn.targetType === 'Organization'
|
|
133
|
+
? `https://github.com/organizations/${conn.accountLogin}/settings/installations/${conn.installationId}`
|
|
134
|
+
: `https://github.com/settings/installations/${conn.installationId}`
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
function openManageInstall() {
|
|
138
|
+
if (manageInstallUrl.value) window.open(manageInstallUrl.value, '_blank', 'noopener')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The just-added service, kept on the board store so the user can configure it (test
|
|
142
|
+
// infra + fragments) right here — the same controls as the inspector. A monorepo can
|
|
143
|
+
// host several services, so adding another keeps the modal open; a whole-repo service
|
|
144
|
+
// can only be added once (its repo is then on the board).
|
|
145
|
+
const configuredBlockId = ref<string | undefined>(undefined)
|
|
146
|
+
const configuredDirectory = ref<string | undefined>(undefined)
|
|
147
|
+
const configuredBlock = computed(() =>
|
|
148
|
+
configuredBlockId.value ? board.getBlock(configuredBlockId.value) : undefined,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
// A monorepo service needs a chosen directory; a whole-repo service can be added once.
|
|
152
|
+
const canAdd = computed(
|
|
153
|
+
() =>
|
|
154
|
+
!needsGitHub.value &&
|
|
155
|
+
selectedRepoId.value !== undefined &&
|
|
156
|
+
(isMonorepo.value ? !!selectedDirectory.value : !configuredBlockId.value),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
async function add() {
|
|
160
|
+
if (!canAdd.value || selectedRepoId.value === undefined) return
|
|
161
|
+
adding.value = true
|
|
162
|
+
try {
|
|
163
|
+
const block = await board.addServiceFromRepo(selectedRepoId.value, {
|
|
164
|
+
directory: isMonorepo.value ? selectedDirectory.value : undefined,
|
|
165
|
+
isMonorepo: isMonorepo.value,
|
|
166
|
+
})
|
|
167
|
+
// Refresh the projection so the new repo↔block link is reflected locally.
|
|
168
|
+
await github.load()
|
|
169
|
+
configuredBlockId.value = block.id
|
|
170
|
+
configuredDirectory.value = isMonorepo.value ? selectedDirectory.value : undefined
|
|
171
|
+
toast.add({
|
|
172
|
+
title: 'Service added',
|
|
173
|
+
description: `${block.title} is on the board — configure it below.`,
|
|
174
|
+
icon: 'i-lucide-check',
|
|
175
|
+
color: 'success',
|
|
176
|
+
})
|
|
177
|
+
// Ready to pick another monorepo service (the just-added directory is taken).
|
|
178
|
+
selectedDirectory.value = undefined
|
|
179
|
+
} catch (e) {
|
|
180
|
+
toast.add({
|
|
181
|
+
title: 'Could not add service',
|
|
182
|
+
description: e instanceof Error ? e.message : String(e),
|
|
183
|
+
icon: 'i-lucide-triangle-alert',
|
|
184
|
+
color: 'error',
|
|
185
|
+
})
|
|
186
|
+
} finally {
|
|
187
|
+
adding.value = false
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function done() {
|
|
192
|
+
ui.closeAddService()
|
|
193
|
+
}
|
|
194
|
+
</script>
|
|
195
|
+
|
|
196
|
+
<template>
|
|
197
|
+
<UModal v-model:open="open" title="Add a service from a repository" :ui="{ content: 'max-w-xl' }">
|
|
198
|
+
<template #body>
|
|
199
|
+
<div class="space-y-6">
|
|
200
|
+
<p class="text-sm text-slate-400">
|
|
201
|
+
Pick an existing GitHub repository to add as a board service. No bootstrapping — the repo
|
|
202
|
+
is linked to a new service frame as-is, and tasks you run on it target that repo.
|
|
203
|
+
</p>
|
|
204
|
+
|
|
205
|
+
<!-- not connected: linking a repo needs the App bound to this workspace -->
|
|
206
|
+
<div
|
|
207
|
+
v-if="needsGitHub"
|
|
208
|
+
class="space-y-3 rounded-md border border-amber-500/30 bg-amber-500/5 p-3"
|
|
209
|
+
>
|
|
210
|
+
<div class="flex items-start gap-2">
|
|
211
|
+
<UIcon name="i-lucide-plug-zap" class="mt-0.5 h-4 w-4 shrink-0 text-amber-400" />
|
|
212
|
+
<p class="text-sm text-amber-200/90">
|
|
213
|
+
Connect this workspace to GitHub first. Link an installation the App is already on, or
|
|
214
|
+
install it.
|
|
215
|
+
</p>
|
|
216
|
+
</div>
|
|
217
|
+
<GitHubConnect />
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<template v-else>
|
|
221
|
+
<UFormField
|
|
222
|
+
label="Repository"
|
|
223
|
+
description="Repositories the GitHub App can access. Don't see yours? Grant the App access below, then refresh."
|
|
224
|
+
required
|
|
225
|
+
>
|
|
226
|
+
<div v-if="!hasRepos" class="text-sm text-slate-400">
|
|
227
|
+
No repositories available yet — grant the App access to one below, then refresh.
|
|
228
|
+
</div>
|
|
229
|
+
<div v-else class="space-y-1.5">
|
|
230
|
+
<UInput
|
|
231
|
+
v-model="repoSearch"
|
|
232
|
+
icon="i-lucide-search"
|
|
233
|
+
placeholder="Filter by owner/name…"
|
|
234
|
+
class="w-full"
|
|
235
|
+
:ui="{ trailing: 'pe-1' }"
|
|
236
|
+
>
|
|
237
|
+
<template v-if="repoSearch" #trailing>
|
|
238
|
+
<UButton
|
|
239
|
+
color="neutral"
|
|
240
|
+
variant="link"
|
|
241
|
+
size="sm"
|
|
242
|
+
icon="i-lucide-x"
|
|
243
|
+
aria-label="Clear filter"
|
|
244
|
+
@click="repoSearch = ''"
|
|
245
|
+
/>
|
|
246
|
+
</template>
|
|
247
|
+
</UInput>
|
|
248
|
+
<USelect
|
|
249
|
+
v-model="selectedRepoId"
|
|
250
|
+
:items="filteredRepoItems"
|
|
251
|
+
placeholder="Choose a repository"
|
|
252
|
+
class="w-full"
|
|
253
|
+
/>
|
|
254
|
+
<p class="text-xs text-slate-500">
|
|
255
|
+
Showing {{ filteredRepoItems.length }} of {{ repoItems.length }} repositories.
|
|
256
|
+
</p>
|
|
257
|
+
</div>
|
|
258
|
+
</UFormField>
|
|
259
|
+
|
|
260
|
+
<!-- monorepo handling: flag + directory picker -->
|
|
261
|
+
<div v-if="selectedRepoId !== undefined" class="space-y-3">
|
|
262
|
+
<USwitch
|
|
263
|
+
:model-value="isMonorepo"
|
|
264
|
+
label="This is a monorepo (hosts more than one service)"
|
|
265
|
+
description="Add several services from one repo, each pinned to a subdirectory."
|
|
266
|
+
@update:model-value="toggleMonorepo"
|
|
267
|
+
/>
|
|
268
|
+
|
|
269
|
+
<div
|
|
270
|
+
v-if="isMonorepo"
|
|
271
|
+
class="rounded-md border border-slate-700/60 bg-slate-900/40 p-3"
|
|
272
|
+
>
|
|
273
|
+
<p class="mb-2 text-xs text-slate-400">
|
|
274
|
+
Browse the repository and pick the directory of the service you want to add. Agents
|
|
275
|
+
working on this service will run within that subdirectory.
|
|
276
|
+
</p>
|
|
277
|
+
<RepoTreeBrowser
|
|
278
|
+
v-model="selectedDirectory"
|
|
279
|
+
:repo-github-id="selectedRepoId!"
|
|
280
|
+
mode="dir"
|
|
281
|
+
/>
|
|
282
|
+
<p class="mt-2 truncate text-xs text-slate-400">
|
|
283
|
+
<template v-if="selectedDirectory">
|
|
284
|
+
Service directory:
|
|
285
|
+
<code class="text-slate-200">{{ selectedDirectory }}</code>
|
|
286
|
+
</template>
|
|
287
|
+
<template v-else>No directory selected yet.</template>
|
|
288
|
+
</p>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
<!-- just-added service: configure it with the same controls as the inspector -->
|
|
293
|
+
<div
|
|
294
|
+
v-if="configuredBlock"
|
|
295
|
+
class="space-y-4 rounded-md border border-emerald-900/50 bg-emerald-950/20 p-3"
|
|
296
|
+
>
|
|
297
|
+
<div
|
|
298
|
+
class="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-emerald-400"
|
|
299
|
+
>
|
|
300
|
+
<UIcon name="i-lucide-check" class="h-3.5 w-3.5" />
|
|
301
|
+
{{ configuredBlock.title }} added — configure it
|
|
302
|
+
</div>
|
|
303
|
+
<ServiceTestConfig
|
|
304
|
+
:block="configuredBlock"
|
|
305
|
+
:repo="{ githubId: selectedRepoId!, directory: configuredDirectory }"
|
|
306
|
+
/>
|
|
307
|
+
<ServiceFragments :block="configuredBlock" />
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
311
|
+
<UButton
|
|
312
|
+
v-if="manageInstallUrl"
|
|
313
|
+
color="neutral"
|
|
314
|
+
variant="subtle"
|
|
315
|
+
size="sm"
|
|
316
|
+
icon="i-lucide-shield-check"
|
|
317
|
+
trailing-icon="i-lucide-external-link"
|
|
318
|
+
title="Open the App's installation settings to grant it access to a repository"
|
|
319
|
+
@click="openManageInstall"
|
|
320
|
+
>
|
|
321
|
+
Grant the App access to a repo
|
|
322
|
+
</UButton>
|
|
323
|
+
<UButton
|
|
324
|
+
color="neutral"
|
|
325
|
+
variant="ghost"
|
|
326
|
+
size="sm"
|
|
327
|
+
icon="i-lucide-refresh-cw"
|
|
328
|
+
:loading="github.loadingAvailable"
|
|
329
|
+
@click="github.loadAvailableRepos()"
|
|
330
|
+
>
|
|
331
|
+
Refresh list
|
|
332
|
+
</UButton>
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
<div class="flex justify-end gap-2">
|
|
336
|
+
<UButton v-if="configuredBlock" color="neutral" variant="soft" size="sm" @click="done">
|
|
337
|
+
Done
|
|
338
|
+
</UButton>
|
|
339
|
+
<UButton
|
|
340
|
+
v-if="!configuredBlock || isMonorepo"
|
|
341
|
+
color="primary"
|
|
342
|
+
icon="i-lucide-plus"
|
|
343
|
+
:loading="adding"
|
|
344
|
+
:disabled="!canAdd"
|
|
345
|
+
@click="add"
|
|
346
|
+
>
|
|
347
|
+
{{ configuredBlock && isMonorepo ? 'Add another service' : 'Add service' }}
|
|
348
|
+
</UButton>
|
|
349
|
+
</div>
|
|
350
|
+
</template>
|
|
351
|
+
</div>
|
|
352
|
+
</template>
|
|
353
|
+
</UModal>
|
|
354
|
+
</template>
|