@cat-factory/app 0.31.0 → 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.
- package/app/components/brainstorm/BrainstormWindow.vue +617 -0
- package/app/components/followUp/FollowUpWindow.vue +257 -0
- package/app/components/kaizen/KaizenPanel.vue +2 -2
- package/app/components/layout/NotificationsInbox.vue +13 -0
- package/app/components/panels/StepResultViewHost.vue +7 -0
- package/app/components/pipeline/PipelineBuilder.vue +21 -0
- package/app/components/pipeline/PipelineProgress.vue +59 -1
- package/app/components/slack/SlackPanel.vue +1 -0
- package/app/composables/api/followUps.ts +52 -0
- package/app/composables/api/reviews.ts +68 -0
- package/app/composables/useApi.ts +2 -0
- package/app/composables/useResultView.ts +3 -1
- package/app/composables/useWorkspaceStream.ts +5 -0
- package/app/stores/brainstorm.ts +210 -0
- package/app/stores/followUps.ts +73 -0
- package/app/stores/pipelines.ts +25 -0
- package/app/stores/ui.ts +51 -1
- package/app/types/brainstorm.ts +55 -0
- package/app/types/domain.ts +13 -0
- package/app/types/execution.ts +41 -0
- package/app/types/notifications.ts +1 -0
- package/app/utils/catalog.spec.ts +2 -0
- package/app/utils/catalog.ts +48 -0
- package/package.json +1 -1
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Brainstorm window — the dedicated surface for the `requirements-brainstorm` /
|
|
3
|
+
// `architecture-brainstorm` gate steps (opened via the universal result-view host; the two
|
|
4
|
+
// stages share this one window). The agent PROPOSES options with explicit trade-offs; the
|
|
5
|
+
// human picks the relevant ones, steers them, and dismisses the rest, then asks to incorporate.
|
|
6
|
+
// Incorporation + the re-run happen ASYNCHRONOUSLY in the durable driver: the window closes and
|
|
7
|
+
// the user returns to the board, summoned back (a notification) only if the re-run raises new
|
|
8
|
+
// options or hits the iteration cap. The converged direction — not the original description — is
|
|
9
|
+
// what the downstream stage (the requirements review / the architect) consumes.
|
|
10
|
+
import { parseOutputOutline } from '~/utils/agentOutput'
|
|
11
|
+
import type {
|
|
12
|
+
BrainstormItem,
|
|
13
|
+
BrainstormSession,
|
|
14
|
+
BrainstormStage,
|
|
15
|
+
ReviewItemCategory,
|
|
16
|
+
ReviewItemSeverity,
|
|
17
|
+
ReviewItemStatus,
|
|
18
|
+
} from '~/types/brainstorm'
|
|
19
|
+
|
|
20
|
+
const board = useBoardStore()
|
|
21
|
+
const brainstorm = useBrainstormStore()
|
|
22
|
+
const ui = useUiStore()
|
|
23
|
+
const toast = useToast()
|
|
24
|
+
|
|
25
|
+
const drafts = ref<Record<string, string>>({})
|
|
26
|
+
const redoComment = ref('')
|
|
27
|
+
const showRedo = ref(false)
|
|
28
|
+
|
|
29
|
+
const { open, blockId, stage, close } = useResultView('brainstorm', {
|
|
30
|
+
// `onOpen` fires synchronously from `useResultView`'s immediate watch, BEFORE the `stage`
|
|
31
|
+
// const below is initialised — so read the stage straight off the store here (referencing
|
|
32
|
+
// `stage.value` would hit its temporal dead zone and throw on every open).
|
|
33
|
+
onOpen: (id) => {
|
|
34
|
+
drafts.value = {}
|
|
35
|
+
redoComment.value = ''
|
|
36
|
+
showRedo.value = false
|
|
37
|
+
const openStage = ui.resultView?.stage
|
|
38
|
+
if (openStage) void brainstorm.load(id, openStage)
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
const activeStage = computed<BrainstormStage>(() => stage.value ?? 'requirements')
|
|
42
|
+
const isArchitecture = computed(() => activeStage.value === 'architecture')
|
|
43
|
+
const subjectNoun = computed(() => (isArchitecture.value ? 'approach' : 'requirements'))
|
|
44
|
+
const docNoun = computed(() =>
|
|
45
|
+
isArchitecture.value ? 'technical approach' : 'requirements direction',
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
49
|
+
const session = computed<BrainstormSession | null>(() =>
|
|
50
|
+
blockId.value ? brainstorm.sessionFor(blockId.value, activeStage.value) : null,
|
|
51
|
+
)
|
|
52
|
+
const busy = computed(() =>
|
|
53
|
+
blockId.value ? brainstorm.isRunning(blockId.value, activeStage.value) : false,
|
|
54
|
+
)
|
|
55
|
+
const loading = computed(() =>
|
|
56
|
+
blockId.value ? brainstorm.isLoading(blockId.value, activeStage.value) : false,
|
|
57
|
+
)
|
|
58
|
+
const reworking = computed(() =>
|
|
59
|
+
session.value ? brainstorm.isIncorporating(session.value.id) : false,
|
|
60
|
+
)
|
|
61
|
+
const acting = ref(false)
|
|
62
|
+
|
|
63
|
+
const SEVERITY_RANK: Record<ReviewItemSeverity, number> = { high: 0, medium: 1, low: 2 }
|
|
64
|
+
const sortedItems = computed<BrainstormItem[]>(() => {
|
|
65
|
+
if (!session.value) return []
|
|
66
|
+
return [...session.value.items].sort(
|
|
67
|
+
(a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity],
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const openCount = computed(() => (session.value ? brainstorm.openCount(session.value) : 0))
|
|
72
|
+
const answeredCount = computed(() => (session.value ? brainstorm.answeredCount(session.value) : 0))
|
|
73
|
+
const status = computed(() => session.value?.status ?? null)
|
|
74
|
+
const merged = computed(() => status.value === 'merged')
|
|
75
|
+
const exceeded = computed(() => status.value === 'exceeded')
|
|
76
|
+
const incorporated = computed(() => status.value === 'incorporated')
|
|
77
|
+
const incorporating = computed(() => status.value === 'incorporating')
|
|
78
|
+
const reReviewing = computed(() => status.value === 'reviewing')
|
|
79
|
+
const working = computed(() => incorporating.value || reReviewing.value)
|
|
80
|
+
const frozen = computed(() => incorporated.value || working.value)
|
|
81
|
+
const canIncorporate = computed(() => !!session.value && brainstorm.canIncorporate(session.value))
|
|
82
|
+
const canProceed = computed(() => !!session.value && brainstorm.canProceed(session.value))
|
|
83
|
+
const iteration = computed(() => session.value?.iteration ?? 1)
|
|
84
|
+
const maxIterations = computed(() => session.value?.maxIterations ?? 1)
|
|
85
|
+
|
|
86
|
+
// The converged direction rendered as collapsible markdown (same reader the prose review
|
|
87
|
+
// window uses), shown once the companion has produced one.
|
|
88
|
+
const outline = computed(() =>
|
|
89
|
+
session.value?.convergedDirection ? parseOutputOutline(session.value.convergedDirection) : null,
|
|
90
|
+
)
|
|
91
|
+
const collapsed = ref<Record<string, boolean>>({})
|
|
92
|
+
function toggle(id: string) {
|
|
93
|
+
collapsed.value = { ...collapsed.value, [id]: !collapsed.value[id] }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const SEVERITY_COLOR = {
|
|
97
|
+
high: 'error',
|
|
98
|
+
medium: 'warning',
|
|
99
|
+
low: 'neutral',
|
|
100
|
+
} as const satisfies Record<ReviewItemSeverity, string>
|
|
101
|
+
const CATEGORY_ICON: Record<ReviewItemCategory, string> = {
|
|
102
|
+
gap: 'i-lucide-puzzle',
|
|
103
|
+
clarification: 'i-lucide-help-circle',
|
|
104
|
+
assumption: 'i-lucide-lightbulb',
|
|
105
|
+
risk: 'i-lucide-shield-alert',
|
|
106
|
+
question: 'i-lucide-message-circle-question',
|
|
107
|
+
}
|
|
108
|
+
const STATUS_COLOR = {
|
|
109
|
+
open: 'warning',
|
|
110
|
+
answered: 'info',
|
|
111
|
+
resolved: 'success',
|
|
112
|
+
dismissed: 'neutral',
|
|
113
|
+
// Brainstorm doesn't request Requirement-Writer recommendations, but the item-status type
|
|
114
|
+
// is shared with the requirements review, so the map must be exhaustive.
|
|
115
|
+
recommend_requested: 'primary',
|
|
116
|
+
} as const satisfies Record<ReviewItemStatus, string>
|
|
117
|
+
|
|
118
|
+
function notifyError(title: string, e: unknown) {
|
|
119
|
+
toast.add({
|
|
120
|
+
title,
|
|
121
|
+
description: e instanceof Error ? e.message : String(e),
|
|
122
|
+
icon: 'i-lucide-triangle-alert',
|
|
123
|
+
color: 'error',
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function submitReply(item: BrainstormItem) {
|
|
128
|
+
if (!session.value) return
|
|
129
|
+
const text = (drafts.value[item.id] ?? '').trim()
|
|
130
|
+
if (!text) return
|
|
131
|
+
try {
|
|
132
|
+
await brainstorm.reply(session.value, item.id, text)
|
|
133
|
+
drafts.value = { ...drafts.value, [item.id]: '' }
|
|
134
|
+
} catch (e) {
|
|
135
|
+
notifyError('Could not save the choice', e)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function setStatus(item: BrainstormItem, itemStatus: ReviewItemStatus) {
|
|
140
|
+
if (!session.value) return
|
|
141
|
+
try {
|
|
142
|
+
await brainstorm.setItemStatus(session.value, item.id, itemStatus)
|
|
143
|
+
} catch (e) {
|
|
144
|
+
notifyError('Could not update the option', e)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function incorporate(feedback?: string) {
|
|
149
|
+
if (!session.value || !blockId.value) return
|
|
150
|
+
try {
|
|
151
|
+
await brainstorm.incorporate(session.value, feedback)
|
|
152
|
+
} catch (e) {
|
|
153
|
+
notifyError('Could not incorporate the choices', e)
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
redoComment.value = ''
|
|
157
|
+
showRedo.value = false
|
|
158
|
+
toast.add({
|
|
159
|
+
title: `Drafting the ${docNoun.value} in the background`,
|
|
160
|
+
description: "You're back on the board — we'll notify you only if more input is needed.",
|
|
161
|
+
icon: 'i-lucide-wand-sparkles',
|
|
162
|
+
})
|
|
163
|
+
close()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function reReview() {
|
|
167
|
+
if (!blockId.value) return
|
|
168
|
+
try {
|
|
169
|
+
const updated = await brainstorm.reReview(blockId.value, activeStage.value)
|
|
170
|
+
toast.add({
|
|
171
|
+
title:
|
|
172
|
+
updated.status === 'incorporated'
|
|
173
|
+
? 'Direction settled — continuing the pipeline'
|
|
174
|
+
: updated.status === 'exceeded'
|
|
175
|
+
? 'Iteration limit reached — choose how to proceed'
|
|
176
|
+
: `${brainstorm.openCount(updated)} new option(s) to react to`,
|
|
177
|
+
icon: 'i-lucide-sparkles',
|
|
178
|
+
})
|
|
179
|
+
} catch (e) {
|
|
180
|
+
notifyError('Could not re-run the brainstorm', e)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function proceed() {
|
|
185
|
+
if (!blockId.value) return
|
|
186
|
+
acting.value = true
|
|
187
|
+
try {
|
|
188
|
+
await brainstorm.proceed(blockId.value, activeStage.value)
|
|
189
|
+
toast.add({ title: 'Proceeding to the next phase', icon: 'i-lucide-arrow-right' })
|
|
190
|
+
} catch (e) {
|
|
191
|
+
notifyError('Could not proceed', e)
|
|
192
|
+
} finally {
|
|
193
|
+
acting.value = false
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset') {
|
|
198
|
+
if (!blockId.value) return
|
|
199
|
+
acting.value = true
|
|
200
|
+
try {
|
|
201
|
+
await brainstorm.resolveExceeded(blockId.value, activeStage.value, choice)
|
|
202
|
+
if (choice === 'stop-reset') {
|
|
203
|
+
toast.add({ title: 'Task reset — edit it and resubmit', icon: 'i-lucide-undo' })
|
|
204
|
+
close()
|
|
205
|
+
} else if (choice === 'proceed') {
|
|
206
|
+
toast.add({ title: 'Proceeding to the next phase', icon: 'i-lucide-arrow-right' })
|
|
207
|
+
} else {
|
|
208
|
+
toast.add({ title: 'One more brainstorm round granted', icon: 'i-lucide-rotate-cw' })
|
|
209
|
+
}
|
|
210
|
+
} catch (e) {
|
|
211
|
+
notifyError('Could not resolve the brainstorm', e)
|
|
212
|
+
} finally {
|
|
213
|
+
acting.value = false
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
</script>
|
|
217
|
+
|
|
218
|
+
<template>
|
|
219
|
+
<Teleport to="body">
|
|
220
|
+
<div
|
|
221
|
+
v-if="open"
|
|
222
|
+
class="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/70 p-4 backdrop-blur-sm"
|
|
223
|
+
@click.self="close"
|
|
224
|
+
>
|
|
225
|
+
<div
|
|
226
|
+
class="flex h-[90vh] w-full max-w-5xl flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl"
|
|
227
|
+
>
|
|
228
|
+
<!-- header -->
|
|
229
|
+
<header class="flex items-center gap-3 border-b border-slate-800 px-6 py-4">
|
|
230
|
+
<div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-amber-500/15">
|
|
231
|
+
<UIcon
|
|
232
|
+
:name="isArchitecture ? 'i-lucide-drafting-compass' : 'i-lucide-lightbulb'"
|
|
233
|
+
class="h-5 w-5 text-amber-300"
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
<div class="min-w-0">
|
|
237
|
+
<h1 class="truncate text-base font-semibold text-white">
|
|
238
|
+
{{ isArchitecture ? 'Architecture brainstorm' : 'Requirements brainstorm' }}
|
|
239
|
+
</h1>
|
|
240
|
+
<p v-if="block" class="truncate text-xs text-slate-500">{{ block.title }}</p>
|
|
241
|
+
</div>
|
|
242
|
+
<div class="ml-auto flex items-center gap-1.5">
|
|
243
|
+
<UBadge v-if="session" color="neutral" variant="subtle" size="sm">
|
|
244
|
+
Iteration {{ iteration }} / {{ maxIterations }}
|
|
245
|
+
</UBadge>
|
|
246
|
+
<UButton icon="i-lucide-x" color="neutral" variant="ghost" size="sm" @click="close" />
|
|
247
|
+
</div>
|
|
248
|
+
</header>
|
|
249
|
+
|
|
250
|
+
<div class="flex min-h-0 flex-1">
|
|
251
|
+
<!-- main column -->
|
|
252
|
+
<div class="min-w-0 flex-1 overflow-y-auto px-6 py-5">
|
|
253
|
+
<p class="mb-4 text-sm text-slate-400">
|
|
254
|
+
An AI partner proposed the {{ subjectNoun }} options below, each with its trade-offs.
|
|
255
|
+
<span class="text-slate-300">Choose</span> the ones you want (and steer them) and
|
|
256
|
+
<span class="text-slate-300">dismiss</span> the rest, then incorporate; it re-runs
|
|
257
|
+
until you converge on a {{ docNoun }}.
|
|
258
|
+
</p>
|
|
259
|
+
|
|
260
|
+
<!-- empty state -->
|
|
261
|
+
<div
|
|
262
|
+
v-if="!session && !busy && !loading"
|
|
263
|
+
class="rounded-lg border border-dashed border-slate-700 p-8 text-center text-sm text-slate-500"
|
|
264
|
+
>
|
|
265
|
+
No brainstorm yet. It runs automatically when this task's pipeline reaches the
|
|
266
|
+
brainstorm step.
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<!-- working state (initial fetch on open, or an agent pass running) -->
|
|
270
|
+
<div
|
|
271
|
+
v-else-if="(busy || loading) && !session"
|
|
272
|
+
class="flex items-center justify-center gap-2 p-8 text-sm text-slate-400"
|
|
273
|
+
>
|
|
274
|
+
<UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
|
|
275
|
+
{{ loading && !busy ? 'Loading the brainstorm…' : 'Generating options…' }}
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<template v-else-if="session">
|
|
279
|
+
<!-- converged -->
|
|
280
|
+
<div
|
|
281
|
+
v-if="incorporated"
|
|
282
|
+
class="mb-4 flex items-center gap-2 rounded-lg border border-emerald-900/60 bg-emerald-950/30 p-4 text-sm text-emerald-300"
|
|
283
|
+
>
|
|
284
|
+
<UIcon name="i-lucide-circle-check" class="h-5 w-5 shrink-0" />
|
|
285
|
+
The {{ docNoun }} is settled. The document below is what the next stage uses.
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<!-- iteration cap hit -->
|
|
289
|
+
<IterationCapPrompt
|
|
290
|
+
v-else-if="exceeded"
|
|
291
|
+
class="mb-4"
|
|
292
|
+
:heading="`Reached the ${maxIterations}-iteration limit with options still open.`"
|
|
293
|
+
detail="Do one more brainstorm round, proceed to the next phase with the last direction anyway, or stop and reset the task so you can edit it and resubmit."
|
|
294
|
+
:loading="acting"
|
|
295
|
+
@resolve="resolveExceeded"
|
|
296
|
+
/>
|
|
297
|
+
|
|
298
|
+
<!-- working: the async cycle is running in the driver -->
|
|
299
|
+
<div
|
|
300
|
+
v-else-if="working"
|
|
301
|
+
class="mb-4 flex items-center gap-2 rounded-lg border border-amber-900/60 bg-amber-950/30 p-4 text-sm text-amber-200"
|
|
302
|
+
>
|
|
303
|
+
<UIcon name="i-lucide-loader-circle" class="h-5 w-5 shrink-0 animate-spin" />
|
|
304
|
+
<span v-if="incorporating">
|
|
305
|
+
Folding your choices into a {{ docNoun }}… You can close this — we’ll notify you
|
|
306
|
+
only if more input is needed.
|
|
307
|
+
</span>
|
|
308
|
+
<span v-else>
|
|
309
|
+
Re-running the brainstorm on the updated direction… You can close this — we’ll
|
|
310
|
+
notify you only if more input is needed.
|
|
311
|
+
</span>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
<!-- options to react to -->
|
|
315
|
+
<div v-if="session.items.length" class="flex flex-col gap-3">
|
|
316
|
+
<div
|
|
317
|
+
v-for="item in sortedItems"
|
|
318
|
+
:key="item.id"
|
|
319
|
+
class="rounded-lg border border-slate-800 bg-slate-900/60 p-3"
|
|
320
|
+
:class="{ 'opacity-60': item.status === 'dismissed' }"
|
|
321
|
+
>
|
|
322
|
+
<div class="flex items-start gap-2">
|
|
323
|
+
<UIcon
|
|
324
|
+
:name="CATEGORY_ICON[item.category]"
|
|
325
|
+
class="mt-0.5 h-4 w-4 shrink-0 text-slate-400"
|
|
326
|
+
/>
|
|
327
|
+
<div class="min-w-0 flex-1">
|
|
328
|
+
<div class="flex flex-wrap items-center gap-1.5">
|
|
329
|
+
<span class="text-sm font-medium text-white">{{ item.title }}</span>
|
|
330
|
+
<UBadge size="xs" variant="subtle" :color="SEVERITY_COLOR[item.severity]">
|
|
331
|
+
{{ item.severity }}
|
|
332
|
+
</UBadge>
|
|
333
|
+
<UBadge size="xs" variant="outline" color="neutral">
|
|
334
|
+
{{ item.category }}
|
|
335
|
+
</UBadge>
|
|
336
|
+
<UBadge
|
|
337
|
+
size="xs"
|
|
338
|
+
variant="soft"
|
|
339
|
+
:color="STATUS_COLOR[item.status]"
|
|
340
|
+
class="ml-auto"
|
|
341
|
+
>
|
|
342
|
+
{{ item.status }}
|
|
343
|
+
</UBadge>
|
|
344
|
+
</div>
|
|
345
|
+
<p class="mt-1 whitespace-pre-line text-sm text-slate-400">
|
|
346
|
+
{{ item.detail }}
|
|
347
|
+
</p>
|
|
348
|
+
|
|
349
|
+
<!-- recorded choice -->
|
|
350
|
+
<div
|
|
351
|
+
v-if="item.reply"
|
|
352
|
+
class="mt-2 rounded-md border-l-2 border-slate-700 bg-slate-950/40 px-3 py-1.5 text-sm text-slate-300"
|
|
353
|
+
>
|
|
354
|
+
<span class="text-[10px] uppercase tracking-wide text-slate-500">
|
|
355
|
+
Your choice
|
|
356
|
+
</span>
|
|
357
|
+
<p class="whitespace-pre-line">{{ item.reply }}</p>
|
|
358
|
+
</div>
|
|
359
|
+
|
|
360
|
+
<!-- react: choose (relevant) or dismiss (irrelevant) -->
|
|
361
|
+
<template v-if="item.status === 'open' || item.status === 'answered'">
|
|
362
|
+
<UTextarea
|
|
363
|
+
v-model="drafts[item.id]"
|
|
364
|
+
:rows="2"
|
|
365
|
+
autoresize
|
|
366
|
+
size="sm"
|
|
367
|
+
class="mt-2 w-full"
|
|
368
|
+
:placeholder="
|
|
369
|
+
item.reply ? 'Refine your choice…' : 'Choose / steer this option…'
|
|
370
|
+
"
|
|
371
|
+
:disabled="frozen"
|
|
372
|
+
/>
|
|
373
|
+
<div class="mt-2 flex flex-wrap items-center gap-2">
|
|
374
|
+
<UButton
|
|
375
|
+
color="primary"
|
|
376
|
+
variant="soft"
|
|
377
|
+
size="xs"
|
|
378
|
+
icon="i-lucide-corner-down-left"
|
|
379
|
+
:disabled="!(drafts[item.id] ?? '').trim() || frozen"
|
|
380
|
+
@click="submitReply(item)"
|
|
381
|
+
>
|
|
382
|
+
Save choice
|
|
383
|
+
</UButton>
|
|
384
|
+
<UButton
|
|
385
|
+
color="neutral"
|
|
386
|
+
variant="ghost"
|
|
387
|
+
size="xs"
|
|
388
|
+
icon="i-lucide-x"
|
|
389
|
+
:disabled="frozen"
|
|
390
|
+
@click="setStatus(item, 'dismissed')"
|
|
391
|
+
>
|
|
392
|
+
Dismiss
|
|
393
|
+
</UButton>
|
|
394
|
+
</div>
|
|
395
|
+
</template>
|
|
396
|
+
|
|
397
|
+
<!-- reopen a dismissed option -->
|
|
398
|
+
<div v-else-if="item.status === 'dismissed'" class="mt-2">
|
|
399
|
+
<UButton
|
|
400
|
+
color="neutral"
|
|
401
|
+
variant="ghost"
|
|
402
|
+
size="xs"
|
|
403
|
+
icon="i-lucide-rotate-ccw"
|
|
404
|
+
:disabled="frozen"
|
|
405
|
+
@click="setStatus(item, 'open')"
|
|
406
|
+
>
|
|
407
|
+
Reopen
|
|
408
|
+
</UButton>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
<!-- converged document: the standard-format direction -->
|
|
416
|
+
<section v-if="outline" class="mt-6 border-t border-slate-800 pt-5">
|
|
417
|
+
<div class="mb-3 flex items-center gap-1.5 text-[11px] text-emerald-400">
|
|
418
|
+
<UIcon name="i-lucide-file-check-2" class="h-3.5 w-3.5" />
|
|
419
|
+
<span class="font-semibold uppercase tracking-wide">
|
|
420
|
+
{{ incorporated ? docNoun : `${docNoun} (draft)` }}
|
|
421
|
+
</span>
|
|
422
|
+
</div>
|
|
423
|
+
<div v-for="s in outline.sections" :key="s.id" class="mb-2">
|
|
424
|
+
<button
|
|
425
|
+
v-if="s.title"
|
|
426
|
+
class="group flex w-full items-center gap-2 text-left"
|
|
427
|
+
@click="toggle(s.id)"
|
|
428
|
+
>
|
|
429
|
+
<UIcon
|
|
430
|
+
name="i-lucide-chevron-right"
|
|
431
|
+
class="h-3.5 w-3.5 shrink-0 text-slate-500 transition-transform"
|
|
432
|
+
:class="collapsed[s.id] ? '' : 'rotate-90'"
|
|
433
|
+
/>
|
|
434
|
+
<span
|
|
435
|
+
class="font-semibold text-white"
|
|
436
|
+
:class="s.depth <= 1 ? 'text-base' : s.depth === 2 ? 'text-sm' : 'text-xs'"
|
|
437
|
+
v-html="s.titleHtml"
|
|
438
|
+
/>
|
|
439
|
+
</button>
|
|
440
|
+
<div
|
|
441
|
+
v-show="!s.title || !collapsed[s.id]"
|
|
442
|
+
class="reader-prose mt-1 pl-5.5 text-[13px] leading-relaxed text-slate-300"
|
|
443
|
+
v-html="s.bodyHtml"
|
|
444
|
+
/>
|
|
445
|
+
</div>
|
|
446
|
+
</section>
|
|
447
|
+
</template>
|
|
448
|
+
</div>
|
|
449
|
+
|
|
450
|
+
<!-- right action rail -->
|
|
451
|
+
<aside class="hidden w-72 shrink-0 flex-col border-l border-slate-800 lg:flex">
|
|
452
|
+
<div class="flex flex-col gap-4 px-4 py-5">
|
|
453
|
+
<div v-if="session" class="space-y-2 text-xs text-slate-400">
|
|
454
|
+
<div class="flex items-center justify-between">
|
|
455
|
+
<span>Options</span>
|
|
456
|
+
<span class="text-slate-300">{{ session.items.length }}</span>
|
|
457
|
+
</div>
|
|
458
|
+
<div class="flex items-center justify-between">
|
|
459
|
+
<span>Open</span>
|
|
460
|
+
<span class="text-slate-300">{{ openCount }}</span>
|
|
461
|
+
</div>
|
|
462
|
+
<div class="flex items-center justify-between">
|
|
463
|
+
<span>Chosen</span>
|
|
464
|
+
<span class="text-slate-300">{{ answeredCount }}</span>
|
|
465
|
+
</div>
|
|
466
|
+
<div v-if="session.model" class="flex items-center justify-between">
|
|
467
|
+
<span>Model</span>
|
|
468
|
+
<span class="truncate pl-2 text-slate-500">{{ session.model }}</span>
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
<!-- action: ready (choose → incorporate / proceed) -->
|
|
473
|
+
<div
|
|
474
|
+
v-if="session && status === 'ready'"
|
|
475
|
+
class="space-y-2 border-t border-slate-800 pt-4"
|
|
476
|
+
>
|
|
477
|
+
<UButton
|
|
478
|
+
v-if="canProceed"
|
|
479
|
+
color="primary"
|
|
480
|
+
size="sm"
|
|
481
|
+
block
|
|
482
|
+
icon="i-lucide-arrow-right"
|
|
483
|
+
:loading="acting"
|
|
484
|
+
@click="proceed"
|
|
485
|
+
>
|
|
486
|
+
Proceed (nothing to incorporate)
|
|
487
|
+
</UButton>
|
|
488
|
+
<UButton
|
|
489
|
+
v-else
|
|
490
|
+
color="primary"
|
|
491
|
+
size="sm"
|
|
492
|
+
block
|
|
493
|
+
icon="i-lucide-wand-sparkles"
|
|
494
|
+
:loading="reworking"
|
|
495
|
+
:disabled="!canIncorporate"
|
|
496
|
+
@click="incorporate()"
|
|
497
|
+
>
|
|
498
|
+
Incorporate choices
|
|
499
|
+
</UButton>
|
|
500
|
+
<p class="text-[11px] leading-relaxed text-slate-500">
|
|
501
|
+
<template v-if="canProceed">
|
|
502
|
+
Every option is dismissed — proceed to the next phase without a direction.
|
|
503
|
+
</template>
|
|
504
|
+
<template v-else-if="canIncorporate">
|
|
505
|
+
Folds your choices into one {{ docNoun }}, then re-runs the brainstorm
|
|
506
|
+
automatically.
|
|
507
|
+
</template>
|
|
508
|
+
<template v-else> Choose or dismiss every option to continue. </template>
|
|
509
|
+
</p>
|
|
510
|
+
</div>
|
|
511
|
+
|
|
512
|
+
<!-- action: merged (inspect → re-run / redo) -->
|
|
513
|
+
<div v-if="session && merged" class="space-y-2 border-t border-slate-800 pt-4">
|
|
514
|
+
<UButton
|
|
515
|
+
color="primary"
|
|
516
|
+
size="sm"
|
|
517
|
+
block
|
|
518
|
+
icon="i-lucide-sparkles"
|
|
519
|
+
:loading="busy"
|
|
520
|
+
@click="reReview"
|
|
521
|
+
>
|
|
522
|
+
{{ busy ? 'Re-running…' : 'Looks good — re-run' }}
|
|
523
|
+
</UButton>
|
|
524
|
+
<UButton
|
|
525
|
+
color="neutral"
|
|
526
|
+
variant="soft"
|
|
527
|
+
size="sm"
|
|
528
|
+
block
|
|
529
|
+
icon="i-lucide-pencil"
|
|
530
|
+
@click="showRedo = !showRedo"
|
|
531
|
+
>
|
|
532
|
+
Redo incorporation
|
|
533
|
+
</UButton>
|
|
534
|
+
<div v-if="showRedo" class="space-y-2">
|
|
535
|
+
<UTextarea
|
|
536
|
+
v-model="redoComment"
|
|
537
|
+
:rows="3"
|
|
538
|
+
autoresize
|
|
539
|
+
size="sm"
|
|
540
|
+
class="w-full"
|
|
541
|
+
placeholder="What should the direction do differently?"
|
|
542
|
+
/>
|
|
543
|
+
<UButton
|
|
544
|
+
color="primary"
|
|
545
|
+
variant="soft"
|
|
546
|
+
size="xs"
|
|
547
|
+
block
|
|
548
|
+
icon="i-lucide-wand-sparkles"
|
|
549
|
+
:loading="reworking"
|
|
550
|
+
:disabled="!redoComment.trim()"
|
|
551
|
+
@click="incorporate(redoComment.trim())"
|
|
552
|
+
>
|
|
553
|
+
Redo with this direction
|
|
554
|
+
</UButton>
|
|
555
|
+
</div>
|
|
556
|
+
<p class="text-[11px] leading-relaxed text-slate-500">
|
|
557
|
+
Re-run runs the agent against this direction. If you’re unhappy with how it was
|
|
558
|
+
merged, redo it with a comment instead.
|
|
559
|
+
</p>
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
<div
|
|
563
|
+
v-if="session && incorporated"
|
|
564
|
+
class="border-t border-slate-800 pt-4 text-[11px] leading-relaxed text-slate-500"
|
|
565
|
+
>
|
|
566
|
+
Direction settled — the pipeline is continuing with the document on the left.
|
|
567
|
+
</div>
|
|
568
|
+
</div>
|
|
569
|
+
</aside>
|
|
570
|
+
</div>
|
|
571
|
+
</div>
|
|
572
|
+
</div>
|
|
573
|
+
</Teleport>
|
|
574
|
+
</template>
|
|
575
|
+
|
|
576
|
+
<style scoped>
|
|
577
|
+
.pl-5\.5 {
|
|
578
|
+
padding-left: 1.375rem;
|
|
579
|
+
}
|
|
580
|
+
/* Minimal CommonMark styling for the converged-direction reader (mirrors the prose
|
|
581
|
+
review window's reader-prose). */
|
|
582
|
+
.reader-prose :deep(p) {
|
|
583
|
+
margin: 0.4rem 0;
|
|
584
|
+
}
|
|
585
|
+
.reader-prose :deep(ul),
|
|
586
|
+
.reader-prose :deep(ol) {
|
|
587
|
+
margin: 0.4rem 0;
|
|
588
|
+
padding-left: 1.25rem;
|
|
589
|
+
list-style: revert;
|
|
590
|
+
}
|
|
591
|
+
.reader-prose :deep(li) {
|
|
592
|
+
margin: 0.2rem 0;
|
|
593
|
+
}
|
|
594
|
+
.reader-prose :deep(strong) {
|
|
595
|
+
color: rgb(226 232 240);
|
|
596
|
+
font-weight: 600;
|
|
597
|
+
}
|
|
598
|
+
.reader-prose :deep(code) {
|
|
599
|
+
border-radius: 0.25rem;
|
|
600
|
+
background: rgb(2 6 23 / 0.6);
|
|
601
|
+
padding: 0.05rem 0.3rem;
|
|
602
|
+
font-size: 0.85em;
|
|
603
|
+
}
|
|
604
|
+
.reader-prose :deep(pre) {
|
|
605
|
+
margin: 0.5rem 0;
|
|
606
|
+
overflow-x: auto;
|
|
607
|
+
border-radius: 0.5rem;
|
|
608
|
+
background: rgb(2 6 23 / 0.6);
|
|
609
|
+
padding: 0.75rem;
|
|
610
|
+
}
|
|
611
|
+
.reader-prose :deep(blockquote) {
|
|
612
|
+
margin: 0.5rem 0;
|
|
613
|
+
border-left: 2px solid rgb(51 65 85);
|
|
614
|
+
padding-left: 0.75rem;
|
|
615
|
+
color: rgb(148 163 184);
|
|
616
|
+
}
|
|
617
|
+
</style>
|