@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,351 @@
1
+ <script setup lang="ts">
2
+ import { computed, reactive, watch } from 'vue'
3
+ import { onKeyStroke } from '@vueuse/core'
4
+ import type { LlmCallMetric } from '~/types/execution'
5
+ import { agentKindMeta } from '~/utils/catalog'
6
+ import { formatMs, formatTokens, pct } from '~/utils/observability'
7
+
8
+ // Drill-down overlay for a run's LLM activity. Opened via
9
+ // `ui.openObservability(instanceId)` from a step surface; loads the full per-call
10
+ // detail (prompts, responses, token usage, output-limit headroom, the
11
+ // transport-vs-execution latency split) from the observability store and lists
12
+ // every model call, each expandable to its full prompt + response. Offers the
13
+ // LLM-friendly JSON export for handing a run to a model to analyse.
14
+ const ui = useUiStore()
15
+ const execution = useExecutionStore()
16
+ const board = useBoardStore()
17
+ const observability = useObservabilityStore()
18
+
19
+ const executionId = computed(() => ui.observabilityInstanceId)
20
+ const open = computed(() => !!executionId.value)
21
+ const instance = computed(() => execution.getInstance(executionId.value ?? undefined))
22
+ const block = computed(() => (instance.value ? board.getBlock(instance.value.blockId) : undefined))
23
+
24
+ const calls = computed<LlmCallMetric[]>(() =>
25
+ executionId.value ? observability.callsFor(executionId.value) : [],
26
+ )
27
+ const loading = computed(() => !!executionId.value && observability.isLoading(executionId.value))
28
+ const exporting = computed(
29
+ () => !!executionId.value && observability.isExporting(executionId.value),
30
+ )
31
+ const error = computed(() =>
32
+ executionId.value ? (observability.errors[executionId.value] ?? null) : null,
33
+ )
34
+
35
+ // Load (and refresh) whenever a different run's panel opens.
36
+ watch(executionId, (id) => {
37
+ if (id) void observability.load(id)
38
+ })
39
+
40
+ // Run-level totals, derived from the loaded calls.
41
+ const totals = computed(() => {
42
+ const c = calls.value
43
+ const upstreamMs = sum(c, (x) => x.upstreamMs)
44
+ const overheadMs = sum(c, (x) => x.overheadMs)
45
+ const total = upstreamMs + overheadMs
46
+ return {
47
+ calls: c.length,
48
+ promptTokens: sum(c, (x) => x.promptTokens),
49
+ completionTokens: sum(c, (x) => x.completionTokens),
50
+ upstreamMs,
51
+ overheadMs,
52
+ transportPct: total > 0 ? pct(overheadMs / total) : null,
53
+ errors: c.filter((x) => !x.ok).length,
54
+ warnings: c.filter((x) => x.ok && isWarning(x.finishReason)).length,
55
+ truncated: c.filter((x) => x.finishReason === 'length').length,
56
+ }
57
+ })
58
+
59
+ function sum(items: LlmCallMetric[], pick: (m: LlmCallMetric) => number): number {
60
+ return items.reduce((acc, m) => acc + pick(m), 0)
61
+ }
62
+ function isWarning(finishReason: string | null): boolean {
63
+ return finishReason === 'length' || finishReason === 'content_filter'
64
+ }
65
+
66
+ const expanded = reactive<Record<string, boolean>>({})
67
+ function toggle(c: LlmCallMetric) {
68
+ expanded[c.id] = !expanded[c.id]
69
+ // A live-streamed row arrives without its prompt/response bodies (the event stays
70
+ // small). On first expand, backfill them from the persisted metrics endpoint —
71
+ // `load` replaces the list with the full rows (same ids), so the open row fills in.
72
+ if (expanded[c.id] && !c.promptText && !c.responseText && executionId.value && !loading.value) {
73
+ void observability.load(executionId.value)
74
+ }
75
+ }
76
+
77
+ function agentMeta(kind: string) {
78
+ return agentKindMeta(kind)
79
+ }
80
+ function clock(ms: number): string {
81
+ return new Date(ms).toLocaleTimeString()
82
+ }
83
+ /** Pretty-print the prompt JSON; fall back to the raw string if it isn't JSON. */
84
+ function prettyPrompt(raw: string): string {
85
+ try {
86
+ return JSON.stringify(JSON.parse(raw), null, 2)
87
+ } catch {
88
+ return raw
89
+ }
90
+ }
91
+ function headroomOf(m: LlmCallMetric): number | null {
92
+ if (m.requestMaxTokens == null || m.requestMaxTokens <= 0) return null
93
+ return pct(Math.min(1, m.completionTokens / m.requestMaxTokens))
94
+ }
95
+
96
+ function close() {
97
+ ui.closeObservability()
98
+ }
99
+ onKeyStroke('Escape', () => {
100
+ if (open.value) close()
101
+ })
102
+ function exportJson() {
103
+ if (executionId.value) void observability.downloadExport(executionId.value)
104
+ }
105
+ </script>
106
+
107
+ <template>
108
+ <Teleport to="body">
109
+ <Transition name="obs-fade">
110
+ <div
111
+ v-if="open"
112
+ class="fixed inset-0 z-50 flex flex-col bg-slate-950/96 backdrop-blur-sm"
113
+ role="dialog"
114
+ aria-modal="true"
115
+ >
116
+ <header class="flex items-center gap-3 border-b border-slate-800 px-6 py-4">
117
+ <div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-sky-500/15">
118
+ <UIcon name="i-lucide-activity" class="h-5 w-5 text-sky-400" />
119
+ </div>
120
+ <div class="min-w-0">
121
+ <h1 class="truncate text-base font-semibold text-white">Model activity</h1>
122
+ <p v-if="block" class="truncate text-xs text-slate-500">
123
+ {{ block.title }} · {{ instance?.pipelineName }}
124
+ </p>
125
+ </div>
126
+ <div class="ml-auto flex items-center gap-1.5">
127
+ <UButton
128
+ icon="i-lucide-download"
129
+ color="neutral"
130
+ variant="soft"
131
+ size="sm"
132
+ :loading="exporting"
133
+ :disabled="!calls.length"
134
+ title="Download an LLM-friendly JSON export of this run"
135
+ @click="exportJson"
136
+ >
137
+ Export JSON
138
+ </UButton>
139
+ <UButton
140
+ icon="i-lucide-x"
141
+ color="neutral"
142
+ variant="ghost"
143
+ size="sm"
144
+ title="Close (Esc)"
145
+ @click="close"
146
+ />
147
+ </div>
148
+ </header>
149
+
150
+ <div class="flex-1 overflow-auto px-6 py-6">
151
+ <div class="mx-auto max-w-4xl space-y-5">
152
+ <!-- run-level summary -->
153
+ <section class="rounded-xl border border-slate-800 bg-slate-900/50 p-4">
154
+ <dl class="grid grid-cols-2 gap-x-6 gap-y-3 text-[13px] sm:grid-cols-4">
155
+ <div>
156
+ <dt class="text-[11px] uppercase tracking-wide text-slate-500">Calls</dt>
157
+ <dd class="mt-0.5 tabular-nums text-slate-200">{{ totals.calls }}</dd>
158
+ </div>
159
+ <div>
160
+ <dt class="text-[11px] uppercase tracking-wide text-slate-500">
161
+ Tokens (in / out)
162
+ </dt>
163
+ <dd class="mt-0.5 tabular-nums text-slate-200">
164
+ {{ formatTokens(totals.promptTokens) }} /
165
+ {{ formatTokens(totals.completionTokens) }}
166
+ </dd>
167
+ </div>
168
+ <div>
169
+ <dt class="text-[11px] uppercase tracking-wide text-slate-500">
170
+ Transport overhead
171
+ </dt>
172
+ <dd class="mt-0.5 tabular-nums text-slate-200">
173
+ <span v-if="totals.transportPct !== null">
174
+ {{ totals.transportPct }}% · {{ formatMs(totals.overheadMs) }}
175
+ </span>
176
+ <span v-else class="text-slate-500">—</span>
177
+ </dd>
178
+ </div>
179
+ <div>
180
+ <dt class="text-[11px] uppercase tracking-wide text-slate-500">
181
+ Model execution
182
+ </dt>
183
+ <dd class="mt-0.5 tabular-nums text-slate-200">
184
+ {{ formatMs(totals.upstreamMs) }}
185
+ </dd>
186
+ </div>
187
+ </dl>
188
+ <div class="mt-3 flex flex-wrap gap-1.5">
189
+ <UBadge v-if="totals.errors" color="error" variant="subtle" size="sm">
190
+ {{ totals.errors }} error{{ totals.errors === 1 ? '' : 's' }}
191
+ </UBadge>
192
+ <UBadge v-if="totals.warnings" color="warning" variant="subtle" size="sm">
193
+ {{ totals.warnings }} warning{{ totals.warnings === 1 ? '' : 's' }}
194
+ </UBadge>
195
+ <UBadge v-if="totals.truncated" color="error" variant="subtle" size="sm">
196
+ {{ totals.truncated }} truncated
197
+ </UBadge>
198
+ </div>
199
+ </section>
200
+
201
+ <!-- states -->
202
+ <p
203
+ v-if="loading && !calls.length"
204
+ class="flex items-center gap-2 py-8 text-center text-sm text-slate-500 justify-center"
205
+ >
206
+ <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" /> Loading model
207
+ activity…
208
+ </p>
209
+ <p
210
+ v-else-if="error"
211
+ class="rounded-lg border border-dashed border-rose-900/60 py-6 text-center text-sm text-rose-400"
212
+ >
213
+ {{ error }}
214
+ </p>
215
+ <p
216
+ v-else-if="!calls.length"
217
+ class="rounded-lg border border-dashed border-slate-800 py-8 text-center text-sm text-slate-500"
218
+ >
219
+ No model calls recorded for this run.
220
+ </p>
221
+
222
+ <!-- per-call list -->
223
+ <ul v-else class="space-y-2">
224
+ <li
225
+ v-for="c in calls"
226
+ :key="c.id"
227
+ class="overflow-hidden rounded-xl border border-slate-800 bg-slate-900/40"
228
+ :class="!c.ok ? 'border-rose-900/60' : ''"
229
+ >
230
+ <button
231
+ class="flex w-full items-center gap-3 px-4 py-2.5 text-left transition hover:bg-slate-900/70"
232
+ @click="toggle(c)"
233
+ >
234
+ <UIcon
235
+ name="i-lucide-chevron-right"
236
+ class="h-4 w-4 shrink-0 text-slate-500 transition-transform"
237
+ :class="expanded[c.id] ? 'rotate-90' : ''"
238
+ />
239
+ <UIcon
240
+ :name="agentMeta(c.agentKind).icon"
241
+ class="h-4 w-4 shrink-0"
242
+ :style="{ color: agentMeta(c.agentKind).color }"
243
+ />
244
+ <span class="text-[13px] text-slate-200">{{ agentMeta(c.agentKind).label }}</span>
245
+ <span
246
+ class="hidden truncate text-[11px] text-slate-500 sm:inline"
247
+ :title="c.model"
248
+ >
249
+ {{ c.provider }}:{{ c.model }}
250
+ </span>
251
+ <div
252
+ class="ml-auto flex items-center gap-2.5 text-[11px] tabular-nums text-slate-400"
253
+ >
254
+ <span
255
+ :title="`${c.promptTokens} prompt / ${c.completionTokens} completion tokens`"
256
+ >
257
+ {{ formatTokens(c.promptTokens) }}↑ {{ formatTokens(c.completionTokens) }}↓
258
+ </span>
259
+ <span v-if="headroomOf(c) !== null" :title="'Output used vs limit'">
260
+ {{ headroomOf(c) }}%
261
+ </span>
262
+ <span title="Transport overhead / model execution">
263
+ {{ formatMs(c.overheadMs) }} / {{ formatMs(c.upstreamMs) }}
264
+ </span>
265
+ <UBadge v-if="!c.ok" color="error" variant="subtle" size="sm">
266
+ {{ c.httpStatus ?? 'error' }}
267
+ </UBadge>
268
+ <UBadge
269
+ v-else-if="isWarning(c.finishReason)"
270
+ color="warning"
271
+ variant="subtle"
272
+ size="sm"
273
+ >
274
+ {{ c.finishReason }}
275
+ </UBadge>
276
+ <span v-else class="text-slate-600">{{ c.finishReason ?? 'ok' }}</span>
277
+ <span class="hidden text-slate-600 md:inline">{{ clock(c.createdAt) }}</span>
278
+ </div>
279
+ </button>
280
+
281
+ <div v-if="expanded[c.id]" class="border-t border-slate-800 px-4 py-3 space-y-3">
282
+ <p v-if="c.errorMessage" class="text-[12px] text-rose-400">
283
+ {{ c.errorMessage }}
284
+ </p>
285
+ <div class="flex flex-wrap gap-x-5 gap-y-1 text-[11px] text-slate-500">
286
+ <span>{{ c.messageCount }} messages</span>
287
+ <span>{{ c.toolCount }} tools</span>
288
+ <span>{{ c.streaming ? 'streamed' : 'buffered' }}</span>
289
+ <span v-if="c.requestMaxTokens != null"
290
+ >max_tokens {{ c.requestMaxTokens }}</span
291
+ >
292
+ <span v-if="c.cachedPromptTokens > 0" class="text-emerald-400"
293
+ >{{ c.cachedPromptTokens }}/{{ c.promptTokens }} prompt cached</span
294
+ >
295
+ <span>total {{ formatMs(c.totalMs) }}</span>
296
+ </div>
297
+ <div>
298
+ <div
299
+ class="mb-1 flex items-center gap-2 text-[11px] uppercase tracking-wide text-slate-500"
300
+ >
301
+ <span>Prompt</span>
302
+ <span
303
+ v-if="c.promptPrefixCount > 0"
304
+ class="normal-case tracking-normal text-slate-600"
305
+ >
306
+ (new messages only — {{ c.promptPrefixCount }} earlier omitted)
307
+ </span>
308
+ </div>
309
+ <pre
310
+ class="max-h-72 overflow-auto rounded-lg bg-slate-950/70 p-3 text-[11px] leading-relaxed text-slate-300"
311
+ >{{ prettyPrompt(c.promptText) }}</pre
312
+ >
313
+ </div>
314
+ <div>
315
+ <div class="mb-1 text-[11px] uppercase tracking-wide text-slate-500">
316
+ Response
317
+ </div>
318
+ <pre
319
+ class="max-h-72 overflow-auto rounded-lg bg-slate-950/70 p-3 text-[11px] leading-relaxed text-slate-300"
320
+ >{{ c.responseText || '—' }}</pre
321
+ >
322
+ </div>
323
+ <div v-if="c.reasoningText">
324
+ <div class="mb-1 text-[11px] uppercase tracking-wide text-slate-500">
325
+ Reasoning
326
+ </div>
327
+ <pre
328
+ class="max-h-72 overflow-auto rounded-lg bg-slate-950/70 p-3 text-[11px] leading-relaxed text-slate-400"
329
+ >{{ c.reasoningText }}</pre
330
+ >
331
+ </div>
332
+ </div>
333
+ </li>
334
+ </ul>
335
+ </div>
336
+ </div>
337
+ </div>
338
+ </Transition>
339
+ </Teleport>
340
+ </template>
341
+
342
+ <style scoped>
343
+ .obs-fade-enter-active,
344
+ .obs-fade-leave-active {
345
+ transition: opacity 0.18s ease;
346
+ }
347
+ .obs-fade-enter-from,
348
+ .obs-fade-leave-to {
349
+ opacity: 0;
350
+ }
351
+ </style>
@@ -0,0 +1,253 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import type { AgentState, PipelineStep, CompanionVerdict } from '~/types/execution'
4
+ import { subtaskIconClass } from '~/utils/pipelineRender'
5
+ import StepMetricsBar from '~/components/observability/StepMetricsBar.vue'
6
+
7
+ // The step's metadata card body: state/timing/model/run id, the container cold-boot
8
+ // phase, the live subtask breakdown, the LLM observability rollup, the applied
9
+ // standards, any raised decision/approval gate, and the companion verdict sequence.
10
+ // The scroll-spy `#step-details` section wrapper + ref stay in the parent reader.
11
+ const props = defineProps<{
12
+ step: PipelineStep
13
+ runFailed: boolean
14
+ durationLabel: string | null
15
+ isRunning: boolean
16
+ stepNumber: number
17
+ totalSteps: number
18
+ instanceId?: string
19
+ companionVerdicts: CompanionVerdict[]
20
+ latestVerdict: CompanionVerdict | null
21
+ }>()
22
+
23
+ const ui = useUiStore()
24
+ const models = useModelsStore()
25
+
26
+ const STATE_META: Record<AgentState, { label: string; color: string }> = {
27
+ pending: { label: 'Pending', color: '#64748b' },
28
+ working: { label: 'Working', color: '#6366f1' },
29
+ waiting_decision: { label: 'Needs input', color: '#f59e0b' },
30
+ done: { label: 'Done', color: '#22c55e' },
31
+ }
32
+
33
+ // The state badge: a step left mid-flight on a failed run keeps `state: 'working'`,
34
+ // so report it as "Failed" rather than the misleading "Working".
35
+ const stateMeta = computed(() => {
36
+ const s = props.step
37
+ if (props.runFailed && s.state === 'working') return { label: 'Failed', color: '#ef4444' }
38
+ return STATE_META[s.state]
39
+ })
40
+
41
+ const modelLabel = computed(() => (props.step.model ? models.labelForRef(props.step.model) : null))
42
+
43
+ const ITEM_ICON: Record<string, string> = {
44
+ completed: 'i-lucide-check-circle-2',
45
+ in_progress: 'i-lucide-loader-circle',
46
+ pending: 'i-lucide-circle',
47
+ }
48
+
49
+ const pctOf = (n: number) => `${Math.round(n * 100)}%`
50
+
51
+ function formatClock(ms?: number | null): string | null {
52
+ return ms ? new Date(ms).toLocaleString() : null
53
+ }
54
+
55
+ async function copyRunId() {
56
+ const id = props.step.runId ?? props.instanceId
57
+ if (id) await navigator.clipboard?.writeText(id)
58
+ }
59
+
60
+ function openObservability() {
61
+ if (props.instanceId) ui.openObservability(props.instanceId)
62
+ }
63
+ </script>
64
+
65
+ <template>
66
+ <div>
67
+ <dl class="grid grid-cols-2 gap-x-6 gap-y-3 text-[13px] sm:grid-cols-3">
68
+ <div>
69
+ <dt class="text-[11px] uppercase tracking-wide text-slate-500">State</dt>
70
+ <dd class="mt-0.5 flex items-center gap-1.5 text-slate-200">
71
+ <UIcon
72
+ v-if="runFailed && step.state === 'working'"
73
+ name="i-lucide-circle-x"
74
+ class="h-3.5 w-3.5 shrink-0"
75
+ :style="{ color: stateMeta.color }"
76
+ />
77
+ <span v-else class="h-2 w-2 rounded-full" :style="{ backgroundColor: stateMeta.color }" />
78
+ {{ stateMeta.label }}
79
+ </dd>
80
+ </div>
81
+ <div>
82
+ <dt class="text-[11px] uppercase tracking-wide text-slate-500">Duration</dt>
83
+ <dd class="mt-0.5 flex items-center gap-1.5 tabular-nums text-slate-200">
84
+ <UIcon
85
+ v-if="isRunning"
86
+ name="i-lucide-loader-circle"
87
+ class="h-3 w-3 animate-spin text-indigo-400"
88
+ />
89
+ <span v-if="durationLabel">{{ durationLabel }}</span>
90
+ <span v-else class="text-slate-500">—</span>
91
+ <span v-if="isRunning" class="text-[11px] text-slate-500">elapsed</span>
92
+ </dd>
93
+ </div>
94
+ <div>
95
+ <dt class="text-[11px] uppercase tracking-wide text-slate-500">Step</dt>
96
+ <dd class="mt-0.5 text-slate-200">{{ stepNumber }} of {{ totalSteps }}</dd>
97
+ </div>
98
+ <div>
99
+ <dt class="text-[11px] uppercase tracking-wide text-slate-500">Started</dt>
100
+ <dd class="mt-0.5 text-slate-300">{{ formatClock(step.startedAt) ?? '—' }}</dd>
101
+ </div>
102
+ <div>
103
+ <dt class="text-[11px] uppercase tracking-wide text-slate-500">Finished</dt>
104
+ <dd class="mt-0.5 text-slate-300">{{ formatClock(step.finishedAt) ?? '—' }}</dd>
105
+ </div>
106
+ <div>
107
+ <dt class="text-[11px] uppercase tracking-wide text-slate-500">Model</dt>
108
+ <dd class="mt-0.5 truncate text-slate-300" :title="step.model">
109
+ {{ modelLabel ?? 'Not recorded' }}
110
+ </dd>
111
+ </div>
112
+ <!-- The run id this step belongs to, surfaced for debugging (copyable). -->
113
+ <div class="col-span-2 sm:col-span-3">
114
+ <dt class="text-[11px] uppercase tracking-wide text-slate-500">Run</dt>
115
+ <dd
116
+ class="mt-0.5 cursor-pointer truncate font-mono text-[12px] text-slate-400 hover:text-slate-200"
117
+ :title="`${step.runId ?? instanceId ?? ''} — click to copy`"
118
+ @click="copyRunId"
119
+ >
120
+ {{ step.runId ?? instanceId ?? '—' }}
121
+ </dd>
122
+ </div>
123
+ </dl>
124
+
125
+ <!-- container cold-boot phase: shown until the container is up and
126
+ the agent starts reporting progress -->
127
+ <div
128
+ v-if="step.startingContainer && !runFailed"
129
+ class="mt-4 flex items-center gap-2 rounded-lg border border-sky-900/50 bg-sky-950/30 px-3 py-2 text-[12px] text-sky-300"
130
+ >
131
+ <UIcon name="i-lucide-loader-circle" class="h-4 w-4 shrink-0 animate-spin" />
132
+ <span>Spinning up container…</span>
133
+ </div>
134
+
135
+ <!-- live subtask breakdown -->
136
+ <div v-if="step.subtasks && step.subtasks.total > 0" class="mt-4">
137
+ <div class="text-[11px] uppercase tracking-wide text-slate-500">
138
+ Subtasks · {{ step.subtasks.completed }}/{{ step.subtasks.total }}
139
+ </div>
140
+ <div class="mt-1 h-1 overflow-hidden rounded-full bg-slate-700/60">
141
+ <div
142
+ class="h-full rounded-full bg-indigo-400 transition-all duration-500"
143
+ :style="{
144
+ width: `${(step.subtasks.completed / step.subtasks.total) * 100}%`,
145
+ }"
146
+ />
147
+ </div>
148
+ <ul v-if="step.subtasks.items?.length" class="mt-2 space-y-1">
149
+ <li
150
+ v-for="(item, idx) in step.subtasks.items"
151
+ :key="idx"
152
+ class="flex items-start gap-1.5 text-[12px]"
153
+ :class="
154
+ item.status === 'completed'
155
+ ? 'text-slate-500 line-through'
156
+ : item.status === 'in_progress'
157
+ ? 'text-slate-100'
158
+ : 'text-slate-400'
159
+ "
160
+ >
161
+ <UIcon
162
+ :name="ITEM_ICON[item.status]"
163
+ class="mt-px h-3 w-3 shrink-0"
164
+ :class="subtaskIconClass(item.status, runFailed)"
165
+ />
166
+ <span>{{ item.label }}</span>
167
+ </li>
168
+ </ul>
169
+ </div>
170
+
171
+ <!-- LLM observability rollup (tokens, output-limit headroom,
172
+ transport-vs-execution); click to open the full per-call panel -->
173
+ <div v-if="step.metrics && step.metrics.calls > 0" class="mt-4">
174
+ <div class="mb-1 flex items-center justify-between">
175
+ <span class="text-[11px] uppercase tracking-wide text-slate-500"> Model activity </span>
176
+ <button class="text-[11px] text-sky-400 hover:text-sky-300" @click="openObservability">
177
+ View all calls →
178
+ </button>
179
+ </div>
180
+ <StepMetricsBar :metrics="step.metrics" clickable @inspect="openObservability" />
181
+ </div>
182
+
183
+ <!-- standards (prompt fragments) folded into this step -->
184
+ <div v-if="step.selectedFragmentIds && step.selectedFragmentIds.length" class="mt-4">
185
+ <div class="text-[11px] uppercase tracking-wide text-slate-500">Standards applied</div>
186
+ <div class="mt-1 flex flex-wrap gap-1">
187
+ <UBadge
188
+ v-for="id in step.selectedFragmentIds"
189
+ :key="id"
190
+ color="neutral"
191
+ variant="subtle"
192
+ size="sm"
193
+ >
194
+ {{ id }}
195
+ </UBadge>
196
+ </div>
197
+ </div>
198
+
199
+ <!-- decision raised on this step -->
200
+ <div v-if="step.decision" class="mt-4">
201
+ <div class="text-[11px] uppercase tracking-wide text-slate-500">Decision</div>
202
+ <p class="mt-0.5 text-[13px] text-slate-200">{{ step.decision.question }}</p>
203
+ <p
204
+ v-if="step.decision.chosen"
205
+ class="mt-0.5 flex items-center gap-1 text-[12px] text-emerald-400"
206
+ >
207
+ <UIcon name="i-lucide-check" class="h-3 w-3 shrink-0" />
208
+ {{ step.decision.chosen }}
209
+ </p>
210
+ <p v-else class="mt-0.5 text-[12px] text-amber-400">Awaiting a human choice</p>
211
+ </div>
212
+
213
+ <!-- approval gate state -->
214
+ <div v-if="step.approval" class="mt-4">
215
+ <div class="text-[11px] uppercase tracking-wide text-slate-500">Approval gate</div>
216
+ <p class="mt-0.5 text-[13px] text-slate-200 capitalize">
217
+ {{ step.approval.status.replace('_', ' ') }}
218
+ </p>
219
+ </div>
220
+
221
+ <!-- companion verdict + full correction sequence -->
222
+ <div v-if="companionVerdicts.length" class="mt-4">
223
+ <div class="flex items-center justify-between">
224
+ <span class="text-[11px] uppercase tracking-wide text-slate-500"> Companion review </span>
225
+ <UBadge :color="latestVerdict?.passed ? 'success' : 'warning'" variant="subtle" size="sm">
226
+ {{ pctOf(latestVerdict!.rating) }}
227
+ {{ latestVerdict?.passed ? '≥' : '<' }} {{ pctOf(latestVerdict!.threshold) }}
228
+ </UBadge>
229
+ </div>
230
+ <ol class="mt-2 space-y-1.5">
231
+ <li v-for="(v, i) in companionVerdicts" :key="i" class="flex items-start gap-2 text-[12px]">
232
+ <span
233
+ class="mt-px inline-flex h-4 shrink-0 items-center rounded px-1 font-mono text-[11px] tabular-nums"
234
+ :class="
235
+ v.passed ? 'bg-emerald-500/15 text-emerald-300' : 'bg-amber-500/15 text-amber-300'
236
+ "
237
+ >
238
+ {{ i + 1 }}
239
+ </span>
240
+ <div class="min-w-0">
241
+ <span :class="v.passed ? 'text-emerald-300' : 'text-amber-300'">
242
+ {{ pctOf(v.rating) }} {{ v.passed ? '≥' : '<' }} {{ pctOf(v.threshold) }}
243
+ </span>
244
+ <span v-if="v.feedback" class="ml-1 text-slate-400">— {{ v.feedback }}</span>
245
+ </div>
246
+ </li>
247
+ </ol>
248
+ <p v-if="companionVerdicts.length > 1" class="mt-1 text-[11px] text-slate-500">
249
+ {{ companionVerdicts.length }} correction iteration(s).
250
+ </p>
251
+ </div>
252
+ </div>
253
+ </template>