@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,364 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Block } from '~/types/domain'
|
|
3
|
+
import { agentKindMeta } from '~/utils/catalog'
|
|
4
|
+
import { gateCompanionFor, COMPANION_STATE_META, isCompanionKind } from '~/utils/pipelineRender'
|
|
5
|
+
import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{ block: Block }>()
|
|
8
|
+
|
|
9
|
+
const execution = useExecutionStore()
|
|
10
|
+
const agentRuns = useAgentRunsStore()
|
|
11
|
+
const ui = useUiStore()
|
|
12
|
+
const models = useModelsStore()
|
|
13
|
+
const reviews = useReviewStage()
|
|
14
|
+
|
|
15
|
+
// The async stage this task's iterative reviewer gate (requirements-review / clarity-review)
|
|
16
|
+
// is mid-cycle in (folding the answers, then re-reviewing), or null. While set, the gate is
|
|
17
|
+
// doing background work and needs NO human, so its "Review" button is replaced by a working
|
|
18
|
+
// indicator.
|
|
19
|
+
const reviewStage = computed(() => reviews.stageForBlock(props.block.id))
|
|
20
|
+
const reviewStageLabel = computed(() =>
|
|
21
|
+
reviewStage.value === 'incorporating'
|
|
22
|
+
? 'Incorporating…'
|
|
23
|
+
: reviewStage.value === 'reviewing'
|
|
24
|
+
? 'Re-reviewing…'
|
|
25
|
+
: null,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const instance = computed(() => execution.getInstance(props.block.executionId))
|
|
29
|
+
// A failed run is no longer executing: a step left mid-flight must stop showing
|
|
30
|
+
// its live "Spinning up…" phase (the shared failure banner renders below).
|
|
31
|
+
const runFailed = computed(() => instance.value?.status === 'failed')
|
|
32
|
+
|
|
33
|
+
// A failed pipeline run surfaces the shared failure banner + retry — the
|
|
34
|
+
// execution failure surface that the old `pr_ready` flip used to hide.
|
|
35
|
+
const failedRun = computed(() => {
|
|
36
|
+
const run = agentRuns.byBlock[props.block.id]
|
|
37
|
+
return run && run.status === 'failed' ? run : null
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const pr = computed(() => props.block.pullRequest)
|
|
41
|
+
/** A PR is merged once the block is `done`; otherwise it is open awaiting merge. */
|
|
42
|
+
const prMerged = computed(() => props.block.status === 'done')
|
|
43
|
+
const prLabel = computed(() => {
|
|
44
|
+
const number = pr.value?.number
|
|
45
|
+
return number ? `PR #${number}` : 'Pull request'
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const stepLabel: Record<string, string> = {
|
|
49
|
+
pending: 'Pending',
|
|
50
|
+
working: 'Working',
|
|
51
|
+
waiting_decision: 'Needs decision',
|
|
52
|
+
done: 'Done',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A step left mid-flight (`working`) on a failed run gave up — not still working. */
|
|
56
|
+
function stepFailed(s: { state: string }) {
|
|
57
|
+
return runFailed.value && s.state === 'working'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A gated step parked for approval reads "Needs approval", not "Needs decision". */
|
|
61
|
+
function labelForStep(s: {
|
|
62
|
+
state: string
|
|
63
|
+
agentKind?: string
|
|
64
|
+
approval?: { status: string } | null
|
|
65
|
+
companion?: { exceeded?: boolean } | null
|
|
66
|
+
startingContainer?: boolean
|
|
67
|
+
}) {
|
|
68
|
+
// A step left mid-flight on a failed run reads "Failed", not the misleading "Working".
|
|
69
|
+
if (stepFailed(s)) return 'Failed'
|
|
70
|
+
// A reviewer gate mid-cycle reads its working stage, not "Needs approval".
|
|
71
|
+
if (reviews.isBackground(s.agentKind, props.block.id) && reviewStageLabel.value)
|
|
72
|
+
return reviewStageLabel.value
|
|
73
|
+
// A companion that spent its rework budget needs a decision, not an approval.
|
|
74
|
+
if (s.approval?.status === 'pending' && s.companion?.exceeded) return 'Needs decision'
|
|
75
|
+
if (s.approval?.status === 'pending') return 'Needs approval'
|
|
76
|
+
// A container-backed step whose container is still cold-booting (only while the
|
|
77
|
+
// run is live — a failed run's mid-flight step is no longer spinning up).
|
|
78
|
+
if (s.startingContainer && !runFailed.value) return 'Spinning up…'
|
|
79
|
+
return stepLabel[s.state]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function openDecisionFor(decisionId: string) {
|
|
83
|
+
if (instance.value) ui.openDecision(instance.value.id, decisionId)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function openApprovalFor(approvalId: string) {
|
|
87
|
+
if (instance.value) ui.openApprovalDetail(instance.value.id, approvalId)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Clicking any agent opens its step-detail overlay — execution metadata (state,
|
|
91
|
+
// timing, model, subtasks) plus the full prose output when the agent produced one.
|
|
92
|
+
function openStep(i: number) {
|
|
93
|
+
if (instance.value) ui.openStepDetail(instance.value.id, i)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Stop the run WITHOUT deleting it: halts the container + driver and records a
|
|
97
|
+
// `cancelled` failure, leaving the run readable + retryable (the block goes
|
|
98
|
+
// `blocked`). The destructive reset (delete the run, return the task to `planned`)
|
|
99
|
+
// is a separate, explicit action.
|
|
100
|
+
const stopping = ref(false)
|
|
101
|
+
async function stopRun() {
|
|
102
|
+
if (!instance.value || stopping.value) return
|
|
103
|
+
stopping.value = true
|
|
104
|
+
try {
|
|
105
|
+
await execution.stop(instance.value.id)
|
|
106
|
+
} finally {
|
|
107
|
+
stopping.value = false
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const resetting = ref(false)
|
|
111
|
+
async function resetRun() {
|
|
112
|
+
if (resetting.value) return
|
|
113
|
+
resetting.value = true
|
|
114
|
+
try {
|
|
115
|
+
await execution.cancel(props.block.id)
|
|
116
|
+
} finally {
|
|
117
|
+
resetting.value = false
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
</script>
|
|
121
|
+
|
|
122
|
+
<template>
|
|
123
|
+
<div class="space-y-4">
|
|
124
|
+
<!-- running pipeline -->
|
|
125
|
+
<div v-if="instance">
|
|
126
|
+
<div class="mb-1 flex items-center justify-between">
|
|
127
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
128
|
+
{{ instance.pipelineName }}
|
|
129
|
+
</span>
|
|
130
|
+
<div class="flex items-center gap-1">
|
|
131
|
+
<!-- Stop without deleting: halts the run but keeps it readable + retryable. -->
|
|
132
|
+
<UButton
|
|
133
|
+
icon="i-lucide-square"
|
|
134
|
+
color="warning"
|
|
135
|
+
variant="ghost"
|
|
136
|
+
size="xs"
|
|
137
|
+
:loading="stopping"
|
|
138
|
+
:disabled="resetting"
|
|
139
|
+
title="Stop the run but keep it (readable + retryable)"
|
|
140
|
+
@click="stopRun"
|
|
141
|
+
>
|
|
142
|
+
Stop
|
|
143
|
+
</UButton>
|
|
144
|
+
<!-- Destructive: discard the run and return the task to planned. -->
|
|
145
|
+
<UButton
|
|
146
|
+
icon="i-lucide-trash-2"
|
|
147
|
+
color="error"
|
|
148
|
+
variant="ghost"
|
|
149
|
+
size="xs"
|
|
150
|
+
:loading="resetting"
|
|
151
|
+
:disabled="stopping"
|
|
152
|
+
title="Discard this run and reset the task to planned"
|
|
153
|
+
@click="resetRun"
|
|
154
|
+
>
|
|
155
|
+
Reset
|
|
156
|
+
</UButton>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
<ul class="space-y-1">
|
|
160
|
+
<li
|
|
161
|
+
v-for="(s, i) in instance.steps"
|
|
162
|
+
:key="i"
|
|
163
|
+
class="rounded-md px-2 py-1"
|
|
164
|
+
:class="i === instance.currentStep ? 'bg-slate-800/70' : ''"
|
|
165
|
+
>
|
|
166
|
+
<div class="flex items-center gap-2">
|
|
167
|
+
<!-- Every agent is clickable: it opens the step-detail overlay (timing,
|
|
168
|
+
model, subtasks + the prose output when there is one). -->
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
class="flex min-w-0 cursor-pointer items-center gap-2 text-left transition hover:text-white"
|
|
172
|
+
:title="s.output ? 'View details & read output' : 'View step details'"
|
|
173
|
+
@click="openStep(i)"
|
|
174
|
+
>
|
|
175
|
+
<UIcon
|
|
176
|
+
:name="agentKindMeta(s.agentKind).icon"
|
|
177
|
+
class="h-4 w-4 shrink-0"
|
|
178
|
+
:style="{ color: agentKindMeta(s.agentKind).color }"
|
|
179
|
+
/>
|
|
180
|
+
<span class="truncate text-xs text-slate-200">
|
|
181
|
+
{{ agentKindMeta(s.agentKind).label }}
|
|
182
|
+
</span>
|
|
183
|
+
<span
|
|
184
|
+
v-if="isCompanionKind(s.agentKind)"
|
|
185
|
+
class="shrink-0 rounded bg-slate-700/60 px-1 text-[9px] font-medium uppercase tracking-wide text-slate-300"
|
|
186
|
+
title="Companion of a producer step"
|
|
187
|
+
>
|
|
188
|
+
Companion
|
|
189
|
+
</span>
|
|
190
|
+
<UIcon
|
|
191
|
+
:name="s.output ? 'i-lucide-book-open-text' : 'i-lucide-info'"
|
|
192
|
+
class="h-3.5 w-3.5 shrink-0 text-slate-500"
|
|
193
|
+
/>
|
|
194
|
+
</button>
|
|
195
|
+
<span
|
|
196
|
+
v-if="s.subtasks && s.subtasks.total > 0"
|
|
197
|
+
class="ml-auto font-mono text-[10px] tabular-nums text-slate-300"
|
|
198
|
+
:title="
|
|
199
|
+
s.subtasks.inProgress > 0
|
|
200
|
+
? `${s.subtasks.completed} of ${s.subtasks.total} subtasks done, ${s.subtasks.inProgress} in progress`
|
|
201
|
+
: `${s.subtasks.completed} of ${s.subtasks.total} subtasks done`
|
|
202
|
+
"
|
|
203
|
+
>
|
|
204
|
+
{{ s.subtasks.completed }}/{{ s.subtasks.total }}
|
|
205
|
+
</span>
|
|
206
|
+
<span
|
|
207
|
+
class="inline-flex items-center gap-1 text-[10px]"
|
|
208
|
+
:class="[
|
|
209
|
+
stepFailed(s) ? 'text-rose-400' : 'text-slate-400',
|
|
210
|
+
{ 'ml-auto': !s.subtasks },
|
|
211
|
+
]"
|
|
212
|
+
>
|
|
213
|
+
<UIcon v-if="stepFailed(s)" name="i-lucide-circle-x" class="h-3 w-3 shrink-0" />
|
|
214
|
+
{{ labelForStep(s) }}
|
|
215
|
+
</span>
|
|
216
|
+
<UButton
|
|
217
|
+
v-if="s.decision && !s.decision.chosen"
|
|
218
|
+
color="warning"
|
|
219
|
+
variant="soft"
|
|
220
|
+
size="xs"
|
|
221
|
+
icon="i-lucide-circle-help"
|
|
222
|
+
@click="openDecisionFor(s.decision.id)"
|
|
223
|
+
>
|
|
224
|
+
Resolve
|
|
225
|
+
</UButton>
|
|
226
|
+
<!-- reviewer gate folding/re-reviewing in the background: a working
|
|
227
|
+
indicator, NOT a "Review" gate (the human is summoned only if needed) -->
|
|
228
|
+
<span
|
|
229
|
+
v-else-if="reviews.isBackground(s.agentKind, block.id) && reviewStage"
|
|
230
|
+
class="inline-flex shrink-0 items-center gap-1 text-[10px] text-indigo-300"
|
|
231
|
+
>
|
|
232
|
+
<UIcon name="i-lucide-loader-circle" class="h-3 w-3 animate-spin" />
|
|
233
|
+
{{ reviewStageLabel }}
|
|
234
|
+
</span>
|
|
235
|
+
<!-- A companion that spent its rework budget parks on the iteration-cap
|
|
236
|
+
gate: it needs a 3-way DECISION (one more round / proceed / stop &
|
|
237
|
+
reset), not a plain approval — flag it distinctly so it can't read as
|
|
238
|
+
a normal "Approve". Opens the same detail surface (IterationCapPrompt). -->
|
|
239
|
+
<UButton
|
|
240
|
+
v-else-if="s.approval && s.approval.status === 'pending' && s.companion?.exceeded"
|
|
241
|
+
color="error"
|
|
242
|
+
variant="soft"
|
|
243
|
+
size="xs"
|
|
244
|
+
icon="i-lucide-alert-triangle"
|
|
245
|
+
@click="openApprovalFor(s.approval.id)"
|
|
246
|
+
>
|
|
247
|
+
Decide
|
|
248
|
+
</UButton>
|
|
249
|
+
<UButton
|
|
250
|
+
v-else-if="s.approval && s.approval.status === 'pending'"
|
|
251
|
+
color="warning"
|
|
252
|
+
variant="soft"
|
|
253
|
+
size="xs"
|
|
254
|
+
:icon="
|
|
255
|
+
agentKindMeta(s.agentKind).resultView
|
|
256
|
+
? 'i-lucide-clipboard-check'
|
|
257
|
+
: 'i-lucide-shield-check'
|
|
258
|
+
"
|
|
259
|
+
@click="openApprovalFor(s.approval.id)"
|
|
260
|
+
>
|
|
261
|
+
{{ agentKindMeta(s.agentKind).resultView ? 'Review' : 'Approve' }}
|
|
262
|
+
</UButton>
|
|
263
|
+
</div>
|
|
264
|
+
<div
|
|
265
|
+
v-if="s.subtasks && s.subtasks.total > 0"
|
|
266
|
+
class="mt-1 ml-6 h-1 overflow-hidden rounded-full bg-slate-700/60"
|
|
267
|
+
>
|
|
268
|
+
<div
|
|
269
|
+
class="h-full rounded-full bg-indigo-400 transition-all duration-500"
|
|
270
|
+
:style="{ width: `${(s.subtasks.completed / s.subtasks.total) * 100}%` }"
|
|
271
|
+
/>
|
|
272
|
+
</div>
|
|
273
|
+
<div
|
|
274
|
+
v-if="s.model"
|
|
275
|
+
class="mt-0.5 flex items-center gap-1 pl-6 text-[10px] text-slate-500"
|
|
276
|
+
:title="s.model"
|
|
277
|
+
>
|
|
278
|
+
<UIcon name="i-lucide-cpu" class="h-3 w-3" />
|
|
279
|
+
{{ models.labelForRef(s.model) }}
|
|
280
|
+
</div>
|
|
281
|
+
<!-- Prompt-fragment standards the library selected for this step. -->
|
|
282
|
+
<div
|
|
283
|
+
v-if="s.selectedFragmentIds && s.selectedFragmentIds.length"
|
|
284
|
+
class="mt-0.5 flex flex-wrap items-center gap-1 pl-6 text-[10px] text-slate-500"
|
|
285
|
+
:title="`Best-practice fragments folded into this step: ${s.selectedFragmentIds.join(', ')}`"
|
|
286
|
+
>
|
|
287
|
+
<UIcon name="i-lucide-book-marked" class="h-3 w-3 shrink-0" />
|
|
288
|
+
<span>{{ s.selectedFragmentIds.length }} standard(s) applied</span>
|
|
289
|
+
</div>
|
|
290
|
+
<!-- Conditionally-run companion (the Tester's fixer): possible/running/
|
|
291
|
+
completed/skipped, so it's clear whether a fix pass ran. -->
|
|
292
|
+
<div
|
|
293
|
+
v-if="gateCompanionFor(s, runFailed)"
|
|
294
|
+
class="mt-0.5 flex items-center gap-1.5 pl-6 text-[10px]"
|
|
295
|
+
>
|
|
296
|
+
<UIcon
|
|
297
|
+
:name="agentKindMeta(gateCompanionFor(s, runFailed)!.kind).icon"
|
|
298
|
+
class="h-3 w-3 shrink-0"
|
|
299
|
+
:class="[
|
|
300
|
+
COMPANION_STATE_META[gateCompanionFor(s, runFailed)!.state].text,
|
|
301
|
+
gateCompanionFor(s, runFailed)!.state === 'running' ? 'animate-spin' : '',
|
|
302
|
+
]"
|
|
303
|
+
/>
|
|
304
|
+
<span class="text-slate-400">
|
|
305
|
+
{{ agentKindMeta(gateCompanionFor(s, runFailed)!.kind).label }} (companion)
|
|
306
|
+
</span>
|
|
307
|
+
<span
|
|
308
|
+
class="ml-auto"
|
|
309
|
+
:class="COMPANION_STATE_META[gateCompanionFor(s, runFailed)!.state].text"
|
|
310
|
+
>
|
|
311
|
+
{{ COMPANION_STATE_META[gateCompanionFor(s, runFailed)!.state].label }}
|
|
312
|
+
</span>
|
|
313
|
+
</div>
|
|
314
|
+
</li>
|
|
315
|
+
</ul>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
<!-- failed run: shared failure banner + retry -->
|
|
319
|
+
<AgentFailureCard v-if="failedRun" :run="failedRun" />
|
|
320
|
+
|
|
321
|
+
<!-- Open PR: link straight to it on GitHub -->
|
|
322
|
+
<div v-if="pr" class="space-y-2">
|
|
323
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
324
|
+
Pull request
|
|
325
|
+
</span>
|
|
326
|
+
<UButton
|
|
327
|
+
:to="pr.url"
|
|
328
|
+
target="_blank"
|
|
329
|
+
rel="noopener"
|
|
330
|
+
external
|
|
331
|
+
color="neutral"
|
|
332
|
+
variant="soft"
|
|
333
|
+
size="sm"
|
|
334
|
+
icon="i-lucide-git-pull-request"
|
|
335
|
+
trailing-icon="i-lucide-external-link"
|
|
336
|
+
block
|
|
337
|
+
>
|
|
338
|
+
<span class="flex w-full items-center gap-2">
|
|
339
|
+
{{ prLabel }}
|
|
340
|
+
<UBadge :color="prMerged ? 'success' : 'info'" variant="subtle" size="sm" class="ml-auto">
|
|
341
|
+
{{ prMerged ? 'Merged' : 'Open' }}
|
|
342
|
+
</UBadge>
|
|
343
|
+
</span>
|
|
344
|
+
</UButton>
|
|
345
|
+
<p v-if="pr.branch" class="flex items-center gap-1 truncate text-[10px] text-slate-500">
|
|
346
|
+
<UIcon name="i-lucide-git-branch" class="h-3 w-3 shrink-0" />
|
|
347
|
+
<span class="truncate" :title="pr.branch">{{ pr.branch }}</span>
|
|
348
|
+
</p>
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
<!-- PR ready: merge -->
|
|
352
|
+
<UButton
|
|
353
|
+
v-if="block.status === 'pr_ready'"
|
|
354
|
+
color="success"
|
|
355
|
+
variant="solid"
|
|
356
|
+
size="sm"
|
|
357
|
+
icon="i-lucide-git-merge"
|
|
358
|
+
block
|
|
359
|
+
@click="execution.mergePr(block.id)"
|
|
360
|
+
>
|
|
361
|
+
Merge PR
|
|
362
|
+
</UButton>
|
|
363
|
+
</div>
|
|
364
|
+
</template>
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onMounted } from 'vue'
|
|
3
|
+
import type { Block } from '~/types/domain'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{ block: Block }>()
|
|
6
|
+
|
|
7
|
+
const board = useBoardStore()
|
|
8
|
+
const mergePresets = useMergePresetsStore()
|
|
9
|
+
const pipelines = usePipelinesStore()
|
|
10
|
+
const accounts = useAccountsStore()
|
|
11
|
+
|
|
12
|
+
// ---- responsible product person --------------------------------------------
|
|
13
|
+
// The account member (a `product` role-holder) accountable for this task; they are
|
|
14
|
+
// notified when requirement review flags findings. Picks from the account roster.
|
|
15
|
+
onMounted(() => {
|
|
16
|
+
const id = accounts.activeAccountId
|
|
17
|
+
if (id && accounts.members.length === 0) void accounts.loadRoster(id).catch(() => {})
|
|
18
|
+
})
|
|
19
|
+
const productMembers = computed(() => accounts.members.filter((m) => m.roles.includes('product')))
|
|
20
|
+
const responsible = computed(() =>
|
|
21
|
+
accounts.members.find((m) => m.userId === props.block.responsibleProductUserId),
|
|
22
|
+
)
|
|
23
|
+
const responsibleLabel = computed(() => {
|
|
24
|
+
const m = responsible.value
|
|
25
|
+
if (!m) return undefined
|
|
26
|
+
return m.name || m.email || m.userId
|
|
27
|
+
})
|
|
28
|
+
const responsibleMenu = computed(() => [
|
|
29
|
+
[
|
|
30
|
+
{ label: 'Unassigned', icon: 'i-lucide-user-x', onSelect: () => setResponsible('') },
|
|
31
|
+
...productMembers.value.map((m) => ({
|
|
32
|
+
label: m.name || m.email || m.userId,
|
|
33
|
+
icon: 'i-lucide-user',
|
|
34
|
+
onSelect: () => setResponsible(m.userId),
|
|
35
|
+
})),
|
|
36
|
+
],
|
|
37
|
+
])
|
|
38
|
+
function setResponsible(userId: string) {
|
|
39
|
+
board.updateBlock(props.block.id, { responsibleProductUserId: userId })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---- merge policy preset ---------------------------------------------------
|
|
43
|
+
// Which merge threshold preset governs this task's auto-merge decision + CI-fixer
|
|
44
|
+
// budget. None selected → the workspace default preset. (The old confidence-based
|
|
45
|
+
// auto-merge threshold is gone; the `merger` step gates on this policy instead.)
|
|
46
|
+
const selectedPreset = computed(() => mergePresets.resolve(props.block.mergePresetId))
|
|
47
|
+
const presetMenu = computed(() => [
|
|
48
|
+
[
|
|
49
|
+
{
|
|
50
|
+
label: mergePresets.defaultPreset
|
|
51
|
+
? `Default (${mergePresets.defaultPreset.name})`
|
|
52
|
+
: 'Workspace default',
|
|
53
|
+
icon: 'i-lucide-rotate-ccw',
|
|
54
|
+
onSelect: () => setPreset(''),
|
|
55
|
+
},
|
|
56
|
+
...mergePresets.presets.map((p) => ({
|
|
57
|
+
label: p.name,
|
|
58
|
+
icon: 'i-lucide-git-merge',
|
|
59
|
+
onSelect: () => setPreset(p.id),
|
|
60
|
+
})),
|
|
61
|
+
],
|
|
62
|
+
])
|
|
63
|
+
function setPreset(id: string) {
|
|
64
|
+
board.updateBlock(props.block.id, { mergePresetId: id })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---- pipeline --------------------------------------------------------------
|
|
68
|
+
// The pipeline this task's Run controls default to. None selected → the user picks
|
|
69
|
+
// at run time (the board falls back to the first defined pipeline).
|
|
70
|
+
const selectedPipeline = computed(() =>
|
|
71
|
+
props.block.pipelineId ? pipelines.getPipeline(props.block.pipelineId) : undefined,
|
|
72
|
+
)
|
|
73
|
+
const pipelineMenu = computed(() => [
|
|
74
|
+
[
|
|
75
|
+
{
|
|
76
|
+
label: 'No default',
|
|
77
|
+
icon: 'i-lucide-rotate-ccw',
|
|
78
|
+
onSelect: () => setPipeline(''),
|
|
79
|
+
},
|
|
80
|
+
...pipelines.pipelines.map((p) => ({
|
|
81
|
+
label: p.name,
|
|
82
|
+
icon: 'i-lucide-workflow',
|
|
83
|
+
onSelect: () => setPipeline(p.id),
|
|
84
|
+
})),
|
|
85
|
+
],
|
|
86
|
+
])
|
|
87
|
+
function setPipeline(id: string) {
|
|
88
|
+
board.updateBlock(props.block.id, { pipelineId: id })
|
|
89
|
+
}
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<template>
|
|
93
|
+
<div class="space-y-4">
|
|
94
|
+
<!-- pipeline -->
|
|
95
|
+
<div>
|
|
96
|
+
<div class="mb-1 flex items-center justify-between">
|
|
97
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
98
|
+
Pipeline
|
|
99
|
+
</span>
|
|
100
|
+
<UDropdownMenu :items="pipelineMenu">
|
|
101
|
+
<UButton
|
|
102
|
+
size="xs"
|
|
103
|
+
variant="ghost"
|
|
104
|
+
color="neutral"
|
|
105
|
+
icon="i-lucide-workflow"
|
|
106
|
+
trailing-icon="i-lucide-chevron-down"
|
|
107
|
+
/>
|
|
108
|
+
</UDropdownMenu>
|
|
109
|
+
</div>
|
|
110
|
+
<div v-if="selectedPipeline" class="flex items-center gap-1">
|
|
111
|
+
<UBadge
|
|
112
|
+
color="primary"
|
|
113
|
+
variant="subtle"
|
|
114
|
+
size="sm"
|
|
115
|
+
class="cursor-pointer"
|
|
116
|
+
@click="setPipeline('')"
|
|
117
|
+
>
|
|
118
|
+
{{ selectedPipeline.name }}<UIcon name="i-lucide-x" class="ml-0.5 h-3 w-3" />
|
|
119
|
+
</UBadge>
|
|
120
|
+
</div>
|
|
121
|
+
<div v-else class="text-[11px] text-slate-500">
|
|
122
|
+
No default — pick a pipeline when you run this task.
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<!-- merge policy preset -->
|
|
127
|
+
<div>
|
|
128
|
+
<div class="mb-1 flex items-center justify-between">
|
|
129
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
130
|
+
Merge policy
|
|
131
|
+
</span>
|
|
132
|
+
<UDropdownMenu :items="presetMenu">
|
|
133
|
+
<UButton
|
|
134
|
+
size="xs"
|
|
135
|
+
variant="ghost"
|
|
136
|
+
color="neutral"
|
|
137
|
+
icon="i-lucide-git-merge"
|
|
138
|
+
trailing-icon="i-lucide-chevron-down"
|
|
139
|
+
/>
|
|
140
|
+
</UDropdownMenu>
|
|
141
|
+
</div>
|
|
142
|
+
<div v-if="selectedPreset" class="text-[11px] text-slate-400">
|
|
143
|
+
<span class="text-slate-300">{{ selectedPreset.name }}</span>
|
|
144
|
+
— auto-merge when complexity ≤ {{ Math.round(selectedPreset.maxComplexity * 100) }}%, risk ≤
|
|
145
|
+
{{ Math.round(selectedPreset.maxRisk * 100) }}%, impact ≤
|
|
146
|
+
{{ Math.round(selectedPreset.maxImpact * 100) }}%; up to
|
|
147
|
+
{{ selectedPreset.ciMaxAttempts }} CI-fix attempts.
|
|
148
|
+
<span v-if="!block.mergePresetId" class="text-slate-500">(workspace default)</span>
|
|
149
|
+
</div>
|
|
150
|
+
<div v-else class="text-[11px] text-slate-500">
|
|
151
|
+
No preset configured — the merger raises a review notification for every PR.
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<!-- responsible product person -->
|
|
156
|
+
<div>
|
|
157
|
+
<div class="mb-1 flex items-center justify-between">
|
|
158
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
159
|
+
Responsible product
|
|
160
|
+
</span>
|
|
161
|
+
<UDropdownMenu :items="responsibleMenu">
|
|
162
|
+
<UButton
|
|
163
|
+
size="xs"
|
|
164
|
+
variant="ghost"
|
|
165
|
+
color="neutral"
|
|
166
|
+
icon="i-lucide-user"
|
|
167
|
+
trailing-icon="i-lucide-chevron-down"
|
|
168
|
+
/>
|
|
169
|
+
</UDropdownMenu>
|
|
170
|
+
</div>
|
|
171
|
+
<div v-if="responsibleLabel" class="flex items-center gap-1">
|
|
172
|
+
<UBadge
|
|
173
|
+
color="primary"
|
|
174
|
+
variant="subtle"
|
|
175
|
+
size="sm"
|
|
176
|
+
class="cursor-pointer"
|
|
177
|
+
@click="setResponsible('')"
|
|
178
|
+
>
|
|
179
|
+
{{ responsibleLabel }}<UIcon name="i-lucide-x" class="ml-0.5 h-3 w-3" />
|
|
180
|
+
</UBadge>
|
|
181
|
+
</div>
|
|
182
|
+
<div v-else class="text-[11px] text-slate-500">
|
|
183
|
+
Unassigned — set a product owner to notify them when requirement review flags this task.
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</template>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Block } from '~/types/domain'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{ block: Block }>()
|
|
5
|
+
|
|
6
|
+
const board = useBoardStore()
|
|
7
|
+
const fragments = useFragmentsStore()
|
|
8
|
+
|
|
9
|
+
// ---- best-practice prompt fragments ----------------------------------------
|
|
10
|
+
// Selected fragments (resolved against the catalog; unknown ids are dropped).
|
|
11
|
+
const selectedFragments = computed(() =>
|
|
12
|
+
(props.block.fragmentIds ?? [])
|
|
13
|
+
.map((id) => fragments.getFragment(id))
|
|
14
|
+
.filter((f): f is NonNullable<typeof f> => !!f),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// Picker menu: fragments suitable for this block's type, not already selected,
|
|
18
|
+
// grouped by category so the dropdown reads like the catalog.
|
|
19
|
+
const fragmentMenu = computed(() => {
|
|
20
|
+
const selected = new Set(props.block.fragmentIds ?? [])
|
|
21
|
+
const groups = new Map<string, { label: string; onSelect: () => void }[]>()
|
|
22
|
+
for (const f of fragments.forBlockType(props.block.type)) {
|
|
23
|
+
if (selected.has(f.id)) continue
|
|
24
|
+
const items = groups.get(f.category) ?? []
|
|
25
|
+
items.push({ label: f.title, onSelect: () => addFragment(f.id) })
|
|
26
|
+
groups.set(f.category, items)
|
|
27
|
+
}
|
|
28
|
+
return [...groups.values()]
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
function addFragment(id: string) {
|
|
32
|
+
const list = props.block.fragmentIds ? [...props.block.fragmentIds] : []
|
|
33
|
+
if (!list.includes(id)) list.push(id)
|
|
34
|
+
board.updateBlock(props.block.id, { fragmentIds: list })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function removeFragment(id: string) {
|
|
38
|
+
if (!props.block.fragmentIds) return
|
|
39
|
+
board.updateBlock(props.block.id, {
|
|
40
|
+
fragmentIds: props.block.fragmentIds.filter((x) => x !== id),
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<div class="space-y-4">
|
|
47
|
+
<!-- module assignment -->
|
|
48
|
+
<div>
|
|
49
|
+
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
50
|
+
Module
|
|
51
|
+
</div>
|
|
52
|
+
<UInput
|
|
53
|
+
v-model="block.moduleName"
|
|
54
|
+
size="sm"
|
|
55
|
+
class="w-full"
|
|
56
|
+
placeholder="e.g. Sessions (created on implement if new)"
|
|
57
|
+
icon="i-lucide-package"
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<!-- best practices (prompt fragments) -->
|
|
62
|
+
<div>
|
|
63
|
+
<div class="mb-1 flex items-center justify-between">
|
|
64
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
65
|
+
Best practices
|
|
66
|
+
</span>
|
|
67
|
+
<UDropdownMenu v-if="fragmentMenu.length" :items="fragmentMenu">
|
|
68
|
+
<UButton
|
|
69
|
+
size="xs"
|
|
70
|
+
variant="ghost"
|
|
71
|
+
color="neutral"
|
|
72
|
+
icon="i-lucide-plus"
|
|
73
|
+
trailing-icon="i-lucide-chevron-down"
|
|
74
|
+
/>
|
|
75
|
+
</UDropdownMenu>
|
|
76
|
+
</div>
|
|
77
|
+
<div v-if="selectedFragments.length" class="mb-1 flex flex-wrap gap-1">
|
|
78
|
+
<UBadge
|
|
79
|
+
v-for="f in selectedFragments"
|
|
80
|
+
:key="f.id"
|
|
81
|
+
color="primary"
|
|
82
|
+
variant="subtle"
|
|
83
|
+
size="sm"
|
|
84
|
+
class="cursor-pointer"
|
|
85
|
+
:title="f.summary"
|
|
86
|
+
@click="removeFragment(f.id)"
|
|
87
|
+
>
|
|
88
|
+
{{ f.title }}<UIcon name="i-lucide-x" class="ml-0.5 h-3 w-3" />
|
|
89
|
+
</UBadge>
|
|
90
|
+
</div>
|
|
91
|
+
<div v-else class="text-[11px] text-slate-500">
|
|
92
|
+
None — agents follow their default guidance.
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The single rendering path for an agent kind's icon (+ optional label) anywhere a
|
|
3
|
+
// pipeline or run lists its steps. Resolves display metadata through
|
|
4
|
+
// `agentKindMeta`, which is total over every kind — palette archetypes, custom
|
|
5
|
+
// agents and the engine's system kinds (ci / merger / blueprints / conflicts) — so
|
|
6
|
+
// a saved pipeline that contains a system kind can never blow up the renderer.
|
|
7
|
+
import { computed } from 'vue'
|
|
8
|
+
import { agentKindMeta } from '~/utils/catalog'
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(
|
|
11
|
+
defineProps<{ kind: string; showLabel?: boolean; iconClass?: string }>(),
|
|
12
|
+
{ showLabel: false, iconClass: 'h-4 w-4' },
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
const meta = computed(() => agentKindMeta(props.kind))
|
|
16
|
+
|
|
17
|
+
// Hover tooltip explaining what the agent does. Lead with the label (the icon
|
|
18
|
+
// alone is ambiguous) then the catalog description, so every place that renders
|
|
19
|
+
// an agent step through this single path gets the same explanation on hover.
|
|
20
|
+
const tooltip = computed(() =>
|
|
21
|
+
meta.value.description ? `${meta.value.label} — ${meta.value.description}` : meta.value.label,
|
|
22
|
+
)
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<span class="inline-flex items-center gap-2" :title="tooltip">
|
|
27
|
+
<UIcon :name="meta.icon" :class="iconClass" class="shrink-0" :style="{ color: meta.color }" />
|
|
28
|
+
<span v-if="showLabel" class="text-xs text-slate-100">{{ meta.label }}</span>
|
|
29
|
+
</span>
|
|
30
|
+
</template>
|