@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,484 @@
1
+ <script setup lang="ts">
2
+ import type { AgentState, ExecutionInstance } from '~/types/domain'
3
+ import { agentKindMeta } from '~/utils/catalog'
4
+ import {
5
+ subtaskIconClass,
6
+ gateCompanionFor,
7
+ COMPANION_STATE_META,
8
+ isCompanionKind,
9
+ isFailedStep,
10
+ FAILED_STEP_META,
11
+ } from '~/utils/pipelineRender'
12
+ import StepMetricsBar from '~/components/observability/StepMetricsBar.vue'
13
+
14
+ const props = defineProps<{ instance: ExecutionInstance }>()
15
+ const emit = defineEmits<{
16
+ openDecision: [decisionId: string]
17
+ openApproval: [approvalId: string]
18
+ }>()
19
+
20
+ const models = useModelsStore()
21
+ const ui = useUiStore()
22
+ const execution = useExecutionStore()
23
+ const reviews = useReviewStage()
24
+
25
+ // While an iterative reviewer gate (requirements-review / clarity-review) folds the
26
+ // answers / re-reviews in the background it needs NO human, so its parked approval is
27
+ // replaced by a working indicator — the human is summoned again only if findings remain.
28
+ function reviewStageLabel(agentKind: string | undefined): string | null {
29
+ if (!reviews.isBackground(agentKind, props.instance.blockId)) return null
30
+ const stage = reviews.stageForBlock(props.instance.blockId)
31
+ return stage === 'incorporating'
32
+ ? 'Incorporating…'
33
+ : stage === 'reviewing'
34
+ ? 'Re-reviewing…'
35
+ : null
36
+ }
37
+
38
+ // Clicking an agent opens its step-detail overlay — execution metadata (state,
39
+ // timing, model, subtasks) plus the full prose output when the agent produced one.
40
+ function openStep(i: number) {
41
+ ui.openStepDetail(props.instance.id, i)
42
+ }
43
+
44
+ // --- restart from a step -----------------------------------------------------
45
+ // Re-run the pipeline from a chosen step onward: the server resets that step +
46
+ // every later step's iteration counters and re-drives a fresh run, keeping the
47
+ // earlier steps' outputs as handoff context. Destructive (later results are
48
+ // dropped), so the hover button arms a two-click confirm. A step with its own
49
+ // unresolved approval is excluded — the approval rail owns that interaction.
50
+ const restartArmed = ref<number | null>(null)
51
+ const restarting = ref<number | null>(null)
52
+ function canRestart(s: ExecutionInstance['steps'][number]) {
53
+ return !(s.approval && s.approval.status === 'pending')
54
+ }
55
+ async function restartFromHere(i: number) {
56
+ if (restarting.value !== null) return
57
+ restarting.value = i
58
+ try {
59
+ await execution.restartFromStep(props.instance.id, i)
60
+ } finally {
61
+ restarting.value = null
62
+ restartArmed.value = null
63
+ }
64
+ }
65
+
66
+ /** Visual language for an individual agent's runtime state. */
67
+ const STATE_META: Record<AgentState, { label: string; color: string; icon: string }> = {
68
+ pending: { label: 'Pending', color: '#64748b', icon: 'i-lucide-circle-dashed' },
69
+ working: { label: 'Working', color: '#6366f1', icon: 'i-lucide-loader' },
70
+ waiting_decision: { label: 'Needs decision', color: '#f59e0b', icon: 'i-lucide-circle-help' },
71
+ done: { label: 'Done', color: '#22c55e', icon: 'i-lucide-circle-check' },
72
+ }
73
+
74
+ /** Visual language for the pipeline instance as a whole. */
75
+ const STATUS_META: Record<ExecutionInstance['status'], { label: string; chip: string }> = {
76
+ running: { label: 'Running', chip: 'primary' },
77
+ blocked: { label: 'Needs you', chip: 'warning' },
78
+ paused: { label: 'Paused (budget)', chip: 'neutral' },
79
+ done: { label: 'Completed', chip: 'success' },
80
+ failed: { label: 'Failed', chip: 'error' },
81
+ }
82
+
83
+ const steps = computed(() => props.instance.steps)
84
+ const total = computed(() => steps.value.length)
85
+
86
+ // The conditionally-run companion (e.g. the Tester's `fixer`) each step drives, with
87
+ // its possible/running/completed/skipped state — rendered as a distinct sub-node so a
88
+ // human can see at a glance whether the fixer ran or was skipped.
89
+ const companionByStep = computed(() => steps.value.map((s) => gateCompanionFor(s, runFailed.value)))
90
+
91
+ // A failed run is no longer executing: a step left mid-flight (state still
92
+ // `working`, `startingContainer` still set) must stop looking live — no spinner,
93
+ // no pulse, no "spinning up container" phase.
94
+ const runFailed = computed(() => props.instance.status === 'failed')
95
+ /** A step that is genuinely, currently working (not a stale mid-flight step). */
96
+ function liveWorking(state: AgentState) {
97
+ return state === 'working' && !runFailed.value
98
+ }
99
+ /**
100
+ * The state visual (label/color/icon) for a step: a step left `working` when the run
101
+ * failed reads as "Failed" with a red cross, not a frozen "Working" loader.
102
+ */
103
+ function stepVisual(state: AgentState) {
104
+ return isFailedStep(state, runFailed.value) ? FAILED_STEP_META : STATE_META[state]
105
+ }
106
+
107
+ /** A step counts as fully complete only once its state is `done`. */
108
+ const completedCount = computed(() => steps.value.filter((s) => s.state === 'done').length)
109
+
110
+ /** Effective 0..1 progress per step (a done step is always 100%). */
111
+ function stepProgress(state: AgentState, progress: number) {
112
+ return state === 'done' ? 1 : progress
113
+ }
114
+
115
+ /** Overall pipeline progress: the mean of every step's effective progress. */
116
+ const overallProgress = computed(() => {
117
+ if (!total.value) return 0
118
+ const sum = steps.value.reduce((acc, s) => acc + stepProgress(s.state, s.progress), 0)
119
+ return sum / total.value
120
+ })
121
+ const overallPct = computed(() => Math.round(overallProgress.value * 100))
122
+
123
+ const statusMeta = computed(() => STATUS_META[props.instance.status])
124
+
125
+ /** The agent the pipeline is currently centred on (for the summary line). */
126
+ const currentAgent = computed(() => {
127
+ const s = steps.value[props.instance.currentStep]
128
+ return s ? agentKindMeta(s.agentKind).label : null
129
+ })
130
+
131
+ /** Connector below a step is "lit" once that step has completed. */
132
+ function connectorDone(index: number) {
133
+ return steps.value[index]?.state === 'done'
134
+ }
135
+
136
+ const legend: { state: AgentState }[] = [
137
+ { state: 'done' },
138
+ { state: 'working' },
139
+ { state: 'waiting_decision' },
140
+ { state: 'pending' },
141
+ ]
142
+
143
+ // Icon per todo-item status, matching how the bootstrap card renders its
144
+ // subtask breakdown — so a zoomed-in task shows the same live todo list.
145
+ const ITEM_ICON: Record<string, string> = {
146
+ completed: 'i-lucide-check-circle-2',
147
+ in_progress: 'i-lucide-loader-circle',
148
+ pending: 'i-lucide-circle',
149
+ }
150
+ </script>
151
+
152
+ <template>
153
+ <div class="flex flex-col gap-5">
154
+ <!-- summary -->
155
+ <div class="rounded-xl border border-slate-800 bg-slate-900/60 p-4">
156
+ <div class="flex flex-wrap items-center gap-3">
157
+ <UBadge :color="statusMeta.chip as any" variant="subtle">{{ statusMeta.label }}</UBadge>
158
+ <span class="text-sm text-slate-300">
159
+ <span class="font-semibold text-white">{{ completedCount }}</span>
160
+ / {{ total }} agents complete
161
+ </span>
162
+ <span v-if="currentAgent && instance.status === 'running'" class="text-xs text-slate-500">
163
+ · currently {{ currentAgent }}
164
+ </span>
165
+ <span class="ml-auto font-mono text-sm tabular-nums text-slate-200">{{ overallPct }}%</span>
166
+ </div>
167
+ <UProgress :model-value="overallPct" class="mt-3" />
168
+
169
+ <!-- legend -->
170
+ <div class="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1">
171
+ <span
172
+ v-for="l in legend"
173
+ :key="l.state"
174
+ class="inline-flex items-center gap-1.5 text-[11px] text-slate-400"
175
+ >
176
+ <span
177
+ class="h-2 w-2 rounded-full"
178
+ :style="{ backgroundColor: STATE_META[l.state].color }"
179
+ />
180
+ {{ STATE_META[l.state].label }}
181
+ </span>
182
+ </div>
183
+ </div>
184
+
185
+ <!-- agent chain as a vertical timeline -->
186
+ <ol class="flex flex-col">
187
+ <li v-for="(s, i) in steps" :key="i" class="relative flex gap-4 pb-5 last:pb-0">
188
+ <!-- connector line to the next step -->
189
+ <span
190
+ v-if="i < steps.length - 1"
191
+ class="absolute top-9 bottom-0 left-[17px] w-0.5 -translate-x-1/2"
192
+ :class="connectorDone(i) ? 'bg-emerald-500/60' : 'bg-slate-700'"
193
+ />
194
+
195
+ <!-- rail node -->
196
+ <span
197
+ class="relative z-10 flex h-9 w-9 shrink-0 items-center justify-center rounded-full border-2 bg-slate-950"
198
+ :class="liveWorking(s.state) ? 'step-active' : ''"
199
+ :style="{ borderColor: stepVisual(s.state).color }"
200
+ >
201
+ <UIcon
202
+ :name="stepVisual(s.state).icon"
203
+ class="h-4 w-4"
204
+ :class="liveWorking(s.state) ? 'animate-spin' : ''"
205
+ :style="{ color: stepVisual(s.state).color }"
206
+ />
207
+ </span>
208
+
209
+ <!-- step content card -->
210
+ <div
211
+ class="flex-1 rounded-xl border p-4 transition"
212
+ :class="[
213
+ i === instance.currentStep && instance.status !== 'done'
214
+ ? 'border-indigo-500/70 bg-slate-900 shadow-lg shadow-indigo-500/10'
215
+ : 'border-slate-800 bg-slate-900/50',
216
+ s.state === 'pending' ? 'opacity-60' : '',
217
+ ]"
218
+ >
219
+ <div
220
+ class="group flex cursor-pointer items-center gap-2"
221
+ :title="s.output ? 'View details & read output' : 'View step details'"
222
+ @click="openStep(i)"
223
+ >
224
+ <div
225
+ class="flex h-8 w-8 items-center justify-center rounded-lg"
226
+ :style="{ backgroundColor: agentKindMeta(s.agentKind).color + '22' }"
227
+ >
228
+ <UIcon
229
+ :name="agentKindMeta(s.agentKind).icon"
230
+ class="h-4 w-4"
231
+ :style="{ color: agentKindMeta(s.agentKind).color }"
232
+ />
233
+ </div>
234
+ <div class="min-w-0">
235
+ <div class="flex items-center gap-1.5">
236
+ <span class="truncate text-sm font-semibold text-white">
237
+ {{ agentKindMeta(s.agentKind).label }}
238
+ </span>
239
+ <span
240
+ v-if="isCompanionKind(s.agentKind)"
241
+ class="shrink-0 rounded bg-slate-700/60 px-1 text-[9px] font-medium uppercase tracking-wide text-slate-300"
242
+ title="Companion of a producer step"
243
+ >
244
+ Companion
245
+ </span>
246
+ </div>
247
+ <div class="text-[10px] uppercase tracking-wide text-slate-500">
248
+ Step {{ i + 1 }} of {{ total }}
249
+ </div>
250
+ </div>
251
+ <span
252
+ class="ml-auto shrink-0 text-[11px] font-medium"
253
+ :style="{ color: stepVisual(s.state).color }"
254
+ >
255
+ {{ stepVisual(s.state).label }}
256
+ </span>
257
+
258
+ <!-- restart-from-here: revealed on row hover, arms a two-click confirm
259
+ (resetting later steps is destructive). Stops propagation so it
260
+ doesn't also open the step-detail overlay. -->
261
+ <template v-if="canRestart(s)">
262
+ <UButton
263
+ v-if="restartArmed !== i"
264
+ icon="i-lucide-rotate-ccw"
265
+ color="neutral"
266
+ variant="ghost"
267
+ size="xs"
268
+ class="shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
269
+ title="Restart pipeline from this step"
270
+ @click.stop="restartArmed = i"
271
+ />
272
+ <template v-else>
273
+ <UButton
274
+ color="warning"
275
+ variant="soft"
276
+ size="xs"
277
+ icon="i-lucide-rotate-ccw"
278
+ :loading="restarting === i"
279
+ class="shrink-0"
280
+ @click.stop="restartFromHere(i)"
281
+ >
282
+ Restart from here
283
+ </UButton>
284
+ <UButton
285
+ color="neutral"
286
+ variant="ghost"
287
+ size="xs"
288
+ class="shrink-0"
289
+ :disabled="restarting === i"
290
+ @click.stop="restartArmed = null"
291
+ >
292
+ Cancel
293
+ </UButton>
294
+ </template>
295
+ </template>
296
+
297
+ <UIcon
298
+ :name="s.output ? 'i-lucide-book-open-text' : 'i-lucide-info'"
299
+ class="h-4 w-4 shrink-0 text-slate-500 transition-colors group-hover:text-indigo-300"
300
+ />
301
+ </div>
302
+
303
+ <!-- per-step progress (only while it has meaningful progress) -->
304
+ <UProgress
305
+ v-if="s.state === 'working' || s.state === 'done'"
306
+ :model-value="Math.round(stepProgress(s.state, s.progress) * 100)"
307
+ size="xs"
308
+ class="mt-3"
309
+ />
310
+
311
+ <!-- container cold-boot phase: shown until the container is up and the
312
+ agent starts reporting progress -->
313
+ <div
314
+ v-if="s.startingContainer && !runFailed"
315
+ class="mt-2 flex items-center gap-1.5 text-[11px] text-sky-300"
316
+ >
317
+ <UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 shrink-0 animate-spin" />
318
+ <span>Spinning up container…</span>
319
+ </div>
320
+
321
+ <!-- live subtask counts from the agent's todo list -->
322
+ <div v-if="s.subtasks && s.subtasks.total > 0" class="mt-2">
323
+ <div class="flex items-center justify-between text-[10px] text-slate-400">
324
+ <span>
325
+ {{ s.subtasks.completed }}/{{ s.subtasks.total }} subtasks
326
+ <span v-if="s.subtasks.inProgress > 0" class="text-indigo-300">
327
+ · {{ s.subtasks.inProgress }} in progress
328
+ </span>
329
+ </span>
330
+ </div>
331
+ <div class="mt-1 h-1 overflow-hidden rounded-full bg-slate-700/60">
332
+ <div
333
+ class="h-full rounded-full bg-indigo-400 transition-all duration-500"
334
+ :style="{ width: `${(s.subtasks.completed / s.subtasks.total) * 100}%` }"
335
+ />
336
+ </div>
337
+
338
+ <!-- the actual todo breakdown, rendered the same way the bootstrap
339
+ card shows its subtasks (status icon + struck-through when done) -->
340
+ <ul v-if="s.subtasks.items?.length" class="mt-2 space-y-1">
341
+ <li
342
+ v-for="(item, i) in s.subtasks.items"
343
+ :key="i"
344
+ class="flex items-start gap-1.5 text-[11px]"
345
+ :class="
346
+ item.status === 'completed'
347
+ ? 'text-slate-500 line-through'
348
+ : item.status === 'in_progress'
349
+ ? 'text-slate-100'
350
+ : 'text-slate-400'
351
+ "
352
+ >
353
+ <UIcon
354
+ :name="ITEM_ICON[item.status]"
355
+ class="mt-px h-3 w-3 shrink-0"
356
+ :class="subtaskIconClass(item.status, runFailed)"
357
+ />
358
+ <span>{{ item.label }}</span>
359
+ </li>
360
+ </ul>
361
+ </div>
362
+
363
+ <!-- model used for this step -->
364
+ <p
365
+ v-if="s.model"
366
+ class="mt-2 flex items-center gap-1 truncate text-[10px] text-slate-500"
367
+ :title="s.model"
368
+ >
369
+ <UIcon name="i-lucide-cpu" class="h-3 w-3 shrink-0" />
370
+ {{ models.labelForRef(s.model) }}
371
+ </p>
372
+
373
+ <!-- LLM observability rollup (tokens, output-limit headroom, transport-
374
+ vs-execution); click opens the full per-call activity panel. -->
375
+ <StepMetricsBar
376
+ v-if="s.metrics && s.metrics.calls > 0"
377
+ :metrics="s.metrics"
378
+ clickable
379
+ class="mt-2"
380
+ @inspect="ui.openObservability(instance.id)"
381
+ />
382
+
383
+ <!-- A one-line hint that the agent produced prose; the full output (and
384
+ all step metadata) lives in the step-detail overlay opened by click. -->
385
+ <p v-if="s.output" class="mt-2 flex items-center gap-1 text-[11px] text-slate-500">
386
+ <UIcon name="i-lucide-book-open-text" class="h-3 w-3 shrink-0" />
387
+ Click to read this agent’s output
388
+ </p>
389
+
390
+ <!-- Conditionally-run companion (today the Tester's fixer): a distinct
391
+ sub-node marked possible / running / completed / skipped. -->
392
+ <div
393
+ v-if="companionByStep[i]"
394
+ class="mt-3 flex items-center gap-2 rounded-lg border border-dashed border-slate-700/70 bg-slate-900/40 px-2.5 py-1.5"
395
+ >
396
+ <span
397
+ class="flex h-6 w-6 shrink-0 items-center justify-center rounded-md border"
398
+ :class="COMPANION_STATE_META[companionByStep[i]!.state].dot"
399
+ >
400
+ <UIcon
401
+ :name="agentKindMeta(companionByStep[i]!.kind).icon"
402
+ class="h-3 w-3"
403
+ :class="[
404
+ COMPANION_STATE_META[companionByStep[i]!.state].text,
405
+ companionByStep[i]!.state === 'running' && !runFailed ? 'animate-spin' : '',
406
+ ]"
407
+ />
408
+ </span>
409
+ <span class="min-w-0 flex-1 truncate text-[12px] text-slate-300">
410
+ {{ agentKindMeta(companionByStep[i]!.kind).label }}
411
+ <span class="text-slate-500">(companion)</span>
412
+ </span>
413
+ <span
414
+ class="shrink-0 text-[11px] font-medium"
415
+ :class="COMPANION_STATE_META[companionByStep[i]!.state].text"
416
+ >
417
+ {{ COMPANION_STATE_META[companionByStep[i]!.state].label }}
418
+ </span>
419
+ </div>
420
+
421
+ <!-- reviewer gate folding/re-reviewing in the background: a working indicator,
422
+ NOT a "Review & approve" gate (the human is summoned only if needed) -->
423
+ <div
424
+ v-if="reviewStageLabel(s.agentKind)"
425
+ class="mt-3 inline-flex items-center gap-1 text-[11px] text-indigo-300"
426
+ >
427
+ <UIcon name="i-lucide-loader-circle" class="h-3 w-3 animate-spin" />
428
+ {{ reviewStageLabel(s.agentKind) }}
429
+ </div>
430
+
431
+ <!-- approval gate: review (and edit) the proposal before continuing -->
432
+ <div v-else-if="s.approval && s.approval.status === 'pending'" class="mt-3">
433
+ <UButton
434
+ color="warning"
435
+ variant="soft"
436
+ size="xs"
437
+ icon="i-lucide-shield-check"
438
+ @click="emit('openApproval', s.approval.id)"
439
+ >
440
+ Review &amp; approve {{ agentKindMeta(s.agentKind).label }}'s proposal
441
+ </UButton>
442
+ </div>
443
+
444
+ <!-- decision: unresolved => prompt, resolved => show the choice -->
445
+ <div v-else-if="s.decision && !s.decision.chosen" class="mt-3">
446
+ <UButton
447
+ color="warning"
448
+ variant="soft"
449
+ size="xs"
450
+ icon="i-lucide-circle-help"
451
+ @click="emit('openDecision', s.decision.id)"
452
+ >
453
+ Resolve: {{ s.decision.question }}
454
+ </UButton>
455
+ </div>
456
+ <p
457
+ v-else-if="s.decision?.chosen"
458
+ class="mt-2 flex items-center gap-1 truncate text-[11px] text-emerald-400"
459
+ :title="s.decision.chosen"
460
+ >
461
+ <UIcon name="i-lucide-check" class="h-3 w-3 shrink-0" />
462
+ {{ s.decision.chosen }}
463
+ </p>
464
+ </div>
465
+ </li>
466
+ </ol>
467
+ </div>
468
+ </template>
469
+
470
+ <style scoped>
471
+ /* Soft indigo halo around the rail node of the actively-working step. */
472
+ @keyframes step-pulse {
473
+ 0%,
474
+ 100% {
475
+ box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.5);
476
+ }
477
+ 50% {
478
+ box-shadow: 0 0 0 6px rgba(99, 102, 241, 0);
479
+ }
480
+ }
481
+ .step-active {
482
+ animation: step-pulse 1.6s ease-in-out infinite;
483
+ }
484
+ </style>