@cat-factory/app 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/app/app.config.ts +8 -0
- package/app/app.vue +11 -0
- package/app/assets/css/main.css +100 -0
- package/app/components/auth/AuthGate.vue +24 -0
- package/app/components/auth/LoginScreen.vue +143 -0
- package/app/components/auth/UserMenu.vue +39 -0
- package/app/components/board/AddTaskModal.vue +444 -0
- package/app/components/board/AgentFailureCard.vue +97 -0
- package/app/components/board/AgentStopButton.vue +61 -0
- package/app/components/board/BoardCanvas.vue +183 -0
- package/app/components/board/ContextPicker.vue +367 -0
- package/app/components/board/RecurringPipelineModal.vue +219 -0
- package/app/components/board/TaskDependencyEdges.vue +132 -0
- package/app/components/board/nodes/AgentChip.vue +59 -0
- package/app/components/board/nodes/BlockNode.vue +433 -0
- package/app/components/board/nodes/DecisionBadge.vue +27 -0
- package/app/components/board/nodes/DraggableTask.vue +48 -0
- package/app/components/board/nodes/ModuleFrame.vue +97 -0
- package/app/components/board/nodes/TaskCard.vue +359 -0
- package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
- package/app/components/bootstrap/BootstrapModal.vue +665 -0
- package/app/components/clarity/ClarityReviewWindow.vue +611 -0
- package/app/components/consensus/ConsensusSessionWindow.vue +210 -0
- package/app/components/documents/DocumentImportModal.vue +161 -0
- package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
- package/app/components/documents/SpawnPreviewModal.vue +161 -0
- package/app/components/documents/TaskContextDocs.vue +83 -0
- package/app/components/focus/BlockFocusView.vue +171 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
- package/app/components/gates/GateResultView.vue +282 -0
- package/app/components/github/AddServiceFromRepoModal.vue +354 -0
- package/app/components/github/GitHubConnect.vue +183 -0
- package/app/components/github/GitHubOnboarding.vue +45 -0
- package/app/components/github/GitHubPanel.vue +584 -0
- package/app/components/github/RepoTreeBrowser.vue +171 -0
- package/app/components/layout/AccountTeamSettings.vue +237 -0
- package/app/components/layout/BoardSwitcher.vue +280 -0
- package/app/components/layout/BoardToolbar.vue +156 -0
- package/app/components/layout/CommandBar.vue +336 -0
- package/app/components/layout/GitHubPatBanner.vue +73 -0
- package/app/components/layout/NotificationsInbox.vue +175 -0
- package/app/components/layout/SideBar.vue +314 -0
- package/app/components/layout/SpendWarningBanner.vue +107 -0
- package/app/components/observability/StepMetricsBar.vue +102 -0
- package/app/components/palettes/AgentPalette.vue +86 -0
- package/app/components/panels/AgentStepDetail.vue +737 -0
- package/app/components/panels/DecisionModal.vue +71 -0
- package/app/components/panels/InspectorPanel.vue +465 -0
- package/app/components/panels/ObservabilityPanel.vue +351 -0
- package/app/components/panels/StepMetadataCard.vue +253 -0
- package/app/components/panels/StepRestartControl.vue +90 -0
- package/app/components/panels/StepResultViewHost.vue +40 -0
- package/app/components/panels/StepTestReport.vue +84 -0
- package/app/components/panels/inspector/ContainerSummary.vue +74 -0
- package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
- package/app/components/panels/inspector/ServiceFragments.vue +82 -0
- package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
- package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
- package/app/components/panels/inspector/TaskDependencies.vue +70 -0
- package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
- package/app/components/panels/inspector/TaskExecution.vue +364 -0
- package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
- package/app/components/panels/inspector/TaskStructure.vue +96 -0
- package/app/components/pipeline/AgentKindIcon.vue +30 -0
- package/app/components/pipeline/IterationCapPrompt.vue +70 -0
- package/app/components/pipeline/PipelineBuilder.vue +817 -0
- package/app/components/pipeline/PipelineProgress.vue +484 -0
- package/app/components/providers/ApiKeysSection.vue +273 -0
- package/app/components/providers/PersonalCredentialModal.vue +128 -0
- package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
- package/app/components/providers/VendorCredentialsModal.vue +197 -0
- package/app/components/recurring/RecurrenceEditor.vue +124 -0
- package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
- package/app/components/settings/DatadogPanel.vue +213 -0
- package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
- package/app/components/settings/MergeThresholdsPanel.vue +378 -0
- package/app/components/settings/ModelDefaultsPanel.vue +250 -0
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
- package/app/components/slack/SlackPanel.vue +299 -0
- package/app/components/tasks/TaskContextIssues.vue +88 -0
- package/app/components/tasks/TaskImportModal.vue +207 -0
- package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
- package/app/components/testing/TestReportWindow.vue +404 -0
- package/app/composables/api/accounts.ts +81 -0
- package/app/composables/api/auth.ts +45 -0
- package/app/composables/api/board.ts +101 -0
- package/app/composables/api/bootstrap.ts +62 -0
- package/app/composables/api/context.ts +25 -0
- package/app/composables/api/documents.ts +74 -0
- package/app/composables/api/execution.ts +127 -0
- package/app/composables/api/fragments.ts +71 -0
- package/app/composables/api/github.ts +131 -0
- package/app/composables/api/models.ts +127 -0
- package/app/composables/api/notifications.ts +23 -0
- package/app/composables/api/presets.ts +29 -0
- package/app/composables/api/recurring.ts +68 -0
- package/app/composables/api/releaseHealth.ts +43 -0
- package/app/composables/api/reviews.ts +146 -0
- package/app/composables/api/slack.ts +54 -0
- package/app/composables/api/tasks.ts +72 -0
- package/app/composables/api/workspaces.ts +36 -0
- package/app/composables/useApi.ts +89 -0
- package/app/composables/useBlockDrag.ts +90 -0
- package/app/composables/useBlockQueries.ts +154 -0
- package/app/composables/useBoardFlow.ts +11 -0
- package/app/composables/useContextLinking.ts +65 -0
- package/app/composables/useDepLabels.ts +26 -0
- package/app/composables/useFrameResize.ts +54 -0
- package/app/composables/useResultView.ts +48 -0
- package/app/composables/useReviewStage.ts +40 -0
- package/app/composables/useSemanticZoom.ts +31 -0
- package/app/composables/useStepApproval.ts +233 -0
- package/app/composables/useStepProse.ts +78 -0
- package/app/composables/useStepTimer.ts +63 -0
- package/app/composables/useTaskExpansion.ts +92 -0
- package/app/composables/useWorkspaceStream.ts +155 -0
- package/app/docs/architecture.md +31 -0
- package/app/pages/index.vue +141 -0
- package/app/stores/accounts.ts +152 -0
- package/app/stores/agentConfig.ts +35 -0
- package/app/stores/agentRuns.ts +122 -0
- package/app/stores/agents.ts +40 -0
- package/app/stores/apiKeys.ts +108 -0
- package/app/stores/auth.ts +166 -0
- package/app/stores/board.spec.ts +205 -0
- package/app/stores/board.ts +286 -0
- package/app/stores/bootstrap.ts +97 -0
- package/app/stores/clarity.ts +196 -0
- package/app/stores/consensus.ts +60 -0
- package/app/stores/documents.ts +176 -0
- package/app/stores/execution.ts +273 -0
- package/app/stores/fragmentLibrary.ts +147 -0
- package/app/stores/fragments.ts +40 -0
- package/app/stores/github.ts +305 -0
- package/app/stores/localModels.ts +51 -0
- package/app/stores/mergePresets.ts +58 -0
- package/app/stores/modelDefaults.ts +76 -0
- package/app/stores/models.ts +134 -0
- package/app/stores/notifications.ts +70 -0
- package/app/stores/observability.ts +144 -0
- package/app/stores/personalSubscriptions.ts +215 -0
- package/app/stores/pipelines.ts +327 -0
- package/app/stores/recurringPipelines.ts +112 -0
- package/app/stores/releaseHealth.ts +75 -0
- package/app/stores/requirements.spec.ts +94 -0
- package/app/stores/requirements.ts +208 -0
- package/app/stores/serviceFragmentDefaults.ts +29 -0
- package/app/stores/services.ts +87 -0
- package/app/stores/slack.ts +142 -0
- package/app/stores/taskExpansion.ts +36 -0
- package/app/stores/tasks.spec.ts +71 -0
- package/app/stores/tasks.ts +176 -0
- package/app/stores/tracker.ts +27 -0
- package/app/stores/ui.ts +434 -0
- package/app/stores/vendorCredentials.ts +54 -0
- package/app/stores/workspace.ts +215 -0
- package/app/stores/workspaceSettings.ts +36 -0
- package/app/types/accounts.ts +77 -0
- package/app/types/bootstrap.ts +83 -0
- package/app/types/clarity.ts +59 -0
- package/app/types/consensus.ts +91 -0
- package/app/types/documents.ts +104 -0
- package/app/types/domain.ts +495 -0
- package/app/types/execution.ts +383 -0
- package/app/types/fragments.ts +72 -0
- package/app/types/github.ts +173 -0
- package/app/types/localModels.ts +73 -0
- package/app/types/merge.ts +71 -0
- package/app/types/models.ts +157 -0
- package/app/types/notifications.ts +74 -0
- package/app/types/recurring.ts +69 -0
- package/app/types/releaseHealth.ts +31 -0
- package/app/types/requirements.ts +61 -0
- package/app/types/services.ts +27 -0
- package/app/types/slack.ts +57 -0
- package/app/types/tasks.ts +82 -0
- package/app/types/tracker.ts +18 -0
- package/app/utils/agentOutput.spec.ts +128 -0
- package/app/utils/agentOutput.ts +173 -0
- package/app/utils/catalog.spec.ts +112 -0
- package/app/utils/catalog.ts +455 -0
- package/app/utils/dnd.ts +29 -0
- package/app/utils/observability.ts +52 -0
- package/app/utils/pipelineRender.ts +151 -0
- package/nuxt.config.ts +55 -0
- package/package.json +45 -0
|
@@ -0,0 +1,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>
|