@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,219 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Add a recurring pipeline to a service frame. Mirrors AddTaskModal: a button on
|
|
3
|
+
// the frame opens this, scoped to that frame (ui.addRecurringFrameId). The user
|
|
4
|
+
// names it, picks a pipeline + cadence, and the backend materialises one reused
|
|
5
|
+
// task block inside the frame that the schedule re-runs. When the Tech-debt
|
|
6
|
+
// pipeline is picked, the workspace issue-tracker choice is surfaced inline (it is
|
|
7
|
+
// where that pipeline files its ticket) and saved alongside.
|
|
8
|
+
import type { Recurrence, ScheduleTemplate } from '~/types/recurring'
|
|
9
|
+
|
|
10
|
+
const ui = useUiStore()
|
|
11
|
+
const board = useBoardStore()
|
|
12
|
+
const pipelines = usePipelinesStore()
|
|
13
|
+
const recurring = useRecurringPipelinesStore()
|
|
14
|
+
const tracker = useTrackerStore()
|
|
15
|
+
const toast = useToast()
|
|
16
|
+
|
|
17
|
+
const open = computed({
|
|
18
|
+
get: () => ui.addRecurringFrameId !== null,
|
|
19
|
+
set: (v: boolean) => {
|
|
20
|
+
if (!v) ui.closeAddRecurring()
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const frame = computed(() =>
|
|
25
|
+
ui.addRecurringFrameId ? board.getBlock(ui.addRecurringFrameId) : undefined,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const name = ref('')
|
|
29
|
+
const description = ref('')
|
|
30
|
+
const pipelineId = ref('')
|
|
31
|
+
const saving = ref(false)
|
|
32
|
+
const recurrence = ref<Recurrence>(defaultRecurrence())
|
|
33
|
+
|
|
34
|
+
// Tracker config (only relevant when the tech-debt pipeline is picked).
|
|
35
|
+
const trackerKind = ref<'github' | 'jira' | null>(null)
|
|
36
|
+
const jiraProjectKey = ref('')
|
|
37
|
+
|
|
38
|
+
function defaultRecurrence(): Recurrence {
|
|
39
|
+
return {
|
|
40
|
+
intervalHours: 168, // weekly
|
|
41
|
+
weekdays: [],
|
|
42
|
+
windowStartHour: null,
|
|
43
|
+
windowEndHour: null,
|
|
44
|
+
timezone: 'UTC',
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const pipelineMenu = computed(() => [
|
|
49
|
+
pipelines.pipelines.map((p) => ({
|
|
50
|
+
label: p.name,
|
|
51
|
+
icon: 'i-lucide-workflow',
|
|
52
|
+
onSelect: () => (pipelineId.value = p.id),
|
|
53
|
+
})),
|
|
54
|
+
])
|
|
55
|
+
const selectedPipeline = computed(() => pipelines.getPipeline(pipelineId.value))
|
|
56
|
+
const selectedPipelineLabel = computed(() => selectedPipeline.value?.name ?? 'Pick a pipeline')
|
|
57
|
+
|
|
58
|
+
// Infer the template from the picked pipeline so the backend seeds the right block
|
|
59
|
+
// description (and so we know to show the tracker config).
|
|
60
|
+
const template = computed<ScheduleTemplate>(() => {
|
|
61
|
+
if (pipelineId.value === 'pl_tech_debt') return 'tech-debt'
|
|
62
|
+
if (pipelineId.value === 'pl_dep_update') return 'dep-update'
|
|
63
|
+
return 'custom'
|
|
64
|
+
})
|
|
65
|
+
const isTechDebt = computed(() => template.value === 'tech-debt')
|
|
66
|
+
|
|
67
|
+
watch(open, (isOpen) => {
|
|
68
|
+
if (!isOpen) return
|
|
69
|
+
name.value = ''
|
|
70
|
+
description.value = ''
|
|
71
|
+
// Default to the Dependency-updates pipeline if present, else the first.
|
|
72
|
+
pipelineId.value =
|
|
73
|
+
pipelines.pipelines.find((p) => p.id === 'pl_dep_update')?.id ??
|
|
74
|
+
pipelines.pipelines[0]?.id ??
|
|
75
|
+
''
|
|
76
|
+
recurrence.value = defaultRecurrence()
|
|
77
|
+
saving.value = false
|
|
78
|
+
trackerKind.value = tracker.settings.tracker
|
|
79
|
+
jiraProjectKey.value = tracker.settings.jiraProjectKey ?? ''
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const canAdd = computed(() => name.value.trim().length > 0 && pipelineId.value.length > 0)
|
|
83
|
+
|
|
84
|
+
async function add() {
|
|
85
|
+
const frameId = ui.addRecurringFrameId
|
|
86
|
+
if (!frameId || !canAdd.value) return
|
|
87
|
+
saving.value = true
|
|
88
|
+
try {
|
|
89
|
+
// Persist the tracker selection first when the tech-debt pipeline needs it, so
|
|
90
|
+
// the very first run can file its ticket.
|
|
91
|
+
if (isTechDebt.value && trackerKind.value) {
|
|
92
|
+
await tracker.save({
|
|
93
|
+
tracker: trackerKind.value,
|
|
94
|
+
jiraProjectKey: trackerKind.value === 'jira' ? jiraProjectKey.value.trim() : null,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
await recurring.create({
|
|
98
|
+
frameId,
|
|
99
|
+
pipelineId: pipelineId.value,
|
|
100
|
+
template: template.value,
|
|
101
|
+
name: name.value.trim(),
|
|
102
|
+
recurrence: recurrence.value,
|
|
103
|
+
...(description.value.trim() ? { description: description.value.trim() } : {}),
|
|
104
|
+
})
|
|
105
|
+
ui.closeAddRecurring()
|
|
106
|
+
} catch (e) {
|
|
107
|
+
toast.add({
|
|
108
|
+
title: 'Could not add recurring pipeline',
|
|
109
|
+
description: e instanceof Error ? e.message : String(e),
|
|
110
|
+
icon: 'i-lucide-triangle-alert',
|
|
111
|
+
color: 'error',
|
|
112
|
+
})
|
|
113
|
+
} finally {
|
|
114
|
+
saving.value = false
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
</script>
|
|
118
|
+
|
|
119
|
+
<template>
|
|
120
|
+
<UModal v-model:open="open" title="Add a recurring pipeline">
|
|
121
|
+
<template #body>
|
|
122
|
+
<div class="space-y-4">
|
|
123
|
+
<p v-if="frame" class="text-xs text-slate-400">
|
|
124
|
+
Recurring pipeline on
|
|
125
|
+
<span class="font-medium text-slate-200">{{ frame.title }}</span>
|
|
126
|
+
</p>
|
|
127
|
+
|
|
128
|
+
<UFormField label="Name" required>
|
|
129
|
+
<UInput
|
|
130
|
+
v-model="name"
|
|
131
|
+
placeholder="e.g. Weekly dependency updates"
|
|
132
|
+
autofocus
|
|
133
|
+
class="w-full"
|
|
134
|
+
/>
|
|
135
|
+
</UFormField>
|
|
136
|
+
|
|
137
|
+
<UFormField label="Pipeline" required>
|
|
138
|
+
<UDropdownMenu :items="pipelineMenu" class="w-full">
|
|
139
|
+
<UButton
|
|
140
|
+
color="neutral"
|
|
141
|
+
variant="subtle"
|
|
142
|
+
size="sm"
|
|
143
|
+
icon="i-lucide-workflow"
|
|
144
|
+
trailing-icon="i-lucide-chevron-down"
|
|
145
|
+
class="w-full justify-between"
|
|
146
|
+
>
|
|
147
|
+
{{ selectedPipelineLabel }}
|
|
148
|
+
</UButton>
|
|
149
|
+
</UDropdownMenu>
|
|
150
|
+
</UFormField>
|
|
151
|
+
|
|
152
|
+
<UFormField label="Prompt">
|
|
153
|
+
<UTextarea
|
|
154
|
+
v-model="description"
|
|
155
|
+
:rows="3"
|
|
156
|
+
autoresize
|
|
157
|
+
placeholder="What should each run do? Describe the work — the same prompt a normal task carries. Leave blank to use the pipeline's default."
|
|
158
|
+
class="w-full"
|
|
159
|
+
/>
|
|
160
|
+
</UFormField>
|
|
161
|
+
|
|
162
|
+
<RecurringRecurrenceEditor v-model="recurrence" />
|
|
163
|
+
|
|
164
|
+
<div v-if="isTechDebt" class="space-y-3 rounded-lg border border-slate-800 p-3">
|
|
165
|
+
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
166
|
+
Issue tracker
|
|
167
|
+
</p>
|
|
168
|
+
<p class="text-[11px] text-slate-500">
|
|
169
|
+
The tech-debt pipeline files a ticket from its analysis before implementing. Choose
|
|
170
|
+
where (saved for the whole workspace).
|
|
171
|
+
</p>
|
|
172
|
+
<div class="flex gap-1">
|
|
173
|
+
<UButton
|
|
174
|
+
size="xs"
|
|
175
|
+
:color="trackerKind === 'github' ? 'primary' : 'neutral'"
|
|
176
|
+
:variant="trackerKind === 'github' ? 'solid' : 'subtle'"
|
|
177
|
+
icon="i-lucide-github"
|
|
178
|
+
@click="trackerKind = 'github'"
|
|
179
|
+
>
|
|
180
|
+
GitHub Issues
|
|
181
|
+
</UButton>
|
|
182
|
+
<UButton
|
|
183
|
+
size="xs"
|
|
184
|
+
:color="trackerKind === 'jira' ? 'primary' : 'neutral'"
|
|
185
|
+
:variant="trackerKind === 'jira' ? 'solid' : 'subtle'"
|
|
186
|
+
icon="i-lucide-square-check"
|
|
187
|
+
@click="trackerKind = 'jira'"
|
|
188
|
+
>
|
|
189
|
+
Jira
|
|
190
|
+
</UButton>
|
|
191
|
+
</div>
|
|
192
|
+
<UFormField v-if="trackerKind === 'jira'" label="Jira project key">
|
|
193
|
+
<UInput v-model="jiraProjectKey" placeholder="e.g. ENG" class="w-full" />
|
|
194
|
+
</UFormField>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<p class="text-[11px] text-slate-500">
|
|
198
|
+
A single recurring task is added inside the service; each run replaces the last. Its run
|
|
199
|
+
history is visible in the inspector.
|
|
200
|
+
</p>
|
|
201
|
+
</div>
|
|
202
|
+
</template>
|
|
203
|
+
|
|
204
|
+
<template #footer>
|
|
205
|
+
<div class="flex w-full justify-end gap-2">
|
|
206
|
+
<UButton color="neutral" variant="ghost" @click="ui.closeAddRecurring()">Cancel</UButton>
|
|
207
|
+
<UButton
|
|
208
|
+
color="primary"
|
|
209
|
+
icon="i-lucide-repeat"
|
|
210
|
+
:loading="saving"
|
|
211
|
+
:disabled="!canAdd"
|
|
212
|
+
@click="add"
|
|
213
|
+
>
|
|
214
|
+
Add recurring pipeline
|
|
215
|
+
</UButton>
|
|
216
|
+
</div>
|
|
217
|
+
</template>
|
|
218
|
+
</UModal>
|
|
219
|
+
</template>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
|
3
|
+
import { useRafFn } from '@vueuse/core'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Draws dependency arrows between task cards as an SVG overlay on top of the
|
|
7
|
+
* board. Tasks are plain DOM nodes (inside frame cards), so we resolve their
|
|
8
|
+
* on-screen rectangles by `[data-block-id]` every frame — this makes arrows
|
|
9
|
+
* follow pan / zoom / drag / expand for free. When a task's frame is collapsed
|
|
10
|
+
* (its card isn't rendered), the arrow anchors to the frame card instead.
|
|
11
|
+
*/
|
|
12
|
+
const board = useBoardStore()
|
|
13
|
+
|
|
14
|
+
const svg = ref<SVGSVGElement | null>(null)
|
|
15
|
+
|
|
16
|
+
type Seg = { id: string; x1: number; y1: number; x2: number; y2: number; done: boolean }
|
|
17
|
+
const segments = ref<Seg[]>([])
|
|
18
|
+
|
|
19
|
+
// task → its dependencies, both ends being tasks
|
|
20
|
+
const taskDeps = computed(() => {
|
|
21
|
+
const out: { id: string; source: string; target: string }[] = []
|
|
22
|
+
for (const t of board.allTasks) {
|
|
23
|
+
for (const depId of t.dependsOn) {
|
|
24
|
+
const dep = board.getBlock(depId)
|
|
25
|
+
if (dep && dep.level === 'task')
|
|
26
|
+
out.push({ id: `${depId}__${t.id}`, source: depId, target: t.id })
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return out
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
/** Resolve a task's anchor: walk up task → module → service to the first card
|
|
33
|
+
* that's actually rendered (a container may be collapsed). */
|
|
34
|
+
function anchorEl(taskId: string): HTMLElement | null {
|
|
35
|
+
let cur = board.getBlock(taskId)
|
|
36
|
+
while (cur) {
|
|
37
|
+
const el = document.querySelector(`[data-block-id="${cur.id}"]`) as HTMLElement | null
|
|
38
|
+
if (el) return el
|
|
39
|
+
cur = cur.parentId ? board.getBlock(cur.parentId) : undefined
|
|
40
|
+
}
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Point on a rect's border along the direction toward (tx,ty). */
|
|
45
|
+
function border(cx: number, cy: number, hw: number, hh: number, tx: number, ty: number) {
|
|
46
|
+
const dx = tx - cx
|
|
47
|
+
const dy = ty - cy
|
|
48
|
+
if (dx === 0 && dy === 0) return { x: cx, y: cy }
|
|
49
|
+
const t = Math.min(dx ? hw / Math.abs(dx) : Infinity, dy ? hh / Math.abs(dy) : Infinity)
|
|
50
|
+
return { x: cx + dx * t, y: cy + dy * t }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function recompute() {
|
|
54
|
+
const el = svg.value
|
|
55
|
+
if (!el) return
|
|
56
|
+
const origin = el.getBoundingClientRect()
|
|
57
|
+
const next: Seg[] = []
|
|
58
|
+
|
|
59
|
+
for (const d of taskDeps.value) {
|
|
60
|
+
const a = anchorEl(d.source)
|
|
61
|
+
const b = anchorEl(d.target)
|
|
62
|
+
if (!a || !b || a === b) continue // missing, or both collapsed into the same frame
|
|
63
|
+
|
|
64
|
+
const ra = a.getBoundingClientRect()
|
|
65
|
+
const rb = b.getBoundingClientRect()
|
|
66
|
+
const ax = ra.left + ra.width / 2 - origin.left
|
|
67
|
+
const ay = ra.top + ra.height / 2 - origin.top
|
|
68
|
+
const bx = rb.left + rb.width / 2 - origin.left
|
|
69
|
+
const by = rb.top + rb.height / 2 - origin.top
|
|
70
|
+
|
|
71
|
+
const start = border(ax, ay, ra.width / 2, ra.height / 2, bx, by)
|
|
72
|
+
const end = border(bx, by, rb.width / 2, rb.height / 2, ax, ay)
|
|
73
|
+
|
|
74
|
+
next.push({
|
|
75
|
+
id: d.id,
|
|
76
|
+
x1: start.x,
|
|
77
|
+
y1: start.y,
|
|
78
|
+
x2: end.x,
|
|
79
|
+
y2: end.y,
|
|
80
|
+
done: board.getBlock(d.source)?.status === 'done',
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
segments.value = next
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const { pause, resume } = useRafFn(recompute, { immediate: false })
|
|
87
|
+
onMounted(resume)
|
|
88
|
+
onBeforeUnmount(pause)
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<template>
|
|
92
|
+
<svg ref="svg" class="pointer-events-none absolute inset-0 z-10 h-full w-full overflow-visible">
|
|
93
|
+
<defs>
|
|
94
|
+
<marker
|
|
95
|
+
id="task-arrow-pending"
|
|
96
|
+
viewBox="0 0 10 10"
|
|
97
|
+
refX="8"
|
|
98
|
+
refY="5"
|
|
99
|
+
markerWidth="6"
|
|
100
|
+
markerHeight="6"
|
|
101
|
+
orient="auto-start-reverse"
|
|
102
|
+
>
|
|
103
|
+
<path d="M0,0 L10,5 L0,10 z" fill="#f59e0b" />
|
|
104
|
+
</marker>
|
|
105
|
+
<marker
|
|
106
|
+
id="task-arrow-done"
|
|
107
|
+
viewBox="0 0 10 10"
|
|
108
|
+
refX="8"
|
|
109
|
+
refY="5"
|
|
110
|
+
markerWidth="6"
|
|
111
|
+
markerHeight="6"
|
|
112
|
+
orient="auto-start-reverse"
|
|
113
|
+
>
|
|
114
|
+
<path d="M0,0 L10,5 L0,10 z" fill="#64748b" />
|
|
115
|
+
</marker>
|
|
116
|
+
</defs>
|
|
117
|
+
|
|
118
|
+
<line
|
|
119
|
+
v-for="s in segments"
|
|
120
|
+
:key="s.id"
|
|
121
|
+
:x1="s.x1"
|
|
122
|
+
:y1="s.y1"
|
|
123
|
+
:x2="s.x2"
|
|
124
|
+
:y2="s.y2"
|
|
125
|
+
:stroke="s.done ? '#64748b' : '#f59e0b'"
|
|
126
|
+
:stroke-width="2"
|
|
127
|
+
:stroke-dasharray="s.done ? '0' : '5 4'"
|
|
128
|
+
:stroke-opacity="0.85"
|
|
129
|
+
:marker-end="s.done ? 'url(#task-arrow-done)' : 'url(#task-arrow-pending)'"
|
|
130
|
+
/>
|
|
131
|
+
</svg>
|
|
132
|
+
</template>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { AgentState, PipelineStep } from '~/types/domain'
|
|
3
|
+
import { agentKindMeta } from '~/utils/catalog'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
step: PipelineStep
|
|
7
|
+
active?: boolean
|
|
8
|
+
size?: 'sm' | 'md'
|
|
9
|
+
}>()
|
|
10
|
+
|
|
11
|
+
const archetype = computed(() => agentKindMeta(props.step.agentKind))
|
|
12
|
+
|
|
13
|
+
const stateRing: Record<AgentState, string> = {
|
|
14
|
+
pending: 'ring-slate-600/60 opacity-60',
|
|
15
|
+
working: 'ring-indigo-400',
|
|
16
|
+
waiting_decision: 'ring-amber-400 board-pulse',
|
|
17
|
+
done: 'ring-emerald-400',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const stateIcon: Record<AgentState, string | null> = {
|
|
21
|
+
pending: null,
|
|
22
|
+
working: 'i-lucide-loader',
|
|
23
|
+
waiting_decision: 'i-lucide-circle-help',
|
|
24
|
+
done: 'i-lucide-check',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const dim = computed(() => (props.size === 'sm' ? 'h-7 w-7' : 'h-9 w-9'))
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<div class="flex flex-col items-center gap-1" :title="archetype.label">
|
|
32
|
+
<div
|
|
33
|
+
class="relative flex items-center justify-center rounded-full ring-2 transition"
|
|
34
|
+
:class="[dim, stateRing[step.state], active ? 'scale-110' : '']"
|
|
35
|
+
:style="{ backgroundColor: archetype.color + '22' }"
|
|
36
|
+
>
|
|
37
|
+
<UIcon :name="archetype.icon" class="text-base" :style="{ color: archetype.color }" />
|
|
38
|
+
<span
|
|
39
|
+
v-if="step.state === 'working'"
|
|
40
|
+
class="absolute -bottom-1 -right-1 rounded-full bg-slate-900 p-0.5"
|
|
41
|
+
>
|
|
42
|
+
<UIcon :name="stateIcon.working!" class="h-3 w-3 animate-spin text-indigo-300" />
|
|
43
|
+
</span>
|
|
44
|
+
<span
|
|
45
|
+
v-else-if="stateIcon[step.state]"
|
|
46
|
+
class="absolute -bottom-1 -right-1 rounded-full bg-slate-900 p-0.5"
|
|
47
|
+
>
|
|
48
|
+
<UIcon
|
|
49
|
+
:name="stateIcon[step.state]!"
|
|
50
|
+
class="h-3 w-3"
|
|
51
|
+
:class="step.state === 'done' ? 'text-emerald-300' : 'text-amber-300'"
|
|
52
|
+
/>
|
|
53
|
+
</span>
|
|
54
|
+
</div>
|
|
55
|
+
<span v-if="size !== 'sm'" class="text-[10px] leading-none text-slate-300">
|
|
56
|
+
{{ archetype.label }}
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|