@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,359 @@
1
+ <script setup lang="ts">
2
+ import type { Block } from '~/types/domain'
3
+ import { STATUS_META, MODULE_META } from '~/utils/catalog'
4
+ import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
5
+ import TaskPipelineMini from './TaskPipelineMini.vue'
6
+
7
+ const props = defineProps<{ taskId: string }>()
8
+
9
+ const board = useBoardStore()
10
+ const execution = useExecutionStore()
11
+ const pipelines = usePipelinesStore()
12
+ const ui = useUiStore()
13
+ const agentRuns = useAgentRunsStore()
14
+ const reviews = useReviewStage()
15
+ const toast = useToast()
16
+
17
+ const task = computed<Block | undefined>(() => board.getBlock(props.taskId))
18
+ const statusMeta = computed(() => (task.value ? STATUS_META[task.value.status] : null))
19
+ const selected = computed(() => ui.selectedBlockId === props.taskId)
20
+
21
+ // ---- dependencies (gate execution order; may point across frames) ----------
22
+ const deps = computed(() =>
23
+ (task.value?.dependsOn ?? []).map((id) => board.getBlock(id)).filter((b): b is Block => !!b),
24
+ )
25
+ /** Deps that haven't merged yet — these block this task from running. */
26
+ const unmet = computed(() => board.unmetDeps(props.taskId))
27
+ const runnable = computed(() => board.isRunnable(props.taskId))
28
+
29
+ /** Label a dependency, noting its frame when it lives in another one. */
30
+ const { depLabel: labelDep } = useDepLabels()
31
+ const depLabel = (dep: Block) => labelDep(dep, task.value?.parentId)
32
+
33
+ /** The pipeline a plain "Start" will use: the task's pinned pipeline, else the first. */
34
+ const defaultPipeline = computed(
35
+ () =>
36
+ (task.value?.pipelineId ? pipelines.getPipeline(task.value.pipelineId) : undefined) ??
37
+ pipelines.pipelines[0],
38
+ )
39
+
40
+ /** The PR the implementer agent opened for this task, if any. */
41
+ const pr = computed(() => task.value?.pullRequest)
42
+ const prLabel = computed(() => (pr.value?.number ? `PR #${pr.value.number}` : 'PR'))
43
+
44
+ // This task's current agent run (if any). A failed run must surface the shared
45
+ // failure banner + retry — NOT a stuck progress bar — so the card never looks
46
+ // like it's still working after the run has terminated.
47
+ const agentRun = computed(() => agentRuns.byBlock[props.taskId])
48
+ const runFailed = computed(() => agentRun.value?.status === 'failed')
49
+
50
+ // When this task backs a recurring pipeline, surface a small repeat badge so the
51
+ // service shows its scheduled work at a glance (full controls live in the inspector).
52
+ const recurring = useRecurringPipelinesStore()
53
+ const schedule = computed(() => recurring.byBlock(props.taskId))
54
+
55
+ // Optimistic "Start": flip the button into a spinning "Starting…" state the
56
+ // instant it's clicked, before the server confirms. The button naturally
57
+ // unmounts once the stream pushes the block into `in_progress`; if the start
58
+ // call faults we revert and surface a toast.
59
+ const starting = ref(false)
60
+
61
+ async function run() {
62
+ if (!runnable.value) {
63
+ toast.add({
64
+ title: 'Blocked by dependencies',
65
+ description: `Waiting on: ${unmet.value.map((d) => d.title).join(', ')}`,
66
+ icon: 'i-lucide-lock',
67
+ })
68
+ return
69
+ }
70
+ const pipeline = defaultPipeline.value
71
+ if (!pipeline) {
72
+ toast.add({ title: 'No pipeline defined', description: 'Create one in the builder first.' })
73
+ return
74
+ }
75
+ starting.value = true
76
+ try {
77
+ // false ⇒ the user cancelled the personal-password prompt; revert quietly (the run
78
+ // never started). On success the button unmounts once the stream pushes in_progress.
79
+ const started = await execution.start(props.taskId, pipeline)
80
+ if (!started) starting.value = false
81
+ } catch (e) {
82
+ // Real confirmation came back as a failure — revert the optimistic state.
83
+ starting.value = false
84
+ toast.add({
85
+ title: 'Failed to start',
86
+ description: e instanceof Error ? e.message : String(e),
87
+ color: 'error',
88
+ icon: 'i-lucide-alert-triangle',
89
+ })
90
+ }
91
+ }
92
+
93
+ function review() {
94
+ ui.select(props.taskId)
95
+ ui.focus(props.taskId)
96
+ }
97
+
98
+ function merge() {
99
+ execution.mergePr(props.taskId)
100
+ }
101
+
102
+ // A `blocked` task is waiting on a human for one of two reasons — an agent-raised
103
+ // decision OR an approval gate — and both must surface here (a failed run is shown
104
+ // separately by the AgentFailureCard above). The board previously only handled
105
+ // decisions, so an approval-gated task was a dead end: it read "Decision needed"
106
+ // (the old generic `blocked` label) with no badge and a click that did nothing.
107
+ const pendingDecision = computed(() =>
108
+ execution.openDecisions.find((d) => d.blockId === props.taskId),
109
+ )
110
+ // The async stage an iterative reviewer gate (requirements-review / clarity-review) is
111
+ // mid-cycle in (folding the answers, then re-reviewing), or null. While set, the gate
112
+ // needs NO human action, so its approval is suppressed below and a working indicator
113
+ // shows instead.
114
+ const reviewStage = computed(() => reviews.stageForBlock(props.taskId))
115
+ const reviewStageLabel = computed(() =>
116
+ reviewStage.value === 'incorporating'
117
+ ? 'Incorporating answers…'
118
+ : reviewStage.value === 'reviewing'
119
+ ? 'Re-reviewing…'
120
+ : null,
121
+ )
122
+ const pendingApproval = computed(() => {
123
+ const a = execution.openApprovals.find((a) => a.blockId === props.taskId)
124
+ // A reviewer gate whose review is incorporating / re-reviewing in the driver is doing
125
+ // background work, not awaiting a human — don't surface it as "Approval needed".
126
+ if (a && reviews.isBackground(a.agentKind, props.taskId)) return undefined
127
+ return a
128
+ })
129
+
130
+ /** What this blocked task actually needs from a human — drives the card's label,
131
+ * pulse and action. Decision takes precedence over approval (a step never holds
132
+ * both at once; this is just a stable order). Null when nothing is pending. */
133
+ const attention = computed<{
134
+ label: string
135
+ icon: string
136
+ action: string
137
+ open: () => void
138
+ } | null>(() => {
139
+ const d = pendingDecision.value
140
+ if (d)
141
+ return {
142
+ label: 'Decision needed',
143
+ icon: 'i-lucide-circle-help',
144
+ action: 'Resolve',
145
+ open: () => ui.openDecision(d.instanceId, d.decision.id),
146
+ }
147
+ const a = pendingApproval.value
148
+ if (a)
149
+ return {
150
+ label: 'Approval needed',
151
+ icon: 'i-lucide-shield-check',
152
+ action: 'Approve',
153
+ open: () => ui.openApprovalDetail(a.instanceId, a.approval.id),
154
+ }
155
+ return null
156
+ })
157
+
158
+ /** Specific header copy: a failed run reads "Failed", a parked task reads its
159
+ * decision/approval reason, otherwise the generic status label. */
160
+ const statusText = computed(() =>
161
+ runFailed.value
162
+ ? 'Failed'
163
+ : (reviewStageLabel.value ?? attention.value?.label ?? statusMeta.value?.label ?? ''),
164
+ )
165
+
166
+ // Clicking the card body only selects the task (opening the inspector so the human can
167
+ // interact with it). Whatever the task is parked on — a decision, an approval, or the
168
+ // requirements review — is opened explicitly via the action button below, never by a
169
+ // click anywhere on the card. (A card-body click used to pop the review window open,
170
+ // which got in the way of just inspecting/editing the task.)
171
+ function selectTask() {
172
+ ui.select(props.taskId)
173
+ }
174
+ </script>
175
+
176
+ <template>
177
+ <div
178
+ v-if="task && statusMeta"
179
+ :data-block-id="task.id"
180
+ class="nodrag w-full cursor-pointer rounded-lg border bg-slate-950/70 p-2 text-left transition"
181
+ :class="[
182
+ selected ? 'border-white' : 'border-slate-700 hover:border-slate-500',
183
+ task.status === 'pr_ready' ? 'board-pulse-green' : attention ? 'board-pulse' : '',
184
+ ]"
185
+ @click.stop="selectTask"
186
+ >
187
+ <!-- header row -->
188
+ <div class="flex items-center gap-1.5">
189
+ <span class="h-2 w-2 shrink-0 rounded-full" :style="{ backgroundColor: statusMeta.color }" />
190
+ <UIcon
191
+ v-if="schedule"
192
+ name="i-lucide-repeat"
193
+ class="h-3 w-3 shrink-0 text-indigo-400"
194
+ :title="schedule.enabled ? 'Recurring pipeline' : 'Recurring pipeline (paused)'"
195
+ />
196
+ <span class="truncate text-[11px] font-semibold text-slate-100">{{ task.title }}</span>
197
+ <span
198
+ class="ml-auto shrink-0 text-[9px] uppercase tracking-wide"
199
+ :class="
200
+ runFailed
201
+ ? 'text-rose-400'
202
+ : reviewStage
203
+ ? 'text-indigo-300'
204
+ : attention
205
+ ? 'text-amber-400'
206
+ : 'text-slate-500'
207
+ "
208
+ >
209
+ {{ statusText }}
210
+ </span>
211
+ </div>
212
+
213
+ <!-- a failed run: the shared failure banner + retry, never a stuck bar -->
214
+ <AgentFailureCard
215
+ v-if="runFailed && agentRun"
216
+ :run="agentRun"
217
+ variant="compact"
218
+ class="mt-1.5"
219
+ />
220
+
221
+ <!-- progress while a pipeline runs (suppressed once the run has failed) -->
222
+ <UProgress
223
+ v-else-if="task.status === 'in_progress' || task.status === 'blocked'"
224
+ :model-value="Math.round(task.progress * 100)"
225
+ size="xs"
226
+ class="mt-1.5"
227
+ />
228
+
229
+ <!-- spatial drill-down: build steps (at `steps` zoom) and each step's live
230
+ subtask todos (one band deeper, at `subtasks` zoom) -->
231
+ <TaskPipelineMini :task-id="taskId" />
232
+
233
+ <!-- dependencies (run order) -->
234
+ <div v-if="deps.length" class="mt-1.5 flex flex-wrap items-center gap-1">
235
+ <UIcon
236
+ :name="runnable ? 'i-lucide-link' : 'i-lucide-lock'"
237
+ class="h-3 w-3"
238
+ :class="runnable ? 'text-slate-500' : 'text-amber-400'"
239
+ />
240
+ <span
241
+ v-for="d in deps"
242
+ :key="d.id"
243
+ class="inline-flex items-center gap-0.5 rounded bg-slate-800/80 px-1 py-0.5 text-[9px]"
244
+ :class="d.status === 'done' ? 'text-slate-400' : 'text-amber-300'"
245
+ :title="depLabel(d)"
246
+ >
247
+ <UIcon
248
+ :name="d.status === 'done' ? 'i-lucide-check' : 'i-lucide-clock'"
249
+ class="h-2.5 w-2.5"
250
+ />
251
+ <span class="max-w-[110px] truncate">{{ depLabel(d) }}</span>
252
+ </span>
253
+ </div>
254
+
255
+ <!-- actions by state -->
256
+ <div class="nodrag mt-2 flex flex-wrap items-center gap-1">
257
+ <!-- a reviewer gate folding/re-reviewing in the background: a working indicator,
258
+ NOT a gate — the human is back on the board and summoned only if input is needed -->
259
+ <span v-if="reviewStage" class="inline-flex items-center gap-1 text-[9px] text-indigo-300">
260
+ <UIcon name="i-lucide-loader-circle" class="h-3 w-3 animate-spin" />
261
+ {{ reviewStageLabel }}
262
+ </span>
263
+
264
+ <!-- parked for a human: a decision to resolve or an approval gate to clear -->
265
+ <UButton
266
+ v-if="attention"
267
+ color="warning"
268
+ variant="soft"
269
+ size="xs"
270
+ :icon="attention.icon"
271
+ @click.stop="attention.open()"
272
+ >
273
+ {{ attention.action }}
274
+ </UButton>
275
+
276
+ <template v-if="task.status === 'planned' || task.status === 'ready'">
277
+ <UButton
278
+ :color="runnable ? 'primary' : 'neutral'"
279
+ variant="soft"
280
+ size="xs"
281
+ :icon="runnable ? 'i-lucide-play' : 'i-lucide-lock'"
282
+ :loading="starting"
283
+ :disabled="!runnable || starting"
284
+ :title="
285
+ runnable
286
+ ? `Start ${defaultPipeline?.name ?? 'pipeline'}`
287
+ : `Waiting on: ${unmet.map((d) => d.title).join(', ')}`
288
+ "
289
+ @click.stop="run"
290
+ >
291
+ {{ starting ? 'Starting…' : runnable ? 'Start' : 'Blocked' }}
292
+ </UButton>
293
+ <span
294
+ v-if="runnable && defaultPipeline"
295
+ class="inline-flex items-center gap-0.5 text-[9px] text-slate-500"
296
+ >
297
+ <UIcon name="i-lucide-workflow" class="h-2.5 w-2.5" />{{ defaultPipeline.name }}
298
+ </span>
299
+ </template>
300
+
301
+ <template v-if="task.status === 'pr_ready'">
302
+ <UButton
303
+ v-if="pr"
304
+ :to="pr.url"
305
+ target="_blank"
306
+ rel="noopener"
307
+ external
308
+ color="neutral"
309
+ variant="soft"
310
+ size="xs"
311
+ icon="i-lucide-git-pull-request"
312
+ :title="`Open ${prLabel} on GitHub`"
313
+ @click.stop
314
+ >
315
+ {{ prLabel }}
316
+ </UButton>
317
+ <UButton
318
+ color="neutral"
319
+ variant="soft"
320
+ size="xs"
321
+ icon="i-lucide-scan-eye"
322
+ @click.stop="review"
323
+ >
324
+ Review
325
+ </UButton>
326
+ <UButton
327
+ color="success"
328
+ variant="solid"
329
+ size="xs"
330
+ icon="i-lucide-git-merge"
331
+ @click.stop="merge"
332
+ >
333
+ Merge
334
+ </UButton>
335
+ </template>
336
+
337
+ <span
338
+ v-else-if="task.status === 'done'"
339
+ class="inline-flex items-center gap-1 text-[9px] text-emerald-400"
340
+ >
341
+ <UIcon name="i-lucide-check-check" class="h-3 w-3" /> implemented
342
+ </span>
343
+ </div>
344
+
345
+ <!-- structural metadata: assigned module -->
346
+ <div
347
+ v-if="task.moduleName"
348
+ class="mt-2 flex flex-wrap items-center gap-1 border-t border-slate-800 pt-2"
349
+ >
350
+ <span
351
+ class="inline-flex items-center gap-1 rounded bg-violet-500/15 px-1.5 py-0.5 text-[9px] text-violet-200"
352
+ :title="`Module: ${task.moduleName}`"
353
+ >
354
+ <UIcon :name="MODULE_META.icon" class="h-3 w-3" :style="{ color: MODULE_META.color }" />
355
+ {{ task.moduleName }}
356
+ </span>
357
+ </div>
358
+ </div>
359
+ </template>
@@ -0,0 +1,159 @@
1
+ <script setup lang="ts">
2
+ import type { AgentState } from '~/types/domain'
3
+ import { agentKindMeta } from '~/utils/catalog'
4
+ import { subtaskIconClass, isFailedStep, FAILED_STEP_META } from '~/utils/pipelineRender'
5
+ import { lodAtLeast } from '~/composables/useSemanticZoom'
6
+
7
+ // Spatial drill-down inside a task card: at the `steps` zoom band the task's
8
+ // build-pipeline steps appear, and one band deeper (`subtasks`) each step's live
9
+ // todo breakdown expands — done / in-progress / pending — exactly the way a
10
+ // zoomed-in bootstrap card reads. Renders nothing until the task has a run and
11
+ // the user has zoomed in far enough, so it's safe to mount on every task card.
12
+ const props = defineProps<{ taskId: string }>()
13
+
14
+ const execution = useExecutionStore()
15
+ const ui = useUiStore()
16
+ const expansion = useTaskExpansionStore()
17
+ const reviews = useReviewStage()
18
+ const { lod } = useSemanticZoom()
19
+
20
+ const instance = computed(() => execution.getByBlock(props.taskId))
21
+ const steps = computed(() => instance.value?.steps ?? [])
22
+
23
+ // A failed run is no longer executing: a step left mid-flight (state still
24
+ // `working`) must stop spinning, matching the failure card the task card shows.
25
+ const runFailed = computed(() => instance.value?.status === 'failed')
26
+
27
+ // Expand the pipeline list only when zoomed in far enough AND the board driver
28
+ // permits this card — on-screen, and the centre-most of any cards that would
29
+ // otherwise overlap (see useTaskExpansion) — so deep-zoom expansions don't pile up.
30
+ const showSteps = computed(
31
+ () =>
32
+ lodAtLeast(lod.value, 'steps') && steps.value.length > 0 && expansion.canExpand(props.taskId),
33
+ )
34
+ const showItems = computed(() => lodAtLeast(lod.value, 'subtasks'))
35
+
36
+ // Clicking a step opens the full agent step-detail overlay — execution metadata
37
+ // (state, timing, model, subtasks) plus the agent's prose output — exactly like
38
+ // clicking it from the inspector panel or the focus-view pipeline, rather than
39
+ // expanding the text inline inside the board card.
40
+ function openStep(i: number) {
41
+ if (instance.value) ui.openStepDetail(instance.value.id, i)
42
+ }
43
+
44
+ /** Per-state accent, matching the inspector/focus pipeline views. */
45
+ const STATE_META: Record<AgentState, { color: string; icon: string }> = {
46
+ pending: { color: '#64748b', icon: 'i-lucide-circle-dashed' },
47
+ working: { color: '#6366f1', icon: 'i-lucide-loader' },
48
+ waiting_decision: { color: '#f59e0b', icon: 'i-lucide-circle-help' },
49
+ done: { color: '#22c55e', icon: 'i-lucide-circle-check' },
50
+ }
51
+
52
+ // Same todo-status icons the bootstrap card uses, so a zoomed-in task reads the
53
+ // same way as a zoomed-in bootstrap.
54
+ const ITEM_ICON: Record<string, string> = {
55
+ completed: 'i-lucide-check-circle-2',
56
+ in_progress: 'i-lucide-loader-circle',
57
+ pending: 'i-lucide-circle',
58
+ }
59
+ </script>
60
+
61
+ <template>
62
+ <div v-if="showSteps" class="mt-2 space-y-1 border-t border-slate-800 pt-2">
63
+ <div class="flex items-center gap-1 text-[9px] uppercase tracking-wide text-slate-500">
64
+ <UIcon name="i-lucide-workflow" class="h-2.5 w-2.5" />
65
+ Build steps
66
+ </div>
67
+ <div v-for="(s, i) in steps" :key="i" class="rounded bg-slate-900/60 px-1.5 py-1">
68
+ <div
69
+ class="flex cursor-pointer items-center gap-1"
70
+ :title="`${agentKindMeta(s.agentKind).label} — ${agentKindMeta(s.agentKind).description}\nClick to view step details & output`"
71
+ @click.stop="openStep(i)"
72
+ >
73
+ <UIcon
74
+ :name="agentKindMeta(s.agentKind).icon"
75
+ class="h-3 w-3 shrink-0"
76
+ :style="{ color: agentKindMeta(s.agentKind).color }"
77
+ />
78
+ <span class="truncate text-[10px] text-slate-200">
79
+ {{ agentKindMeta(s.agentKind).label }}
80
+ </span>
81
+ <UIcon
82
+ v-if="s.output"
83
+ name="i-lucide-file-text"
84
+ class="h-2.5 w-2.5 shrink-0 text-slate-500"
85
+ />
86
+ <span
87
+ v-if="s.subtasks && s.subtasks.total > 0"
88
+ class="ml-auto shrink-0 font-mono text-[9px] tabular-nums text-slate-400"
89
+ >
90
+ {{ s.subtasks.completed }}/{{ s.subtasks.total }}
91
+ </span>
92
+ <UIcon
93
+ v-else
94
+ :name="
95
+ isFailedStep(s.state, runFailed) ? FAILED_STEP_META.icon : STATE_META[s.state].icon
96
+ "
97
+ class="ml-auto h-2.5 w-2.5 shrink-0"
98
+ :class="s.state === 'working' && !runFailed ? 'animate-spin' : ''"
99
+ :style="{
100
+ color: isFailedStep(s.state, runFailed)
101
+ ? FAILED_STEP_META.color
102
+ : STATE_META[s.state].color,
103
+ }"
104
+ />
105
+ </div>
106
+
107
+ <!-- pending approval gate: jump straight to the conclusions reader. Suppressed
108
+ while a reviewer gate is folding/re-reviewing in the background (no human needed). -->
109
+ <button
110
+ v-if="
111
+ s.approval &&
112
+ s.approval.status === 'pending' &&
113
+ instance &&
114
+ !reviews.isBackground(s.agentKind, props.taskId)
115
+ "
116
+ type="button"
117
+ class="mt-1 flex w-full items-center justify-center gap-1 rounded bg-amber-500 px-1.5 py-0.5 text-[9px] font-semibold text-amber-950 transition hover:bg-amber-400"
118
+ @click.stop="ui.openApprovalDetail(instance.id, s.approval.id)"
119
+ >
120
+ <UIcon name="i-lucide-shield-check" class="h-2.5 w-2.5" />
121
+ Review &amp; approve
122
+ </button>
123
+
124
+ <!-- per-step subtask progress bar -->
125
+ <div
126
+ v-if="s.subtasks && s.subtasks.total > 0"
127
+ class="mt-1 h-0.5 w-full overflow-hidden rounded bg-slate-700/60"
128
+ >
129
+ <div
130
+ class="h-full rounded bg-indigo-400 transition-all"
131
+ :style="{ width: `${(s.subtasks.completed / s.subtasks.total) * 100}%` }"
132
+ />
133
+ </div>
134
+
135
+ <!-- deepest band: the actual todo list (done / in-progress / pending) -->
136
+ <ul v-if="showItems && s.subtasks?.items?.length" class="mt-1 space-y-0.5">
137
+ <li
138
+ v-for="(item, j) in s.subtasks.items"
139
+ :key="j"
140
+ class="flex items-start gap-1 text-[9px]"
141
+ :class="
142
+ item.status === 'completed'
143
+ ? 'text-slate-500 line-through'
144
+ : item.status === 'in_progress'
145
+ ? 'text-slate-100'
146
+ : 'text-slate-400'
147
+ "
148
+ >
149
+ <UIcon
150
+ :name="ITEM_ICON[item.status]"
151
+ class="mt-px h-2.5 w-2.5 shrink-0"
152
+ :class="subtaskIconClass(item.status, runFailed)"
153
+ />
154
+ <span>{{ item.label }}</span>
155
+ </li>
156
+ </ul>
157
+ </div>
158
+ </div>
159
+ </template>