@cat-factory/app 0.30.6 → 0.32.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 (32) hide show
  1. package/app/components/brainstorm/BrainstormWindow.vue +617 -0
  2. package/app/components/followUp/FollowUpWindow.vue +257 -0
  3. package/app/components/kaizen/KaizenPanel.vue +208 -0
  4. package/app/components/kaizen/KaizenStepStatus.vue +94 -0
  5. package/app/components/layout/NotificationsInbox.vue +13 -0
  6. package/app/components/layout/SideBar.vue +12 -0
  7. package/app/components/panels/AgentStepDetail.vue +6 -0
  8. package/app/components/panels/StepResultViewHost.vue +7 -0
  9. package/app/components/pipeline/PipelineBuilder.vue +21 -0
  10. package/app/components/pipeline/PipelineProgress.vue +59 -1
  11. package/app/components/settings/WorkspaceSettingsPanel.vue +20 -0
  12. package/app/components/slack/SlackPanel.vue +1 -0
  13. package/app/composables/api/followUps.ts +52 -0
  14. package/app/composables/api/kaizen.ts +16 -0
  15. package/app/composables/api/reviews.ts +68 -0
  16. package/app/composables/useApi.ts +4 -0
  17. package/app/composables/useResultView.ts +3 -1
  18. package/app/composables/useWorkspaceStream.ts +11 -0
  19. package/app/pages/index.vue +2 -0
  20. package/app/stores/brainstorm.ts +210 -0
  21. package/app/stores/followUps.ts +73 -0
  22. package/app/stores/kaizen.ts +101 -0
  23. package/app/stores/pipelines.ts +25 -0
  24. package/app/stores/ui.ts +64 -1
  25. package/app/stores/workspaceSettings.ts +1 -0
  26. package/app/types/brainstorm.ts +55 -0
  27. package/app/types/domain.ts +64 -0
  28. package/app/types/execution.ts +41 -0
  29. package/app/types/notifications.ts +1 -0
  30. package/app/utils/catalog.spec.ts +2 -0
  31. package/app/utils/catalog.ts +68 -3
  32. package/package.json +1 -1
@@ -0,0 +1,257 @@
1
+ <script setup lang="ts">
2
+ // Follow-up companion window — the dedicated surface for the future-looking Coder's
3
+ // surfaced items, opened via the universal result-view host (`ui.openFollowUps`). It reads
4
+ // the live items straight off the run's Coder step (`step.followUps`, kept fresh by the
5
+ // execution stream — a synchronous window, no `onOpen` loader) and lets a human decide each:
6
+ // file a follow-up as a tracker issue, send it back to the Coder, answer a question, or
7
+ // dismiss it. The pipeline's following steps stay blocked until every item is decided.
8
+ import { computed, reactive } from 'vue'
9
+ import { useResultView } from '~/composables/useResultView'
10
+ import { useExecutionStore } from '~/stores/execution'
11
+ import { useBoardStore } from '~/stores/board'
12
+ import { useFollowUpsStore } from '~/stores/followUps'
13
+ import type { FollowUpItem } from '~/types/execution'
14
+ import { FOLLOW_UP_COMPANION_META } from '~/utils/catalog'
15
+
16
+ const execution = useExecutionStore()
17
+ const board = useBoardStore()
18
+ const followUps = useFollowUpsStore()
19
+
20
+ const { open, blockId, instanceId, stepIndex, close } = useResultView('follow-ups')
21
+
22
+ const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
23
+ const instance = computed(() =>
24
+ instanceId.value === null ? null : (execution.getInstance(instanceId.value) ?? null),
25
+ )
26
+ const step = computed(() => {
27
+ if (instance.value === null || stepIndex.value === null) return null
28
+ return instance.value.steps[stepIndex.value] ?? null
29
+ })
30
+ const items = computed<FollowUpItem[]>(() => step.value?.followUps?.items ?? [])
31
+ const pendingCount = computed(() => items.value.filter((i) => i.status === 'pending').length)
32
+ const loops = computed(() => step.value?.followUps?.loops ?? 0)
33
+ const maxLoops = computed(() => step.value?.followUps?.maxLoops ?? 0)
34
+
35
+ // Draft answers per question item (keyed by item id), so typing doesn't clobber on re-render.
36
+ const drafts = reactive<Record<string, string>>({})
37
+
38
+ function execId(): string | null {
39
+ return instanceId.value
40
+ }
41
+
42
+ async function onFile(item: FollowUpItem) {
43
+ const id = execId()
44
+ if (id) await followUps.fileItem(id, item.id).catch(() => {})
45
+ }
46
+ async function onQueue(item: FollowUpItem) {
47
+ const id = execId()
48
+ if (id) await followUps.queueItem(id, item.id).catch(() => {})
49
+ }
50
+ async function onAnswer(item: FollowUpItem) {
51
+ const id = execId()
52
+ const answer = (drafts[item.id] ?? '').trim()
53
+ if (id && answer) await followUps.answerItem(id, item.id, answer).catch(() => {})
54
+ }
55
+ async function onDismiss(item: FollowUpItem) {
56
+ const id = execId()
57
+ if (id) await followUps.dismissItem(id, item.id).catch(() => {})
58
+ }
59
+
60
+ const STATUS_META: Record<
61
+ FollowUpItem['status'],
62
+ { label: string; badge: 'neutral' | 'info' | 'success' | 'warning'; text: string }
63
+ > = {
64
+ pending: { label: 'Needs a decision', badge: 'warning', text: 'text-amber-300' },
65
+ filed: { label: 'Filed as issue', badge: 'success', text: 'text-emerald-300' },
66
+ queued: { label: 'Sent to Coder', badge: 'info', text: 'text-sky-300' },
67
+ answered: { label: 'Answered', badge: 'info', text: 'text-sky-300' },
68
+ dismissed: { label: 'Dismissed', badge: 'neutral', text: 'text-slate-400' },
69
+ }
70
+ </script>
71
+
72
+ <template>
73
+ <Teleport to="body">
74
+ <div
75
+ v-if="open"
76
+ class="fixed inset-0 z-50 flex items-stretch justify-center bg-slate-950/70 backdrop-blur-sm"
77
+ @click.self="close"
78
+ >
79
+ <div
80
+ class="m-4 flex w-full max-w-3xl flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl"
81
+ >
82
+ <!-- Header -->
83
+ <header class="flex items-center gap-3 border-b border-slate-800 px-5 py-3">
84
+ <span
85
+ class="flex h-8 w-8 items-center justify-center rounded-lg bg-pink-500/15 text-pink-300"
86
+ >
87
+ <UIcon :name="FOLLOW_UP_COMPANION_META.icon" class="h-4 w-4" />
88
+ </span>
89
+ <div class="min-w-0 flex-1">
90
+ <h2 class="truncate text-sm font-semibold text-slate-100">
91
+ {{ FOLLOW_UP_COMPANION_META.label }}{{ block ? ` — ${block.title}` : '' }}
92
+ </h2>
93
+ <p class="truncate text-[11px] text-slate-400">
94
+ Forward-looking follow-ups & questions the Coder surfaced. The pipeline continues once
95
+ every item is decided.
96
+ </p>
97
+ </div>
98
+ <UBadge :color="pendingCount > 0 ? 'warning' : 'success'" variant="subtle" size="sm">
99
+ {{ pendingCount > 0 ? `${pendingCount} to decide` : 'All decided' }}
100
+ </UBadge>
101
+ <button
102
+ class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
103
+ @click="close"
104
+ >
105
+ <UIcon name="i-lucide-x" class="h-4 w-4" />
106
+ </button>
107
+ </header>
108
+
109
+ <div class="min-h-0 flex-1 overflow-y-auto px-5 py-4">
110
+ <!-- Empty -->
111
+ <div
112
+ v-if="items.length === 0"
113
+ class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
114
+ >
115
+ <UIcon :name="FOLLOW_UP_COMPANION_META.icon" class="h-8 w-8 opacity-40" />
116
+ <p class="text-sm">No follow-ups yet.</p>
117
+ <p class="max-w-sm text-[11px] text-slate-500">
118
+ As the Coder works it streams loose ends, side-tasks and questions here. They appear
119
+ live — you can act on them before the Coder even finishes.
120
+ </p>
121
+ </div>
122
+
123
+ <div v-else class="space-y-3">
124
+ <p
125
+ v-if="followUps.error"
126
+ class="rounded-md bg-rose-500/10 px-3 py-2 text-[12px] text-rose-300"
127
+ >
128
+ {{ followUps.error }}
129
+ </p>
130
+
131
+ <article
132
+ v-for="item in items"
133
+ :key="item.id"
134
+ class="rounded-xl border border-slate-800 bg-slate-900/60 px-4 py-3"
135
+ :class="item.status === 'pending' ? 'border-amber-500/40' : ''"
136
+ >
137
+ <div class="flex items-start gap-2">
138
+ <UIcon
139
+ :name="item.kind === 'question' ? 'i-lucide-circle-help' : 'i-lucide-compass'"
140
+ class="mt-0.5 h-4 w-4 shrink-0"
141
+ :class="item.kind === 'question' ? 'text-sky-300' : 'text-pink-300'"
142
+ />
143
+ <div class="min-w-0 flex-1">
144
+ <div class="flex items-center gap-2">
145
+ <h3 class="min-w-0 flex-1 truncate text-[13px] font-medium text-slate-100">
146
+ {{ item.title }}
147
+ </h3>
148
+ <UBadge :color="STATUS_META[item.status].badge" variant="subtle" size="sm">
149
+ {{ STATUS_META[item.status].label }}
150
+ </UBadge>
151
+ </div>
152
+ <p v-if="item.detail" class="mt-1 whitespace-pre-wrap text-[12px] text-slate-300">
153
+ {{ item.detail }}
154
+ </p>
155
+ <p v-if="item.suggestedAction" class="mt-1 text-[11px] text-slate-400">
156
+ <span class="text-slate-500">Suggested:</span> {{ item.suggestedAction }}
157
+ </p>
158
+ <p v-if="item.status === 'filed' && item.ticketUrl" class="mt-1 text-[11px]">
159
+ <a
160
+ :href="item.ticketUrl"
161
+ target="_blank"
162
+ rel="noopener"
163
+ class="text-emerald-300 hover:underline"
164
+ >
165
+ {{ item.ticketExternalId ?? 'View issue' }}
166
+ </a>
167
+ </p>
168
+ <p
169
+ v-if="item.status === 'answered' && item.answer"
170
+ class="mt-1 text-[11px] text-slate-300"
171
+ >
172
+ <span class="text-slate-500">Your answer:</span> {{ item.answer }}
173
+ </p>
174
+
175
+ <!-- Actions (only while the item is still undecided) -->
176
+ <div v-if="item.status === 'pending'" class="mt-2.5">
177
+ <!-- A question: answer it -->
178
+ <div v-if="item.kind === 'question'" class="space-y-2">
179
+ <textarea
180
+ v-model="drafts[item.id]"
181
+ rows="2"
182
+ placeholder="Answer this question — it's folded into the Coder's next pass…"
183
+ class="w-full resize-y rounded-md border border-slate-700 bg-slate-950/60 px-2.5 py-1.5 text-[12px] text-slate-100 placeholder:text-slate-600 focus:border-sky-500 focus:outline-none"
184
+ />
185
+ <div class="flex items-center gap-2">
186
+ <UButton
187
+ size="xs"
188
+ color="primary"
189
+ :loading="followUps.isActing(item.id)"
190
+ :disabled="!(drafts[item.id] ?? '').trim()"
191
+ @click="onAnswer(item)"
192
+ >
193
+ Answer & send back
194
+ </UButton>
195
+ <UButton
196
+ size="xs"
197
+ color="neutral"
198
+ variant="ghost"
199
+ :loading="followUps.isActing(item.id)"
200
+ @click="onDismiss(item)"
201
+ >
202
+ Dismiss
203
+ </UButton>
204
+ </div>
205
+ </div>
206
+
207
+ <!-- A follow-up: file / send back / dismiss -->
208
+ <div v-else class="flex flex-wrap items-center gap-2">
209
+ <UButton
210
+ size="xs"
211
+ color="primary"
212
+ icon="i-lucide-ticket"
213
+ :loading="followUps.isActing(item.id)"
214
+ @click="onFile(item)"
215
+ >
216
+ File as issue
217
+ </UButton>
218
+ <UButton
219
+ size="xs"
220
+ color="info"
221
+ variant="soft"
222
+ icon="i-lucide-corner-up-left"
223
+ :loading="followUps.isActing(item.id)"
224
+ @click="onQueue(item)"
225
+ >
226
+ Send to Coder
227
+ </UButton>
228
+ <UButton
229
+ size="xs"
230
+ color="neutral"
231
+ variant="ghost"
232
+ :loading="followUps.isActing(item.id)"
233
+ @click="onDismiss(item)"
234
+ >
235
+ Dismiss
236
+ </UButton>
237
+ </div>
238
+ </div>
239
+ </div>
240
+ </div>
241
+ </article>
242
+ </div>
243
+ </div>
244
+
245
+ <footer
246
+ class="flex items-center justify-between border-t border-slate-800 px-5 py-2.5 text-[11px] text-slate-400"
247
+ >
248
+ <span>
249
+ {{ items.length }} item{{ items.length === 1 ? '' : 's' }} ·
250
+ {{ pendingCount }} undecided
251
+ </span>
252
+ <span v-if="maxLoops > 0">Coder loops used: {{ loops }} / {{ maxLoops }}</span>
253
+ </footer>
254
+ </div>
255
+ </div>
256
+ </Teleport>
257
+ </template>
@@ -0,0 +1,208 @@
1
+ <script setup lang="ts">
2
+ import { computed, watch } from 'vue'
3
+ import { onKeyStroke } from '@vueuse/core'
4
+ import type { KaizenGrading } from '~/types/domain'
5
+ import { agentKindMeta } from '~/utils/catalog'
6
+
7
+ // The Kaizen screen: a full-panel overlay listing the workspace's grading history and
8
+ // its verified-combo library. Opened via `ui.openKaizen()` from the sidebar. Read-only —
9
+ // grading is scheduled by the engine and run by the background sweep, never from here.
10
+ const ui = useUiStore()
11
+ const kaizen = useKaizenStore()
12
+
13
+ const open = computed(() => ui.kaizenScreenOpen)
14
+
15
+ watch(open, (isOpen) => {
16
+ if (isOpen) void kaizen.loadOverview()
17
+ })
18
+
19
+ function close() {
20
+ ui.closeKaizen()
21
+ }
22
+ onKeyStroke('Escape', () => {
23
+ if (open.value) close()
24
+ })
25
+
26
+ function meta(kind: string) {
27
+ return agentKindMeta(kind)
28
+ }
29
+ function when(ms: number): string {
30
+ return new Date(ms).toLocaleString()
31
+ }
32
+ function gradeTone(g: KaizenGrading): string {
33
+ if (g.status === 'failed') return 'text-slate-500'
34
+ if (g.grade == null) return 'text-slate-400'
35
+ if (g.grade >= 5) return 'text-emerald-400'
36
+ if (g.grade >= 4) return 'text-lime-400'
37
+ if (g.grade === 3) return 'text-amber-400'
38
+ return 'text-rose-400'
39
+ }
40
+ function statusLabel(g: KaizenGrading): string {
41
+ if (g.status === 'scheduled') return 'Scheduled'
42
+ if (g.status === 'running') return 'Grading…'
43
+ if (g.status === 'failed') return 'Failed'
44
+ return g.grade != null ? `${g.grade}/5` : 'Graded'
45
+ }
46
+ </script>
47
+
48
+ <template>
49
+ <Teleport to="body">
50
+ <Transition name="kz-fade">
51
+ <div
52
+ v-if="open"
53
+ class="fixed inset-0 z-[60] flex flex-col bg-slate-950/96 backdrop-blur-sm"
54
+ role="dialog"
55
+ aria-modal="true"
56
+ >
57
+ <header class="flex items-center gap-3 border-b border-slate-800 px-6 py-4">
58
+ <div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-teal-500/15">
59
+ <UIcon name="i-lucide-sparkles" class="h-5 w-5 text-teal-400" />
60
+ </div>
61
+ <div class="min-w-0">
62
+ <h1 class="truncate text-base font-semibold text-white">Kaizen</h1>
63
+ <p class="truncate text-xs text-slate-500">
64
+ Continuous-improvement grading of agent runs
65
+ </p>
66
+ </div>
67
+ <div class="ml-auto flex items-center gap-2">
68
+ <UButton
69
+ icon="i-lucide-refresh-cw"
70
+ size="xs"
71
+ color="neutral"
72
+ variant="ghost"
73
+ :loading="kaizen.loadingOverview"
74
+ @click="kaizen.loadOverview()"
75
+ >
76
+ Refresh
77
+ </UButton>
78
+ <UButton icon="i-lucide-x" size="xs" color="neutral" variant="ghost" @click="close">
79
+ Close
80
+ </UButton>
81
+ </div>
82
+ </header>
83
+
84
+ <div
85
+ v-if="kaizen.available === false"
86
+ class="flex flex-1 items-center justify-center text-sm text-slate-500"
87
+ >
88
+ Kaizen is not configured on this deployment.
89
+ </div>
90
+
91
+ <div v-else class="grid flex-1 grid-cols-1 gap-6 overflow-auto p-6 lg:grid-cols-3">
92
+ <!-- Verified combos -->
93
+ <section class="lg:col-span-1">
94
+ <h2 class="mb-2 flex items-center gap-2 text-sm font-semibold text-slate-200">
95
+ <UIcon name="i-lucide-badge-check" class="h-4 w-4 text-emerald-400" />
96
+ Verified combos
97
+ <span class="text-xs font-normal text-slate-500">({{ kaizen.verifiedCount }})</span>
98
+ </h2>
99
+ <p class="mb-3 text-[11px] text-slate-500">
100
+ A prompt + agent + model combination that graded 4 or 5 with no recommendations five
101
+ times in a row. These are no longer graded.
102
+ </p>
103
+ <ul class="space-y-2">
104
+ <li
105
+ v-for="c in kaizen.verified"
106
+ :key="c.comboKey"
107
+ class="rounded-lg border border-slate-800 bg-slate-900/40 p-2.5"
108
+ >
109
+ <div class="flex items-center gap-2">
110
+ <UIcon
111
+ :name="meta(c.agentKind).icon"
112
+ class="h-3.5 w-3.5 shrink-0"
113
+ :style="{ color: meta(c.agentKind).color }"
114
+ />
115
+ <span class="text-xs font-medium text-slate-200">{{
116
+ meta(c.agentKind).label
117
+ }}</span>
118
+ <UIcon
119
+ v-if="c.verified"
120
+ name="i-lucide-badge-check"
121
+ class="ml-auto h-3.5 w-3.5 text-emerald-400"
122
+ />
123
+ <span v-else class="ml-auto text-[11px] text-slate-500">
124
+ {{ c.consecutiveHighGrades }}/5
125
+ </span>
126
+ </div>
127
+ <div class="mt-1 truncate text-[11px] text-slate-500" :title="c.model">
128
+ {{ c.model }} · prompt v{{ c.promptVersion }}
129
+ </div>
130
+ </li>
131
+ <li v-if="kaizen.verified.length === 0" class="text-xs text-slate-600">
132
+ No combos yet.
133
+ </li>
134
+ </ul>
135
+ </section>
136
+
137
+ <!-- Grading history -->
138
+ <section class="lg:col-span-2">
139
+ <h2 class="mb-2 flex items-center gap-2 text-sm font-semibold text-slate-200">
140
+ <UIcon name="i-lucide-history" class="h-4 w-4 text-teal-400" />
141
+ Grading history
142
+ </h2>
143
+ <div class="overflow-hidden rounded-lg border border-slate-800">
144
+ <table class="w-full text-left text-xs">
145
+ <thead class="bg-slate-900/60 text-[11px] uppercase tracking-wide text-slate-500">
146
+ <tr>
147
+ <th class="px-3 py-2 font-medium">When</th>
148
+ <th class="px-3 py-2 font-medium">Agent</th>
149
+ <th class="px-3 py-2 font-medium">Model</th>
150
+ <th class="px-3 py-2 font-medium">Grade</th>
151
+ <th class="px-3 py-2 font-medium">Recommendations</th>
152
+ </tr>
153
+ </thead>
154
+ <tbody class="divide-y divide-slate-800/70">
155
+ <tr v-for="g in kaizen.history" :key="g.id" class="align-top">
156
+ <td class="whitespace-nowrap px-3 py-2 text-slate-500">
157
+ {{ when(g.createdAt) }}
158
+ </td>
159
+ <td class="px-3 py-2">
160
+ <span class="flex items-center gap-1.5">
161
+ <UIcon
162
+ :name="meta(g.agentKind).icon"
163
+ class="h-3.5 w-3.5"
164
+ :style="{ color: meta(g.agentKind).color }"
165
+ />
166
+ <span class="text-slate-200">{{ meta(g.agentKind).label }}</span>
167
+ <span class="text-slate-600">v{{ g.promptVersion }}</span>
168
+ </span>
169
+ </td>
170
+ <td class="max-w-[12rem] truncate px-3 py-2 text-slate-400" :title="g.model">
171
+ {{ g.model }}
172
+ </td>
173
+ <td class="whitespace-nowrap px-3 py-2 font-semibold" :class="gradeTone(g)">
174
+ {{ statusLabel(g) }}
175
+ </td>
176
+ <td class="px-3 py-2 text-slate-400">
177
+ <ul v-if="g.recommendations.length" class="list-disc space-y-0.5 pl-4">
178
+ <li v-for="(r, i) in g.recommendations" :key="i">{{ r }}</li>
179
+ </ul>
180
+ <span v-else-if="g.status === 'complete'" class="text-slate-600">—</span>
181
+ <span v-else-if="g.error" class="text-rose-400/80">{{ g.error }}</span>
182
+ </td>
183
+ </tr>
184
+ <tr v-if="kaizen.history.length === 0">
185
+ <td colspan="5" class="px-3 py-6 text-center text-slate-600">
186
+ No gradings yet.
187
+ </td>
188
+ </tr>
189
+ </tbody>
190
+ </table>
191
+ </div>
192
+ </section>
193
+ </div>
194
+ </div>
195
+ </Transition>
196
+ </Teleport>
197
+ </template>
198
+
199
+ <style scoped>
200
+ .kz-fade-enter-active,
201
+ .kz-fade-leave-active {
202
+ transition: opacity 0.15s ease;
203
+ }
204
+ .kz-fade-enter-from,
205
+ .kz-fade-leave-to {
206
+ opacity: 0;
207
+ }
208
+ </style>
@@ -0,0 +1,94 @@
1
+ <script setup lang="ts">
2
+ import { computed, watch } from 'vue'
3
+
4
+ // Per-step Kaizen grading status, shown inside the run window (NOT on the board). Reads
5
+ // the grading for this run's step from the kaizen store, lazily loading the run's
6
+ // gradings on first mount, and renders the scheduled→running→complete status plus the
7
+ // grade, summary and recommendations once available.
8
+ const props = defineProps<{
9
+ /** The run (execution) id. */
10
+ instanceId: string | null | undefined
11
+ /** The step's index within the run. */
12
+ stepIndex: number | null | undefined
13
+ }>()
14
+
15
+ const kaizen = useKaizenStore()
16
+
17
+ const grading = computed(() => {
18
+ if (!props.instanceId || props.stepIndex == null) return null
19
+ return kaizen.gradingForStep(props.instanceId, props.stepIndex)
20
+ })
21
+
22
+ // Load the run's gradings once when we have an id and nothing cached yet. The stream
23
+ // keeps them live afterwards.
24
+ watch(
25
+ () => props.instanceId,
26
+ (id) => {
27
+ if (id && kaizen.gradingsFor(id).length === 0 && kaizen.available !== false) {
28
+ void kaizen.loadForExecution(id)
29
+ }
30
+ },
31
+ { immediate: true },
32
+ )
33
+
34
+ const tone = computed(() => {
35
+ const g = grading.value
36
+ if (!g || g.grade == null) return 'text-slate-400'
37
+ if (g.grade >= 5) return 'text-emerald-400'
38
+ if (g.grade >= 4) return 'text-lime-400'
39
+ if (g.grade === 3) return 'text-amber-400'
40
+ return 'text-rose-400'
41
+ })
42
+ </script>
43
+
44
+ <template>
45
+ <section v-if="grading" class="rounded-xl border border-slate-800 bg-slate-900/50 p-4">
46
+ <div class="flex items-center gap-2">
47
+ <UIcon name="i-lucide-sparkles" class="h-4 w-4 text-teal-400" />
48
+ <h3 class="text-sm font-semibold text-slate-200">Kaizen grading</h3>
49
+ <span class="ml-auto flex items-center gap-1.5 text-xs">
50
+ <template v-if="grading.status === 'scheduled'">
51
+ <UIcon name="i-lucide-clock" class="h-3.5 w-3.5 text-slate-500" />
52
+ <span class="text-slate-400">Scheduled</span>
53
+ </template>
54
+ <template v-else-if="grading.status === 'running'">
55
+ <UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 animate-spin text-teal-400" />
56
+ <span class="text-teal-300">Grading…</span>
57
+ </template>
58
+ <template v-else-if="grading.status === 'failed'">
59
+ <UIcon name="i-lucide-circle-alert" class="h-3.5 w-3.5 text-rose-400" />
60
+ <span class="text-rose-400">Failed</span>
61
+ </template>
62
+ <template v-else>
63
+ <span class="font-semibold" :class="tone">{{ grading.grade }}/5</span>
64
+ </template>
65
+ </span>
66
+ </div>
67
+
68
+ <p v-if="grading.status === 'scheduled'" class="mt-2 text-[11px] text-slate-500">
69
+ A Kaizen grading is queued for this step. It runs in the background after the run.
70
+ </p>
71
+
72
+ <template v-else-if="grading.status === 'complete'">
73
+ <p v-if="grading.summary" class="mt-2 text-xs text-slate-300">{{ grading.summary }}</p>
74
+ <div v-if="grading.recommendations.length" class="mt-2">
75
+ <p class="text-[11px] font-medium uppercase tracking-wide text-slate-500">
76
+ Recommendations
77
+ </p>
78
+ <ul class="mt-1 list-disc space-y-0.5 pl-4 text-xs text-slate-300">
79
+ <li v-for="(r, i) in grading.recommendations" :key="i">{{ r }}</li>
80
+ </ul>
81
+ </div>
82
+ <p v-else class="mt-2 text-[11px] text-emerald-400/80">
83
+ Smooth interaction — nothing to improve.
84
+ </p>
85
+ <p v-if="grading.graderModel" class="mt-2 text-[10px] text-slate-600">
86
+ Graded by {{ grading.graderModel }}
87
+ </p>
88
+ </template>
89
+
90
+ <p v-else-if="grading.status === 'failed'" class="mt-2 text-[11px] text-rose-400/80">
91
+ {{ grading.error ?? 'The grading could not be completed.' }}
92
+ </p>
93
+ </section>
94
+ </template>
@@ -36,6 +36,9 @@ const META: Record<Notification['type'], { icon: string; color: Accent; action:
36
36
  // Clicking the title opens the human-testing window for the task (see `reveal`); "act" just
37
37
  // marks it read (the gate is resolved in that window — confirm / request a fix — not here).
38
38
  human_test_ready: { icon: 'i-lucide-user-check', color: 'primary', action: 'Mark read' },
39
+ // Clicking the title opens the Follow-up companion window for the run (see `reveal`); "act"
40
+ // just marks it read (items are decided in that window — file / send back / answer — not here).
41
+ followup_pending: { icon: 'i-lucide-compass', color: 'warning', action: 'Mark read' },
39
42
  }
40
43
 
41
44
  /** A notification the escalation sweep has flagged as overdue (waited past the threshold). */
@@ -81,9 +84,19 @@ function reveal(n: Notification) {
81
84
  else if (n.type === 'clarity_review') ui.openClarityReview(n.blockId)
82
85
  else if (n.type === 'decision_required') revealDecision(n)
83
86
  else if (n.type === 'human_test_ready') revealHumanTest(n)
87
+ else if (n.type === 'followup_pending') revealFollowUps(n)
84
88
  else ui.select(n.blockId)
85
89
  }
86
90
 
91
+ /**
92
+ * Open the Follow-up companion window for a run whose Coder parked on undecided items.
93
+ * Falls back to focusing the block when the run isn't loaded.
94
+ */
95
+ function revealFollowUps(n: Notification) {
96
+ if (n.executionId && execution.getInstance(n.executionId)) ui.openFollowUps(n.executionId)
97
+ else if (n.blockId) ui.select(n.blockId)
98
+ }
99
+
87
100
  /**
88
101
  * Open the human-testing window for a parked `human-test` gate: find the run's parked
89
102
  * human-test step and open it through the universal step dispatch (its archetype declares
@@ -138,6 +138,18 @@ watch(
138
138
  >
139
139
  Sandbox
140
140
  </UButton>
141
+ <!-- The Kaizen screen: grading history + verified prompt/agent/model combos. -->
142
+ <UButton
143
+ block
144
+ color="primary"
145
+ variant="soft"
146
+ size="sm"
147
+ icon="i-lucide-sparkles"
148
+ class="justify-start"
149
+ @click="ui.openKaizen()"
150
+ >
151
+ Kaizen
152
+ </UButton>
141
153
  </div>
142
154
  </section>
143
155
 
@@ -313,6 +313,12 @@ async function copyOutput() {
313
313
  />
314
314
  </section>
315
315
 
316
+ <!-- post-run Kaizen grading status + results for this step (run-details only) -->
317
+ <KaizenStepStatus
318
+ :instance-id="ctx?.instanceId ?? null"
319
+ :step-index="ctx?.stepIndex ?? null"
320
+ />
321
+
316
322
  <!-- companion rework budget spent: the shared iteration-cap decision
317
323
  (one more round / proceed with the current output / stop & reset) -->
318
324
  <IterationCapPrompt
@@ -13,18 +13,22 @@
13
13
  import { computed, type Component } from 'vue'
14
14
  import RequirementsReviewWindow from '~/components/requirements/RequirementsReviewWindow.vue'
15
15
  import ClarityReviewWindow from '~/components/clarity/ClarityReviewWindow.vue'
16
+ import BrainstormWindow from '~/components/brainstorm/BrainstormWindow.vue'
16
17
  import TestReportWindow from '~/components/testing/TestReportWindow.vue'
17
18
  import HumanTestWindow from '~/components/humanTest/HumanTestWindow.vue'
18
19
  import GateResultView from '~/components/gates/GateResultView.vue'
19
20
  import ConsensusSessionWindow from '~/components/consensus/ConsensusSessionWindow.vue'
20
21
  import GenericStructuredResultView from '~/components/panels/GenericStructuredResultView.vue'
21
22
  import ServiceSpecWindow from '~/components/spec/ServiceSpecWindow.vue'
23
+ import FollowUpWindow from '~/components/followUp/FollowUpWindow.vue'
22
24
 
23
25
  const ui = useUiStore()
24
26
 
25
27
  const STEP_RESULT_VIEWS: Record<string, Component> = {
26
28
  'requirements-review': RequirementsReviewWindow,
27
29
  'clarity-review': ClarityReviewWindow,
30
+ // Shared by both brainstorm stages (requirements + architecture); the window reads the stage.
31
+ brainstorm: BrainstormWindow,
28
32
  tester: TestReportWindow,
29
33
  // The human-testing gate: env URL + confirm / request-fix / pull-main / recreate / destroy.
30
34
  'human-test': HumanTestWindow,
@@ -38,6 +42,9 @@ const STEP_RESULT_VIEWS: Record<string, Component> = {
38
42
  // The service's prescriptive spec tree (+ Gherkin), opened from the inspector's "View
39
43
  // Requirements" button. Not a pipeline-step view — opened directly via `ui.openServiceSpec`.
40
44
  'service-spec': ServiceSpecWindow,
45
+ // The future-looking Follow-up companion: the Coder's surfaced loose ends / questions.
46
+ // Opened directly via `ui.openFollowUps` (the blinking chip + the `followup_pending` card).
47
+ 'follow-ups': FollowUpWindow,
41
48
  }
42
49
 
43
50
  const active = computed<Component | null>(() => {