@cat-factory/app 0.29.1 → 0.30.1
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/app/components/board/BoardCanvas.vue +7 -1
- package/app/components/board/nodes/TaskCard.vue +3 -0
- package/app/components/documents/DocumentImportModal.vue +2 -7
- package/app/components/documents/DocumentSourceConnectModal.vue +2 -7
- package/app/components/github/GitHubPanel.vue +2 -7
- package/app/components/layout/BoardToolbar.vue +7 -1
- package/app/components/panels/DecisionModal.vue +2 -1
- package/app/components/panels/ObservabilityPanel.vue +177 -5
- package/app/components/providers/VendorCredentialsModal.vue +2 -7
- package/app/components/settings/LocalModelEndpointsPanel.vue +2 -7
- package/app/components/settings/ObservabilityConnectionPanel.vue +2 -7
- package/app/components/settings/OpenRouterCatalogPanel.vue +2 -7
- package/app/components/settings/ProviderConnectionPanel.vue +2 -7
- package/app/components/settings/UserSecretsSection.vue +2 -7
- package/app/components/settings/WorkspaceSettingsPanel.vue +21 -7
- package/app/components/slack/SlackPanel.vue +2 -7
- package/app/components/tasks/TaskImportModal.vue +2 -7
- package/app/components/tasks/TaskSourceConnectModal.vue +2 -7
- package/app/composables/api/execution.ts +8 -0
- package/app/composables/useIntegrationBack.ts +20 -0
- package/app/stores/observability.ts +30 -1
- package/app/stores/workspaceSettings.ts +1 -0
- package/app/types/domain.ts +3 -0
- package/app/types/execution.ts +36 -0
- package/package.json +1 -1
|
@@ -188,7 +188,13 @@ async function onDrop(event: DragEvent) {
|
|
|
188
188
|
</script>
|
|
189
189
|
|
|
190
190
|
<template>
|
|
191
|
-
<div
|
|
191
|
+
<div
|
|
192
|
+
ref="boardEl"
|
|
193
|
+
data-testid="board-canvas"
|
|
194
|
+
class="relative h-full w-full"
|
|
195
|
+
@drop="onDrop"
|
|
196
|
+
@dragover="onDragOver"
|
|
197
|
+
>
|
|
192
198
|
<VueFlow
|
|
193
199
|
:id="BOARD_FLOW_ID"
|
|
194
200
|
:nodes="nodes"
|
|
@@ -173,6 +173,8 @@ function selectTask() {
|
|
|
173
173
|
<div
|
|
174
174
|
v-if="task && statusMeta"
|
|
175
175
|
:data-block-id="task.id"
|
|
176
|
+
:data-status="task.status"
|
|
177
|
+
data-testid="task-card"
|
|
176
178
|
class="nodrag w-full cursor-pointer rounded-lg border bg-slate-950/70 p-2 text-left transition"
|
|
177
179
|
:class="[
|
|
178
180
|
selected ? 'border-white' : 'border-slate-700 hover:border-slate-500',
|
|
@@ -273,6 +275,7 @@ function selectTask() {
|
|
|
273
275
|
color="warning"
|
|
274
276
|
variant="soft"
|
|
275
277
|
size="xs"
|
|
278
|
+
data-testid="task-resolve"
|
|
276
279
|
:icon="attention.icon"
|
|
277
280
|
@click.stop="attention.open()"
|
|
278
281
|
>
|
|
@@ -17,6 +17,7 @@ const open = computed({
|
|
|
17
17
|
if (!v) ui.closeDocumentImport()
|
|
18
18
|
},
|
|
19
19
|
})
|
|
20
|
+
const back = useIntegrationBack(open)
|
|
20
21
|
|
|
21
22
|
const targetFrameId = computed(() => ui.documentImport?.targetFrameId ?? null)
|
|
22
23
|
const targetFrameTitle = computed(() =>
|
|
@@ -76,13 +77,7 @@ function preview(externalId: string) {
|
|
|
76
77
|
<template>
|
|
77
78
|
<UModal v-model:open="open" title="Import from a document source">
|
|
78
79
|
<template #title>
|
|
79
|
-
<IntegrationBackTitle
|
|
80
|
-
title="Import from a document source"
|
|
81
|
-
@back="
|
|
82
|
-
open = false
|
|
83
|
-
ui.openIntegrations()
|
|
84
|
-
"
|
|
85
|
-
/>
|
|
80
|
+
<IntegrationBackTitle title="Import from a document source" @back="back" />
|
|
86
81
|
</template>
|
|
87
82
|
<template #body>
|
|
88
83
|
<div v-if="!documents.anyConnected" class="space-y-3 text-center">
|
|
@@ -25,6 +25,7 @@ const open = computed({
|
|
|
25
25
|
if (!v) ui.closeDocumentConnect()
|
|
26
26
|
},
|
|
27
27
|
})
|
|
28
|
+
const back = useIntegrationBack(open)
|
|
28
29
|
|
|
29
30
|
/** One value per credential field, reset whenever the modal (re)opens. */
|
|
30
31
|
const values = ref<Record<string, string>>({})
|
|
@@ -80,13 +81,7 @@ async function disconnect() {
|
|
|
80
81
|
<template>
|
|
81
82
|
<UModal v-model:open="open" :title="descriptor?.label ?? 'Connect source'">
|
|
82
83
|
<template #title>
|
|
83
|
-
<IntegrationBackTitle
|
|
84
|
-
:title="descriptor?.label ?? 'Connect source'"
|
|
85
|
-
@back="
|
|
86
|
-
open = false
|
|
87
|
-
ui.openIntegrations()
|
|
88
|
-
"
|
|
89
|
-
/>
|
|
84
|
+
<IntegrationBackTitle :title="descriptor?.label ?? 'Connect source'" @back="back" />
|
|
90
85
|
</template>
|
|
91
86
|
<template #body>
|
|
92
87
|
<div v-if="descriptor" class="space-y-4">
|
|
@@ -22,6 +22,7 @@ const open = computed({
|
|
|
22
22
|
if (!v) ui.closeGitHub()
|
|
23
23
|
},
|
|
24
24
|
})
|
|
25
|
+
const back = useIntegrationBack(open)
|
|
25
26
|
|
|
26
27
|
// On open: refresh projections when connected. The not-connected state renders
|
|
27
28
|
// <GitHubConnect>, which discovers and links installations on its own.
|
|
@@ -201,13 +202,7 @@ async function merge(pr: GitHubPullRequest) {
|
|
|
201
202
|
<template>
|
|
202
203
|
<UModal v-model:open="open" title="GitHub" :ui="{ content: 'max-w-2xl' }">
|
|
203
204
|
<template #title>
|
|
204
|
-
<IntegrationBackTitle
|
|
205
|
-
title="GitHub"
|
|
206
|
-
@back="
|
|
207
|
-
open = false
|
|
208
|
-
ui.openIntegrations()
|
|
209
|
-
"
|
|
210
|
-
/>
|
|
205
|
+
<IntegrationBackTitle title="GitHub" @back="back" />
|
|
211
206
|
</template>
|
|
212
207
|
<template #body>
|
|
213
208
|
<div class="space-y-5">
|
|
@@ -108,7 +108,13 @@ const decisionItems = computed(() =>
|
|
|
108
108
|
|
|
109
109
|
<!-- decisions queue -->
|
|
110
110
|
<UDropdownMenu v-if="execution.pendingDecisionCount" :items="decisionItems">
|
|
111
|
-
<UButton
|
|
111
|
+
<UButton
|
|
112
|
+
color="warning"
|
|
113
|
+
variant="soft"
|
|
114
|
+
size="sm"
|
|
115
|
+
icon="i-lucide-circle-help"
|
|
116
|
+
data-testid="decision-badge"
|
|
117
|
+
>
|
|
112
118
|
{{ execution.pendingDecisionCount }} decision{{
|
|
113
119
|
execution.pendingDecisionCount === 1 ? '' : 's'
|
|
114
120
|
}}
|
|
@@ -32,7 +32,7 @@ function choose(option: string) {
|
|
|
32
32
|
<template>
|
|
33
33
|
<UModal v-model:open="open" title="Decision required">
|
|
34
34
|
<template #body>
|
|
35
|
-
<div v-if="decision && agent" class="space-y-4">
|
|
35
|
+
<div v-if="decision && agent" class="space-y-4" data-testid="decision-modal">
|
|
36
36
|
<div class="flex items-center gap-2 text-sm text-slate-400">
|
|
37
37
|
<div
|
|
38
38
|
class="flex h-8 w-8 items-center justify-center rounded-lg"
|
|
@@ -56,6 +56,7 @@ function choose(option: string) {
|
|
|
56
56
|
color="primary"
|
|
57
57
|
variant="soft"
|
|
58
58
|
block
|
|
59
|
+
data-testid="decision-option"
|
|
59
60
|
class="justify-start"
|
|
60
61
|
@click="choose(opt)"
|
|
61
62
|
>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, reactive, watch } from 'vue'
|
|
2
|
+
import { computed, reactive, ref, watch } from 'vue'
|
|
3
3
|
import { onKeyStroke } from '@vueuse/core'
|
|
4
|
-
import type { LlmCallMetric } from '~/types/execution'
|
|
4
|
+
import type { AgentContextSnapshot, LlmCallMetric } from '~/types/execution'
|
|
5
5
|
import { agentKindMeta } from '~/utils/catalog'
|
|
6
6
|
import { formatMs, formatTokens, pct } from '~/utils/observability'
|
|
7
7
|
|
|
@@ -32,11 +32,38 @@ const error = computed(() =>
|
|
|
32
32
|
executionId.value ? (observability.errors[executionId.value] ?? null) : null,
|
|
33
33
|
)
|
|
34
34
|
|
|
35
|
-
//
|
|
35
|
+
// Which view is shown: the per-call model activity, or the complete provided context.
|
|
36
|
+
const view = ref<'calls' | 'context'>('calls')
|
|
37
|
+
|
|
38
|
+
const contextSnapshots = computed<AgentContextSnapshot[]>(() =>
|
|
39
|
+
executionId.value ? observability.contextFor(executionId.value) : [],
|
|
40
|
+
)
|
|
41
|
+
const contextLoading = computed(
|
|
42
|
+
() => !!executionId.value && observability.isContextLoading(executionId.value),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
// Load (and refresh) whenever a different run's panel opens. Reset to the calls view
|
|
46
|
+
// and load both the calls and the provided-context snapshots.
|
|
36
47
|
watch(executionId, (id) => {
|
|
37
|
-
if (id)
|
|
48
|
+
if (id) {
|
|
49
|
+
view.value = 'calls'
|
|
50
|
+
void observability.load(id)
|
|
51
|
+
void observability.loadContext(id)
|
|
52
|
+
}
|
|
38
53
|
})
|
|
39
54
|
|
|
55
|
+
const expandedCtx = reactive<Record<string, boolean>>({})
|
|
56
|
+
function toggleCtx(s: AgentContextSnapshot) {
|
|
57
|
+
expandedCtx[s.id] = !expandedCtx[s.id]
|
|
58
|
+
}
|
|
59
|
+
function prettyExtras(extras: Record<string, unknown>): string {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.stringify(extras, null, 2)
|
|
62
|
+
} catch {
|
|
63
|
+
return String(extras)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
40
67
|
// Run-level totals, derived from the loaded calls.
|
|
41
68
|
const totals = computed(() => {
|
|
42
69
|
const c = calls.value
|
|
@@ -124,7 +151,32 @@ function exportJson() {
|
|
|
124
151
|
</p>
|
|
125
152
|
</div>
|
|
126
153
|
<div class="ml-auto flex items-center gap-1.5">
|
|
154
|
+
<div class="mr-1 flex rounded-lg border border-slate-800 p-0.5 text-[12px]">
|
|
155
|
+
<button
|
|
156
|
+
class="rounded-md px-2.5 py-1 transition"
|
|
157
|
+
:class="
|
|
158
|
+
view === 'calls'
|
|
159
|
+
? 'bg-slate-800 text-slate-100'
|
|
160
|
+
: 'text-slate-400 hover:text-slate-200'
|
|
161
|
+
"
|
|
162
|
+
@click="view = 'calls'"
|
|
163
|
+
>
|
|
164
|
+
Model activity
|
|
165
|
+
</button>
|
|
166
|
+
<button
|
|
167
|
+
class="rounded-md px-2.5 py-1 transition"
|
|
168
|
+
:class="
|
|
169
|
+
view === 'context'
|
|
170
|
+
? 'bg-slate-800 text-slate-100'
|
|
171
|
+
: 'text-slate-400 hover:text-slate-200'
|
|
172
|
+
"
|
|
173
|
+
@click="view = 'context'"
|
|
174
|
+
>
|
|
175
|
+
Provided context
|
|
176
|
+
</button>
|
|
177
|
+
</div>
|
|
127
178
|
<UButton
|
|
179
|
+
v-if="view === 'calls'"
|
|
128
180
|
icon="i-lucide-download"
|
|
129
181
|
color="neutral"
|
|
130
182
|
variant="soft"
|
|
@@ -148,7 +200,7 @@ function exportJson() {
|
|
|
148
200
|
</header>
|
|
149
201
|
|
|
150
202
|
<div class="flex-1 overflow-auto px-6 py-6">
|
|
151
|
-
<div class="mx-auto max-w-4xl space-y-5">
|
|
203
|
+
<div v-if="view === 'calls'" class="mx-auto max-w-4xl space-y-5">
|
|
152
204
|
<!-- run-level summary -->
|
|
153
205
|
<section class="rounded-xl border border-slate-800 bg-slate-900/50 p-4">
|
|
154
206
|
<dl class="grid grid-cols-2 gap-x-6 gap-y-3 text-[13px] sm:grid-cols-4">
|
|
@@ -333,6 +385,126 @@ function exportJson() {
|
|
|
333
385
|
</li>
|
|
334
386
|
</ul>
|
|
335
387
|
</div>
|
|
388
|
+
|
|
389
|
+
<!-- Provided context: the complete context each container agent was given. -->
|
|
390
|
+
<div v-else class="mx-auto max-w-4xl space-y-5">
|
|
391
|
+
<p
|
|
392
|
+
v-if="contextLoading && !contextSnapshots.length"
|
|
393
|
+
class="flex items-center justify-center gap-2 py-8 text-center text-sm text-slate-500"
|
|
394
|
+
>
|
|
395
|
+
<UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" /> Loading provided
|
|
396
|
+
context…
|
|
397
|
+
</p>
|
|
398
|
+
<p
|
|
399
|
+
v-else-if="!contextSnapshots.length"
|
|
400
|
+
class="rounded-lg border border-dashed border-slate-800 py-8 text-center text-sm text-slate-500"
|
|
401
|
+
>
|
|
402
|
+
No agent context stored for this run. It is captured per dispatch when the workspace
|
|
403
|
+
has "Store full agent context" enabled.
|
|
404
|
+
</p>
|
|
405
|
+
|
|
406
|
+
<ul v-else class="space-y-2">
|
|
407
|
+
<li
|
|
408
|
+
v-for="s in contextSnapshots"
|
|
409
|
+
:key="s.id"
|
|
410
|
+
class="overflow-hidden rounded-xl border border-slate-800 bg-slate-900/40"
|
|
411
|
+
>
|
|
412
|
+
<button
|
|
413
|
+
class="flex w-full items-center gap-3 px-4 py-2.5 text-left transition hover:bg-slate-900/70"
|
|
414
|
+
@click="toggleCtx(s)"
|
|
415
|
+
>
|
|
416
|
+
<UIcon
|
|
417
|
+
name="i-lucide-chevron-right"
|
|
418
|
+
class="h-4 w-4 shrink-0 text-slate-500 transition-transform"
|
|
419
|
+
:class="expandedCtx[s.id] ? 'rotate-90' : ''"
|
|
420
|
+
/>
|
|
421
|
+
<UIcon
|
|
422
|
+
:name="agentMeta(s.agentKind).icon"
|
|
423
|
+
class="h-4 w-4 shrink-0"
|
|
424
|
+
:style="{ color: agentMeta(s.agentKind).color }"
|
|
425
|
+
/>
|
|
426
|
+
<span class="text-[13px] text-slate-200">{{ agentMeta(s.agentKind).label }}</span>
|
|
427
|
+
<span v-if="s.model" class="hidden truncate text-[11px] text-slate-500 sm:inline">
|
|
428
|
+
{{ s.model }}
|
|
429
|
+
</span>
|
|
430
|
+
<div
|
|
431
|
+
class="ml-auto flex items-center gap-2.5 text-[11px] tabular-nums text-slate-400"
|
|
432
|
+
>
|
|
433
|
+
<span :title="'Injected context files'">{{ s.contextFiles.length }} files</span>
|
|
434
|
+
<span :title="'Best-practice fragments'"
|
|
435
|
+
>{{ s.fragments.length }} fragments</span
|
|
436
|
+
>
|
|
437
|
+
<span class="hidden text-slate-600 md:inline">{{ clock(s.createdAt) }}</span>
|
|
438
|
+
</div>
|
|
439
|
+
</button>
|
|
440
|
+
|
|
441
|
+
<div v-if="expandedCtx[s.id]" class="border-t border-slate-800 px-4 py-3 space-y-3">
|
|
442
|
+
<div>
|
|
443
|
+
<div class="mb-1 text-[11px] uppercase tracking-wide text-slate-500">
|
|
444
|
+
System prompt
|
|
445
|
+
</div>
|
|
446
|
+
<pre
|
|
447
|
+
class="max-h-72 overflow-auto rounded-lg bg-slate-950/70 p-3 text-[11px] leading-relaxed text-slate-300"
|
|
448
|
+
>{{ s.systemPrompt || '—' }}</pre
|
|
449
|
+
>
|
|
450
|
+
</div>
|
|
451
|
+
<div>
|
|
452
|
+
<div class="mb-1 text-[11px] uppercase tracking-wide text-slate-500">
|
|
453
|
+
User prompt
|
|
454
|
+
</div>
|
|
455
|
+
<pre
|
|
456
|
+
class="max-h-72 overflow-auto rounded-lg bg-slate-950/70 p-3 text-[11px] leading-relaxed text-slate-300"
|
|
457
|
+
>{{ s.userPrompt || '—' }}</pre
|
|
458
|
+
>
|
|
459
|
+
</div>
|
|
460
|
+
<div v-if="s.fragments.length">
|
|
461
|
+
<div class="mb-1 text-[11px] uppercase tracking-wide text-slate-500">
|
|
462
|
+
Best-practice fragments
|
|
463
|
+
</div>
|
|
464
|
+
<div
|
|
465
|
+
v-for="f in s.fragments"
|
|
466
|
+
:key="f.id"
|
|
467
|
+
class="mb-2 rounded-lg bg-slate-950/70 p-3"
|
|
468
|
+
>
|
|
469
|
+
<div class="mb-1 text-[11px] text-slate-400">{{ f.id }}</div>
|
|
470
|
+
<pre
|
|
471
|
+
class="max-h-48 overflow-auto text-[11px] leading-relaxed text-slate-300"
|
|
472
|
+
>{{ f.body }}</pre
|
|
473
|
+
>
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
<div v-if="s.contextFiles.length">
|
|
477
|
+
<div class="mb-1 text-[11px] uppercase tracking-wide text-slate-500">
|
|
478
|
+
Injected context files
|
|
479
|
+
</div>
|
|
480
|
+
<div
|
|
481
|
+
v-for="file in s.contextFiles"
|
|
482
|
+
:key="file.path"
|
|
483
|
+
class="mb-2 rounded-lg bg-slate-950/70 p-3"
|
|
484
|
+
>
|
|
485
|
+
<div class="mb-1 text-[11px] text-slate-400">
|
|
486
|
+
{{ file.title }}
|
|
487
|
+
<span class="text-slate-600">· {{ file.path }}</span>
|
|
488
|
+
</div>
|
|
489
|
+
<pre
|
|
490
|
+
class="max-h-72 overflow-auto text-[11px] leading-relaxed text-slate-300"
|
|
491
|
+
>{{ file.content }}</pre
|
|
492
|
+
>
|
|
493
|
+
</div>
|
|
494
|
+
</div>
|
|
495
|
+
<div>
|
|
496
|
+
<div class="mb-1 text-[11px] uppercase tracking-wide text-slate-500">
|
|
497
|
+
Details
|
|
498
|
+
</div>
|
|
499
|
+
<pre
|
|
500
|
+
class="max-h-48 overflow-auto rounded-lg bg-slate-950/70 p-3 text-[11px] leading-relaxed text-slate-400"
|
|
501
|
+
>{{ prettyExtras(s.extras) }}</pre
|
|
502
|
+
>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
</li>
|
|
506
|
+
</ul>
|
|
507
|
+
</div>
|
|
336
508
|
</div>
|
|
337
509
|
</div>
|
|
338
510
|
</Transition>
|
|
@@ -18,6 +18,7 @@ const open = computed({
|
|
|
18
18
|
get: () => ui.vendorCredentialsOpen,
|
|
19
19
|
set: (v: boolean) => (v ? ui.openVendorCredentials() : ui.closeVendorCredentials()),
|
|
20
20
|
})
|
|
21
|
+
const back = useIntegrationBack(open)
|
|
21
22
|
|
|
22
23
|
// Horizontal tabs replace the old long vertical scroll: each credential kind is its own
|
|
23
24
|
// section (pooled subscriptions, direct vendor keys, proxy/gateway keys, personal subs).
|
|
@@ -118,13 +119,7 @@ function vendorLabel(v: SubscriptionVendor): string {
|
|
|
118
119
|
<template>
|
|
119
120
|
<UModal v-model:open="open" title="LLM Vendors" :ui="{ content: 'max-w-2xl' }">
|
|
120
121
|
<template #title>
|
|
121
|
-
<IntegrationBackTitle
|
|
122
|
-
title="LLM Vendors"
|
|
123
|
-
@back="
|
|
124
|
-
open = false
|
|
125
|
-
ui.openIntegrations()
|
|
126
|
-
"
|
|
127
|
-
/>
|
|
122
|
+
<IntegrationBackTitle title="LLM Vendors" @back="back" />
|
|
128
123
|
</template>
|
|
129
124
|
<template #body>
|
|
130
125
|
<UTabs
|
|
@@ -17,6 +17,7 @@ const open = computed({
|
|
|
17
17
|
get: () => ui.localModelsOpen,
|
|
18
18
|
set: (v: boolean) => (v ? ui.openLocalModels() : ui.closeLocalModels()),
|
|
19
19
|
})
|
|
20
|
+
const back = useIntegrationBack(open)
|
|
20
21
|
|
|
21
22
|
// Load the user's endpoints whenever the panel opens (loaded independently of the
|
|
22
23
|
// workspace snapshot, like personal subscriptions).
|
|
@@ -154,13 +155,7 @@ async function remove(p: LocalRunner) {
|
|
|
154
155
|
<template>
|
|
155
156
|
<UModal v-model:open="open" title="My local runners" :ui="{ content: 'max-w-2xl' }">
|
|
156
157
|
<template #title>
|
|
157
|
-
<IntegrationBackTitle
|
|
158
|
-
title="My local runners"
|
|
159
|
-
@back="
|
|
160
|
-
open = false
|
|
161
|
-
ui.openIntegrations()
|
|
162
|
-
"
|
|
163
|
-
/>
|
|
158
|
+
<IntegrationBackTitle title="My local runners" @back="back" />
|
|
164
159
|
</template>
|
|
165
160
|
<template #body>
|
|
166
161
|
<div class="space-y-4">
|
|
@@ -16,6 +16,7 @@ const open = computed({
|
|
|
16
16
|
get: () => ui.observabilityConnectionOpen,
|
|
17
17
|
set: (v: boolean) => (v ? ui.openObservabilityConnection() : ui.closeObservabilityConnection()),
|
|
18
18
|
})
|
|
19
|
+
const back = useIntegrationBack(open)
|
|
19
20
|
|
|
20
21
|
// The providers a user can connect. Datadog only today; the picker is ready for more.
|
|
21
22
|
const PROVIDERS: { value: ObservabilityProviderKind; label: string }[] = [
|
|
@@ -130,13 +131,7 @@ const connectedLabel = computed(() => {
|
|
|
130
131
|
<template>
|
|
131
132
|
<UModal v-model:open="open" title="Post-release health" :ui="{ content: 'max-w-lg' }">
|
|
132
133
|
<template #title>
|
|
133
|
-
<IntegrationBackTitle
|
|
134
|
-
title="Post-release health"
|
|
135
|
-
@back="
|
|
136
|
-
open = false
|
|
137
|
-
ui.openIntegrations()
|
|
138
|
-
"
|
|
139
|
-
/>
|
|
134
|
+
<IntegrationBackTitle title="Post-release health" @back="back" />
|
|
140
135
|
</template>
|
|
141
136
|
<template #body>
|
|
142
137
|
<div class="space-y-4">
|
|
@@ -22,6 +22,7 @@ const open = computed({
|
|
|
22
22
|
get: () => ui.openRouterOpen,
|
|
23
23
|
set: (v: boolean) => (v ? ui.openOpenRouter() : ui.closeOpenRouter()),
|
|
24
24
|
})
|
|
25
|
+
const back = useIntegrationBack(open)
|
|
25
26
|
|
|
26
27
|
// Popular slugs offered by "Enable recommended" — these mirror the curated `openrouter`
|
|
27
28
|
// refs in the backend MODEL_CATALOG. Only the ones present in the live browse list are
|
|
@@ -195,13 +196,7 @@ function manageKeys() {
|
|
|
195
196
|
<template>
|
|
196
197
|
<UModal v-model:open="open" title="OpenRouter" :ui="{ content: 'max-w-2xl' }">
|
|
197
198
|
<template #title>
|
|
198
|
-
<IntegrationBackTitle
|
|
199
|
-
title="OpenRouter"
|
|
200
|
-
@back="
|
|
201
|
-
open = false
|
|
202
|
-
ui.openIntegrations()
|
|
203
|
-
"
|
|
204
|
-
/>
|
|
199
|
+
<IntegrationBackTitle title="OpenRouter" @back="back" />
|
|
205
200
|
</template>
|
|
206
201
|
<template #body>
|
|
207
202
|
<div class="space-y-4">
|
|
@@ -38,6 +38,7 @@ const open = computed({
|
|
|
38
38
|
if (!v) ui.closeProviderConnection()
|
|
39
39
|
},
|
|
40
40
|
})
|
|
41
|
+
const back = useIntegrationBack(open)
|
|
41
42
|
|
|
42
43
|
const meta = computed(() => (kind.value ? META[kind.value] : null))
|
|
43
44
|
const descriptor = computed(() => (kind.value ? store.descriptorFor(kind.value) : null))
|
|
@@ -224,13 +225,7 @@ function fieldHelp(key: string): string | undefined {
|
|
|
224
225
|
<template>
|
|
225
226
|
<UModal v-model:open="open" :title="meta?.title ?? 'Provider'" :ui="{ content: 'max-w-xl' }">
|
|
226
227
|
<template #title>
|
|
227
|
-
<IntegrationBackTitle
|
|
228
|
-
:title="meta?.title ?? 'Provider'"
|
|
229
|
-
@back="
|
|
230
|
-
open = false
|
|
231
|
-
ui.openIntegrations()
|
|
232
|
-
"
|
|
233
|
-
/>
|
|
228
|
+
<IntegrationBackTitle :title="meta?.title ?? 'Provider'" @back="back" />
|
|
234
229
|
</template>
|
|
235
230
|
<template #body>
|
|
236
231
|
<div v-if="descriptor" class="space-y-4">
|
|
@@ -16,6 +16,7 @@ const open = computed({
|
|
|
16
16
|
get: () => ui.userSecretsOpen,
|
|
17
17
|
set: (v: boolean) => (v ? ui.openUserSecrets() : ui.closeUserSecrets()),
|
|
18
18
|
})
|
|
19
|
+
const back = useIntegrationBack(open)
|
|
19
20
|
|
|
20
21
|
// The kind being edited (only `github_pat` today; descriptors drive the rest generically).
|
|
21
22
|
const kind = ref<UserSecretKind>('github_pat')
|
|
@@ -125,13 +126,7 @@ async function remove() {
|
|
|
125
126
|
<template>
|
|
126
127
|
<UModal v-model:open="open" title="My GitHub token" :ui="{ content: 'max-w-xl' }">
|
|
127
128
|
<template #title>
|
|
128
|
-
<IntegrationBackTitle
|
|
129
|
-
title="My GitHub token"
|
|
130
|
-
@back="
|
|
131
|
-
open = false
|
|
132
|
-
ui.openIntegrations()
|
|
133
|
-
"
|
|
134
|
-
/>
|
|
129
|
+
<IntegrationBackTitle title="My GitHub token" @back="back" />
|
|
135
130
|
</template>
|
|
136
131
|
<template #body>
|
|
137
132
|
<div class="space-y-4">
|
|
@@ -22,6 +22,7 @@ const open = computed({
|
|
|
22
22
|
get: () => ui.workspaceSettingsOpen,
|
|
23
23
|
set: (v: boolean) => (v ? ui.openWorkspaceSettings() : ui.closeWorkspaceSettings()),
|
|
24
24
|
})
|
|
25
|
+
const back = useIntegrationBack(open)
|
|
25
26
|
|
|
26
27
|
// Which tab is shown — driven by the ui store so other surfaces (command bar,
|
|
27
28
|
// integrations) can deep-link straight to a tab.
|
|
@@ -66,6 +67,7 @@ const draft = reactive({
|
|
|
66
67
|
taskLimitMode: 'off' as TaskLimitMode,
|
|
67
68
|
taskLimitShared: 5 as number,
|
|
68
69
|
perType: {} as Record<CreateTaskType, number>,
|
|
70
|
+
storeAgentContext: true,
|
|
69
71
|
// Budget: empty string ⇒ "use the built-in default" (null on the wire).
|
|
70
72
|
spendCurrency: '',
|
|
71
73
|
spendMonthlyLimit: '',
|
|
@@ -79,6 +81,7 @@ function hydrate() {
|
|
|
79
81
|
draft.taskLimitShared = s.taskLimitShared ?? 5
|
|
80
82
|
const pt = s.taskLimitPerType ?? {}
|
|
81
83
|
for (const t of TASK_TYPES) draft.perType[t] = pt[t] ?? 3
|
|
84
|
+
draft.storeAgentContext = s.storeAgentContext
|
|
82
85
|
draft.spendCurrency = s.spendCurrency ?? ''
|
|
83
86
|
draft.spendMonthlyLimit = s.spendMonthlyLimit == null ? '' : String(s.spendMonthlyLimit)
|
|
84
87
|
draft.spendModelPrices = s.spendModelPrices ? JSON.stringify(s.spendModelPrices, null, 2) : ''
|
|
@@ -105,6 +108,7 @@ async function save() {
|
|
|
105
108
|
{} as Record<CreateTaskType, number>,
|
|
106
109
|
)
|
|
107
110
|
: null,
|
|
111
|
+
storeAgentContext: draft.storeAgentContext,
|
|
108
112
|
})
|
|
109
113
|
toast.add({ title: 'Settings saved', icon: 'i-lucide-check', color: 'success' })
|
|
110
114
|
} catch (e) {
|
|
@@ -162,13 +166,7 @@ async function saveBudget() {
|
|
|
162
166
|
<template>
|
|
163
167
|
<UModal v-model:open="open" title="Workspace settings" :ui="{ content: 'max-w-3xl' }">
|
|
164
168
|
<template #title>
|
|
165
|
-
<IntegrationBackTitle
|
|
166
|
-
title="Workspace settings"
|
|
167
|
-
@back="
|
|
168
|
-
open = false
|
|
169
|
-
ui.openIntegrations()
|
|
170
|
-
"
|
|
171
|
-
/>
|
|
169
|
+
<IntegrationBackTitle title="Workspace settings" @back="back" />
|
|
172
170
|
</template>
|
|
173
171
|
<template #body>
|
|
174
172
|
<UTabs
|
|
@@ -238,6 +236,22 @@ async function saveBudget() {
|
|
|
238
236
|
</div>
|
|
239
237
|
</section>
|
|
240
238
|
|
|
239
|
+
<!-- Agent observability -->
|
|
240
|
+
<section class="space-y-2">
|
|
241
|
+
<h3 class="text-sm font-semibold text-slate-200">Agent observability</h3>
|
|
242
|
+
<p class="text-[11px] text-slate-400">
|
|
243
|
+
Store the complete context provided to each agent — the composed prompts, the
|
|
244
|
+
best-practice fragments folded in, and the full content of the files injected into
|
|
245
|
+
its container — so it can be inspected later in the observability view. The bodies
|
|
246
|
+
are kept for the same window as the per-call LLM telemetry. Turn off to stop storing
|
|
247
|
+
it (existing snapshots are pruned by retention).
|
|
248
|
+
</p>
|
|
249
|
+
<label class="flex items-center gap-2">
|
|
250
|
+
<USwitch v-model="draft.storeAgentContext" size="sm" />
|
|
251
|
+
<span class="text-sm text-slate-200">Store full agent context</span>
|
|
252
|
+
</label>
|
|
253
|
+
</section>
|
|
254
|
+
|
|
241
255
|
<div class="flex justify-end">
|
|
242
256
|
<UButton
|
|
243
257
|
color="primary"
|
|
@@ -17,6 +17,7 @@ const open = computed({
|
|
|
17
17
|
get: () => ui.slackOpen,
|
|
18
18
|
set: (v: boolean) => (v ? ui.openSlack() : ui.closeSlack()),
|
|
19
19
|
})
|
|
20
|
+
const back = useIntegrationBack(open)
|
|
20
21
|
|
|
21
22
|
const ROUTABLE: { type: NotificationType; label: string }[] = [
|
|
22
23
|
{ type: 'merge_review', label: 'Merge review' },
|
|
@@ -143,13 +144,7 @@ async function saveMapping() {
|
|
|
143
144
|
<template>
|
|
144
145
|
<UModal v-model:open="open" title="Slack notifications" :ui="{ content: 'max-w-2xl' }">
|
|
145
146
|
<template #title>
|
|
146
|
-
<IntegrationBackTitle
|
|
147
|
-
title="Slack notifications"
|
|
148
|
-
@back="
|
|
149
|
-
open = false
|
|
150
|
-
ui.openIntegrations()
|
|
151
|
-
"
|
|
152
|
-
/>
|
|
147
|
+
<IntegrationBackTitle title="Slack notifications" @back="back" />
|
|
153
148
|
</template>
|
|
154
149
|
<template #body>
|
|
155
150
|
<div class="space-y-5">
|
|
@@ -21,6 +21,7 @@ const open = computed({
|
|
|
21
21
|
if (!v) ui.closeTaskImport()
|
|
22
22
|
},
|
|
23
23
|
})
|
|
24
|
+
const back = useIntegrationBack(open)
|
|
24
25
|
|
|
25
26
|
const source = ref<TaskSourceKind | undefined>(undefined)
|
|
26
27
|
const ref_ = ref('')
|
|
@@ -197,13 +198,7 @@ async function doSpawnEpic() {
|
|
|
197
198
|
<template>
|
|
198
199
|
<UModal v-model:open="open" :title="title">
|
|
199
200
|
<template #title>
|
|
200
|
-
<IntegrationBackTitle
|
|
201
|
-
:title="title"
|
|
202
|
-
@back="
|
|
203
|
-
open = false
|
|
204
|
-
ui.openIntegrations()
|
|
205
|
-
"
|
|
206
|
-
/>
|
|
201
|
+
<IntegrationBackTitle :title="title" @back="back" />
|
|
207
202
|
</template>
|
|
208
203
|
<template #body>
|
|
209
204
|
<!-- Empty state: no source offered (none connected/installed, or all disabled) -->
|
|
@@ -32,6 +32,7 @@ const open = computed({
|
|
|
32
32
|
if (!v) ui.closeTaskConnect()
|
|
33
33
|
},
|
|
34
34
|
})
|
|
35
|
+
const back = useIntegrationBack(open)
|
|
35
36
|
|
|
36
37
|
/** One value per credential field, reset whenever the modal (re)opens. */
|
|
37
38
|
const values = ref<Record<string, string>>({})
|
|
@@ -107,13 +108,7 @@ async function toggleEnabled(enabled: boolean) {
|
|
|
107
108
|
<template>
|
|
108
109
|
<UModal v-model:open="open" :title="descriptor?.label ?? 'Task source'">
|
|
109
110
|
<template #title>
|
|
110
|
-
<IntegrationBackTitle
|
|
111
|
-
:title="descriptor?.label ?? 'Task source'"
|
|
112
|
-
@back="
|
|
113
|
-
open = false
|
|
114
|
-
ui.openIntegrations()
|
|
115
|
-
"
|
|
116
|
-
/>
|
|
111
|
+
<IntegrationBackTitle :title="descriptor?.label ?? 'Task source'" @back="back" />
|
|
117
112
|
</template>
|
|
118
113
|
<template #body>
|
|
119
114
|
<div v-if="descriptor" class="space-y-4">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Block, ExecutionInstance } from '~/types/domain'
|
|
2
2
|
import type {
|
|
3
|
+
AgentContextSnapshot,
|
|
3
4
|
IterationCapChoice,
|
|
4
5
|
LlmCallMetric,
|
|
5
6
|
LlmMetricsExport,
|
|
@@ -120,6 +121,13 @@ export function executionApi({ http, ws, pwHeaders }: ApiContext) {
|
|
|
120
121
|
`${ws(workspaceId)}/executions/${encodeURIComponent(executionId)}/llm-metrics/export`,
|
|
121
122
|
),
|
|
122
123
|
|
|
124
|
+
// The complete provided context per container-agent dispatch (composed prompts,
|
|
125
|
+
// folded-in fragments, injected files). Empty when not wired / storing is off.
|
|
126
|
+
getAgentContext: (workspaceId: string, executionId: string) =>
|
|
127
|
+
http<{ executionId: string; snapshots: AgentContextSnapshot[] }>(
|
|
128
|
+
`${ws(workspaceId)}/executions/${encodeURIComponent(executionId)}/agent-context`,
|
|
129
|
+
),
|
|
130
|
+
|
|
123
131
|
// ---- spend safeguard --------------------------------------------------
|
|
124
132
|
resumeSpend: (workspaceId: string) =>
|
|
125
133
|
http<ExecutionInstance[]>(`${ws(workspaceId)}/spend/resume`, { method: 'POST' }),
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Ref, WritableComputedRef } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The shared "back to Integrations" handler for an integration sub-panel's modal header.
|
|
5
|
+
* Every panel reached from the Integrations hub closes itself and reopens the hub when its
|
|
6
|
+
* {@link IntegrationBackTitle} Back control fires `@back`. Centralising it keeps the ~13
|
|
7
|
+
* panels from each re-implementing the two-step close-then-reopen inline — which also
|
|
8
|
+
* dodges a Vue SFC-compiler trap: a multi-statement inline template handler
|
|
9
|
+
* (`open = false` ⏎ `ui.openIntegrations()`) is rejected at build time, so callers had to
|
|
10
|
+
* resort to an obscure comma-operator expression. A named handler reads clearly instead.
|
|
11
|
+
*
|
|
12
|
+
* Pass the panel's `open` model (the writable ref/computed bound to its `UModal`).
|
|
13
|
+
*/
|
|
14
|
+
export function useIntegrationBack(open: Ref<boolean> | WritableComputedRef<boolean>) {
|
|
15
|
+
const ui = useUiStore()
|
|
16
|
+
return () => {
|
|
17
|
+
open.value = false
|
|
18
|
+
ui.openIntegrations()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { ref } from 'vue'
|
|
3
|
-
import type { LlmCallActivity, LlmCallMetric } from '~/types/execution'
|
|
3
|
+
import type { AgentContextSnapshot, LlmCallActivity, LlmCallMetric } from '~/types/execution'
|
|
4
4
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -19,6 +19,10 @@ export const useObservabilityStore = defineStore('observability', () => {
|
|
|
19
19
|
|
|
20
20
|
/** Per-execution-id call list (newest first). */
|
|
21
21
|
const callsByExecution = ref<Record<string, LlmCallMetric[]>>({})
|
|
22
|
+
/** Per-execution-id provided-context snapshot list (newest first). */
|
|
23
|
+
const contextByExecution = ref<Record<string, AgentContextSnapshot[]>>({})
|
|
24
|
+
/** Execution ids whose context is currently loading. */
|
|
25
|
+
const contextLoading = ref<Set<string>>(new Set())
|
|
22
26
|
/** Execution ids currently loading. */
|
|
23
27
|
const loading = ref<Set<string>>(new Set())
|
|
24
28
|
/** Execution ids currently exporting. */
|
|
@@ -109,6 +113,27 @@ export const useObservabilityStore = defineStore('observability', () => {
|
|
|
109
113
|
callsByExecution.value = { ...callsByExecution.value, [executionId]: [row, ...existing] }
|
|
110
114
|
}
|
|
111
115
|
|
|
116
|
+
function contextFor(executionId: string): AgentContextSnapshot[] {
|
|
117
|
+
return contextByExecution.value[executionId] ?? []
|
|
118
|
+
}
|
|
119
|
+
function isContextLoading(executionId: string): boolean {
|
|
120
|
+
return contextLoading.value.has(executionId)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Load (or refresh) the per-dispatch provided-context snapshots for a run. */
|
|
124
|
+
async function loadContext(executionId: string) {
|
|
125
|
+
if (!workspace.workspaceId) return
|
|
126
|
+
withFlag(contextLoading, executionId, true)
|
|
127
|
+
try {
|
|
128
|
+
const { snapshots } = await api.getAgentContext(workspace.requireId(), executionId)
|
|
129
|
+
contextByExecution.value = { ...contextByExecution.value, [executionId]: snapshots }
|
|
130
|
+
} catch {
|
|
131
|
+
// Best-effort: the panel shows an empty state; nothing is persisted client-side.
|
|
132
|
+
} finally {
|
|
133
|
+
withFlag(contextLoading, executionId, false)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
112
137
|
/**
|
|
113
138
|
* Fetch the LLM-friendly export bundle and trigger a client-side download. The
|
|
114
139
|
* events socket auths via a Bearer header (a plain `<a download>` can't), so we
|
|
@@ -140,5 +165,9 @@ export const useObservabilityStore = defineStore('observability', () => {
|
|
|
140
165
|
load,
|
|
141
166
|
appendCall,
|
|
142
167
|
downloadExport,
|
|
168
|
+
contextByExecution,
|
|
169
|
+
contextFor,
|
|
170
|
+
isContextLoading,
|
|
171
|
+
loadContext,
|
|
143
172
|
}
|
|
144
173
|
})
|
package/app/types/domain.ts
CHANGED
|
@@ -493,6 +493,8 @@ export interface WorkspaceSettings {
|
|
|
493
493
|
taskLimitShared: number | null
|
|
494
494
|
/** The per-type caps, when `taskLimitMode` is `per_type` (type → cap). */
|
|
495
495
|
taskLimitPerType: Partial<Record<CreateTaskType, number>> | null
|
|
496
|
+
/** Whether to store the complete provided-context snapshot for each container agent. */
|
|
497
|
+
storeAgentContext: boolean
|
|
496
498
|
/** Spend budget currency (ISO 4217). Null ⇒ the built-in default (`EUR`). */
|
|
497
499
|
spendCurrency: string | null
|
|
498
500
|
/** Monthly spend budget in `spendCurrency`. Null ⇒ the built-in default. */
|
|
@@ -507,6 +509,7 @@ export interface UpdateWorkspaceSettingsInput {
|
|
|
507
509
|
taskLimitMode?: TaskLimitMode
|
|
508
510
|
taskLimitShared?: number | null
|
|
509
511
|
taskLimitPerType?: Partial<Record<CreateTaskType, number>> | null
|
|
512
|
+
storeAgentContext?: boolean
|
|
510
513
|
spendCurrency?: string | null
|
|
511
514
|
spendMonthlyLimit?: number | null
|
|
512
515
|
spendModelPrices?: Record<string, { inputPerMillion: number; outputPerMillion: number }> | null
|
package/app/types/execution.ts
CHANGED
|
@@ -229,6 +229,42 @@ export type LlmCallActivity = Omit<
|
|
|
229
229
|
'promptText' | 'responseText' | 'reasoningText' | 'promptPrefixCount' | 'promptHash'
|
|
230
230
|
>
|
|
231
231
|
|
|
232
|
+
/** One best-practice fragment folded into an agent's system prompt. */
|
|
233
|
+
export interface AgentContextFragment {
|
|
234
|
+
id: string
|
|
235
|
+
body: string
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** One file injected into the agent's container as context, with its full body. */
|
|
239
|
+
export interface AgentContextFile {
|
|
240
|
+
path: string
|
|
241
|
+
title: string
|
|
242
|
+
url: string
|
|
243
|
+
content: string
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The complete, redacted context provided to one container-agent dispatch: the composed
|
|
248
|
+
* system + user prompts, the fragment bodies folded in, and the full content of the files
|
|
249
|
+
* injected into the container. Loaded on demand for the observability view. Mirrors the
|
|
250
|
+
* backend `AgentContextSnapshot` (it never carries any credential).
|
|
251
|
+
*/
|
|
252
|
+
export interface AgentContextSnapshot {
|
|
253
|
+
id: string
|
|
254
|
+
workspaceId: string
|
|
255
|
+
executionId: string
|
|
256
|
+
agentKind: string
|
|
257
|
+
stepIndex: number
|
|
258
|
+
createdAt: number
|
|
259
|
+
model: string | null
|
|
260
|
+
harness: string | null
|
|
261
|
+
systemPrompt: string
|
|
262
|
+
userPrompt: string
|
|
263
|
+
fragments: AgentContextFragment[]
|
|
264
|
+
contextFiles: AgentContextFile[]
|
|
265
|
+
extras: Record<string, unknown>
|
|
266
|
+
}
|
|
267
|
+
|
|
232
268
|
/** One per-agent-kind insight in the LLM-friendly export (rollup + derived ratios). */
|
|
233
269
|
export interface LlmExportInsight extends StepMetrics {
|
|
234
270
|
agentKind: string
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.1",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|