@cat-factory/app 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/app/app.config.ts +8 -0
  4. package/app/app.vue +11 -0
  5. package/app/assets/css/main.css +100 -0
  6. package/app/components/auth/AuthGate.vue +24 -0
  7. package/app/components/auth/LoginScreen.vue +143 -0
  8. package/app/components/auth/UserMenu.vue +39 -0
  9. package/app/components/board/AddTaskModal.vue +444 -0
  10. package/app/components/board/AgentFailureCard.vue +97 -0
  11. package/app/components/board/AgentStopButton.vue +61 -0
  12. package/app/components/board/BoardCanvas.vue +183 -0
  13. package/app/components/board/ContextPicker.vue +367 -0
  14. package/app/components/board/RecurringPipelineModal.vue +219 -0
  15. package/app/components/board/TaskDependencyEdges.vue +132 -0
  16. package/app/components/board/nodes/AgentChip.vue +59 -0
  17. package/app/components/board/nodes/BlockNode.vue +433 -0
  18. package/app/components/board/nodes/DecisionBadge.vue +27 -0
  19. package/app/components/board/nodes/DraggableTask.vue +48 -0
  20. package/app/components/board/nodes/ModuleFrame.vue +97 -0
  21. package/app/components/board/nodes/TaskCard.vue +359 -0
  22. package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
  23. package/app/components/bootstrap/BootstrapModal.vue +665 -0
  24. package/app/components/clarity/ClarityReviewWindow.vue +611 -0
  25. package/app/components/consensus/ConsensusSessionWindow.vue +210 -0
  26. package/app/components/documents/DocumentImportModal.vue +161 -0
  27. package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
  28. package/app/components/documents/SpawnPreviewModal.vue +161 -0
  29. package/app/components/documents/TaskContextDocs.vue +83 -0
  30. package/app/components/focus/BlockFocusView.vue +171 -0
  31. package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
  32. package/app/components/gates/GateResultView.vue +282 -0
  33. package/app/components/github/AddServiceFromRepoModal.vue +354 -0
  34. package/app/components/github/GitHubConnect.vue +183 -0
  35. package/app/components/github/GitHubOnboarding.vue +45 -0
  36. package/app/components/github/GitHubPanel.vue +584 -0
  37. package/app/components/github/RepoTreeBrowser.vue +171 -0
  38. package/app/components/layout/AccountTeamSettings.vue +237 -0
  39. package/app/components/layout/BoardSwitcher.vue +280 -0
  40. package/app/components/layout/BoardToolbar.vue +156 -0
  41. package/app/components/layout/CommandBar.vue +336 -0
  42. package/app/components/layout/GitHubPatBanner.vue +73 -0
  43. package/app/components/layout/NotificationsInbox.vue +175 -0
  44. package/app/components/layout/SideBar.vue +314 -0
  45. package/app/components/layout/SpendWarningBanner.vue +107 -0
  46. package/app/components/observability/StepMetricsBar.vue +102 -0
  47. package/app/components/palettes/AgentPalette.vue +86 -0
  48. package/app/components/panels/AgentStepDetail.vue +737 -0
  49. package/app/components/panels/DecisionModal.vue +71 -0
  50. package/app/components/panels/InspectorPanel.vue +465 -0
  51. package/app/components/panels/ObservabilityPanel.vue +351 -0
  52. package/app/components/panels/StepMetadataCard.vue +253 -0
  53. package/app/components/panels/StepRestartControl.vue +90 -0
  54. package/app/components/panels/StepResultViewHost.vue +40 -0
  55. package/app/components/panels/StepTestReport.vue +84 -0
  56. package/app/components/panels/inspector/ContainerSummary.vue +74 -0
  57. package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
  58. package/app/components/panels/inspector/ServiceFragments.vue +82 -0
  59. package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
  60. package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
  61. package/app/components/panels/inspector/TaskDependencies.vue +70 -0
  62. package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
  63. package/app/components/panels/inspector/TaskExecution.vue +364 -0
  64. package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
  65. package/app/components/panels/inspector/TaskStructure.vue +96 -0
  66. package/app/components/pipeline/AgentKindIcon.vue +30 -0
  67. package/app/components/pipeline/IterationCapPrompt.vue +70 -0
  68. package/app/components/pipeline/PipelineBuilder.vue +817 -0
  69. package/app/components/pipeline/PipelineProgress.vue +484 -0
  70. package/app/components/providers/ApiKeysSection.vue +273 -0
  71. package/app/components/providers/PersonalCredentialModal.vue +128 -0
  72. package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
  73. package/app/components/providers/VendorCredentialsModal.vue +197 -0
  74. package/app/components/recurring/RecurrenceEditor.vue +124 -0
  75. package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
  76. package/app/components/settings/DatadogPanel.vue +213 -0
  77. package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
  78. package/app/components/settings/MergeThresholdsPanel.vue +378 -0
  79. package/app/components/settings/ModelDefaultsPanel.vue +250 -0
  80. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
  81. package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
  82. package/app/components/slack/SlackPanel.vue +299 -0
  83. package/app/components/tasks/TaskContextIssues.vue +88 -0
  84. package/app/components/tasks/TaskImportModal.vue +207 -0
  85. package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
  86. package/app/components/testing/TestReportWindow.vue +404 -0
  87. package/app/composables/api/accounts.ts +81 -0
  88. package/app/composables/api/auth.ts +45 -0
  89. package/app/composables/api/board.ts +101 -0
  90. package/app/composables/api/bootstrap.ts +62 -0
  91. package/app/composables/api/context.ts +25 -0
  92. package/app/composables/api/documents.ts +74 -0
  93. package/app/composables/api/execution.ts +127 -0
  94. package/app/composables/api/fragments.ts +71 -0
  95. package/app/composables/api/github.ts +131 -0
  96. package/app/composables/api/models.ts +127 -0
  97. package/app/composables/api/notifications.ts +23 -0
  98. package/app/composables/api/presets.ts +29 -0
  99. package/app/composables/api/recurring.ts +68 -0
  100. package/app/composables/api/releaseHealth.ts +43 -0
  101. package/app/composables/api/reviews.ts +146 -0
  102. package/app/composables/api/slack.ts +54 -0
  103. package/app/composables/api/tasks.ts +72 -0
  104. package/app/composables/api/workspaces.ts +36 -0
  105. package/app/composables/useApi.ts +89 -0
  106. package/app/composables/useBlockDrag.ts +90 -0
  107. package/app/composables/useBlockQueries.ts +154 -0
  108. package/app/composables/useBoardFlow.ts +11 -0
  109. package/app/composables/useContextLinking.ts +65 -0
  110. package/app/composables/useDepLabels.ts +26 -0
  111. package/app/composables/useFrameResize.ts +54 -0
  112. package/app/composables/useResultView.ts +48 -0
  113. package/app/composables/useReviewStage.ts +40 -0
  114. package/app/composables/useSemanticZoom.ts +31 -0
  115. package/app/composables/useStepApproval.ts +233 -0
  116. package/app/composables/useStepProse.ts +78 -0
  117. package/app/composables/useStepTimer.ts +63 -0
  118. package/app/composables/useTaskExpansion.ts +92 -0
  119. package/app/composables/useWorkspaceStream.ts +155 -0
  120. package/app/docs/architecture.md +31 -0
  121. package/app/pages/index.vue +141 -0
  122. package/app/stores/accounts.ts +152 -0
  123. package/app/stores/agentConfig.ts +35 -0
  124. package/app/stores/agentRuns.ts +122 -0
  125. package/app/stores/agents.ts +40 -0
  126. package/app/stores/apiKeys.ts +108 -0
  127. package/app/stores/auth.ts +166 -0
  128. package/app/stores/board.spec.ts +205 -0
  129. package/app/stores/board.ts +286 -0
  130. package/app/stores/bootstrap.ts +97 -0
  131. package/app/stores/clarity.ts +196 -0
  132. package/app/stores/consensus.ts +60 -0
  133. package/app/stores/documents.ts +176 -0
  134. package/app/stores/execution.ts +273 -0
  135. package/app/stores/fragmentLibrary.ts +147 -0
  136. package/app/stores/fragments.ts +40 -0
  137. package/app/stores/github.ts +305 -0
  138. package/app/stores/localModels.ts +51 -0
  139. package/app/stores/mergePresets.ts +58 -0
  140. package/app/stores/modelDefaults.ts +76 -0
  141. package/app/stores/models.ts +134 -0
  142. package/app/stores/notifications.ts +70 -0
  143. package/app/stores/observability.ts +144 -0
  144. package/app/stores/personalSubscriptions.ts +215 -0
  145. package/app/stores/pipelines.ts +327 -0
  146. package/app/stores/recurringPipelines.ts +112 -0
  147. package/app/stores/releaseHealth.ts +75 -0
  148. package/app/stores/requirements.spec.ts +94 -0
  149. package/app/stores/requirements.ts +208 -0
  150. package/app/stores/serviceFragmentDefaults.ts +29 -0
  151. package/app/stores/services.ts +87 -0
  152. package/app/stores/slack.ts +142 -0
  153. package/app/stores/taskExpansion.ts +36 -0
  154. package/app/stores/tasks.spec.ts +71 -0
  155. package/app/stores/tasks.ts +176 -0
  156. package/app/stores/tracker.ts +27 -0
  157. package/app/stores/ui.ts +434 -0
  158. package/app/stores/vendorCredentials.ts +54 -0
  159. package/app/stores/workspace.ts +215 -0
  160. package/app/stores/workspaceSettings.ts +36 -0
  161. package/app/types/accounts.ts +77 -0
  162. package/app/types/bootstrap.ts +83 -0
  163. package/app/types/clarity.ts +59 -0
  164. package/app/types/consensus.ts +91 -0
  165. package/app/types/documents.ts +104 -0
  166. package/app/types/domain.ts +495 -0
  167. package/app/types/execution.ts +383 -0
  168. package/app/types/fragments.ts +72 -0
  169. package/app/types/github.ts +173 -0
  170. package/app/types/localModels.ts +73 -0
  171. package/app/types/merge.ts +71 -0
  172. package/app/types/models.ts +157 -0
  173. package/app/types/notifications.ts +74 -0
  174. package/app/types/recurring.ts +69 -0
  175. package/app/types/releaseHealth.ts +31 -0
  176. package/app/types/requirements.ts +61 -0
  177. package/app/types/services.ts +27 -0
  178. package/app/types/slack.ts +57 -0
  179. package/app/types/tasks.ts +82 -0
  180. package/app/types/tracker.ts +18 -0
  181. package/app/utils/agentOutput.spec.ts +128 -0
  182. package/app/utils/agentOutput.ts +173 -0
  183. package/app/utils/catalog.spec.ts +112 -0
  184. package/app/utils/catalog.ts +455 -0
  185. package/app/utils/dnd.ts +29 -0
  186. package/app/utils/observability.ts +52 -0
  187. package/app/utils/pipelineRender.ts +151 -0
  188. package/nuxt.config.ts +55 -0
  189. package/package.json +45 -0
@@ -0,0 +1,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>