@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.
- package/app/components/brainstorm/BrainstormWindow.vue +617 -0
- package/app/components/followUp/FollowUpWindow.vue +257 -0
- package/app/components/kaizen/KaizenPanel.vue +208 -0
- package/app/components/kaizen/KaizenStepStatus.vue +94 -0
- package/app/components/layout/NotificationsInbox.vue +13 -0
- package/app/components/layout/SideBar.vue +12 -0
- package/app/components/panels/AgentStepDetail.vue +6 -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/settings/WorkspaceSettingsPanel.vue +20 -0
- package/app/components/slack/SlackPanel.vue +1 -0
- package/app/composables/api/followUps.ts +52 -0
- package/app/composables/api/kaizen.ts +16 -0
- package/app/composables/api/reviews.ts +68 -0
- package/app/composables/useApi.ts +4 -0
- package/app/composables/useResultView.ts +3 -1
- package/app/composables/useWorkspaceStream.ts +11 -0
- package/app/pages/index.vue +2 -0
- package/app/stores/brainstorm.ts +210 -0
- package/app/stores/followUps.ts +73 -0
- package/app/stores/kaizen.ts +101 -0
- package/app/stores/pipelines.ts +25 -0
- package/app/stores/ui.ts +64 -1
- package/app/stores/workspaceSettings.ts +1 -0
- package/app/types/brainstorm.ts +55 -0
- package/app/types/domain.ts +64 -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 +68 -3
- package/package.json +1 -1
|
@@ -381,6 +381,27 @@ async function clone(p: Pipeline) {
|
|
|
381
381
|
"
|
|
382
382
|
@click="pipelines.toggleDraftConsensus(unit.index)"
|
|
383
383
|
/>
|
|
384
|
+
<!-- Follow-up companion: the future-looking Coder surfaces loose ends /
|
|
385
|
+
side-tasks / questions mid-run (coder steps only). Enabled by default. -->
|
|
386
|
+
<UButton
|
|
387
|
+
v-if="unit.kind === 'coder'"
|
|
388
|
+
:icon="
|
|
389
|
+
pipelines.draftFollowUps[unit.index] === false
|
|
390
|
+
? 'i-lucide-circle-slash'
|
|
391
|
+
: 'i-lucide-compass'
|
|
392
|
+
"
|
|
393
|
+
:color="
|
|
394
|
+
pipelines.draftFollowUps[unit.index] === false ? 'neutral' : 'secondary'
|
|
395
|
+
"
|
|
396
|
+
variant="ghost"
|
|
397
|
+
size="xs"
|
|
398
|
+
:title="
|
|
399
|
+
pipelines.draftFollowUps[unit.index] === false
|
|
400
|
+
? 'Follow-up companion disabled — click to enable (Coder surfaces loose ends / questions)'
|
|
401
|
+
: 'Follow-up companion enabled — Coder surfaces loose ends / side-tasks / questions; click to disable'
|
|
402
|
+
"
|
|
403
|
+
@click="pipelines.toggleDraftFollowUps(unit.index)"
|
|
404
|
+
/>
|
|
384
405
|
<UButton
|
|
385
406
|
icon="i-lucide-chevron-up"
|
|
386
407
|
color="neutral"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { AgentState, ExecutionInstance } from '~/types/domain'
|
|
3
|
-
import {
|
|
3
|
+
import type { PipelineStep } from '~/types/execution'
|
|
4
|
+
import { agentKindMeta, FOLLOW_UP_COMPANION_META } from '~/utils/catalog'
|
|
4
5
|
import {
|
|
5
6
|
subtaskIconClass,
|
|
6
7
|
gateCompanionFor,
|
|
@@ -43,6 +44,18 @@ function openStep(i: number) {
|
|
|
43
44
|
ui.openStepDetail(props.instance.id, i)
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
// Follow-up companion (the future-looking Coder): how many surfaced items still need a
|
|
48
|
+
// decision, and the chip's roll-up label. The chip blinks while any item is pending.
|
|
49
|
+
function followUpPending(step: PipelineStep): number {
|
|
50
|
+
return (step.followUps?.items ?? []).filter((it) => it.status === 'pending').length
|
|
51
|
+
}
|
|
52
|
+
function followUpLabel(step: PipelineStep): string {
|
|
53
|
+
const items = step.followUps?.items ?? []
|
|
54
|
+
if (items.length === 0) return 'Watching…'
|
|
55
|
+
const pending = followUpPending(step)
|
|
56
|
+
return pending > 0 ? `${pending} to decide` : 'All decided'
|
|
57
|
+
}
|
|
58
|
+
|
|
46
59
|
// --- restart from a step -----------------------------------------------------
|
|
47
60
|
// Re-run the pipeline from a chosen step onward: the server resets that step +
|
|
48
61
|
// every later step's iteration counters and re-drives a fresh run, keeping the
|
|
@@ -420,6 +433,37 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
420
433
|
</span>
|
|
421
434
|
</div>
|
|
422
435
|
|
|
436
|
+
<!-- Follow-up companion (future-looking Coder): a blinking chip that lights up the
|
|
437
|
+
moment the Coder streams an item; click to triage. Blinks while any item is
|
|
438
|
+
undecided (the gate holds the pipeline until they're all decided). -->
|
|
439
|
+
<button
|
|
440
|
+
v-if="s.followUps?.enabled"
|
|
441
|
+
type="button"
|
|
442
|
+
class="mt-3 flex w-full items-center gap-2 rounded-lg border border-dashed px-2.5 py-1.5 text-left transition hover:border-pink-400/60"
|
|
443
|
+
:class="
|
|
444
|
+
followUpPending(s) > 0
|
|
445
|
+
? 'border-pink-500/50 bg-pink-500/10 followup-blink'
|
|
446
|
+
: 'border-slate-700/70 bg-slate-900/40'
|
|
447
|
+
"
|
|
448
|
+
@click="ui.openFollowUps(instance.id, i)"
|
|
449
|
+
>
|
|
450
|
+
<span
|
|
451
|
+
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-md border border-pink-500/40 bg-pink-500/15"
|
|
452
|
+
>
|
|
453
|
+
<UIcon :name="FOLLOW_UP_COMPANION_META.icon" class="h-3 w-3 text-pink-300" />
|
|
454
|
+
</span>
|
|
455
|
+
<span class="min-w-0 flex-1 truncate text-[12px] text-slate-300">
|
|
456
|
+
{{ FOLLOW_UP_COMPANION_META.label }}
|
|
457
|
+
<span class="text-slate-500">(companion)</span>
|
|
458
|
+
</span>
|
|
459
|
+
<span
|
|
460
|
+
class="shrink-0 text-[11px] font-medium"
|
|
461
|
+
:class="followUpPending(s) > 0 ? 'text-pink-300' : 'text-slate-400'"
|
|
462
|
+
>
|
|
463
|
+
{{ followUpLabel(s) }}
|
|
464
|
+
</span>
|
|
465
|
+
</button>
|
|
466
|
+
|
|
423
467
|
<!-- reviewer gate folding/re-reviewing in the background: a working indicator,
|
|
424
468
|
NOT a "Review & approve" gate (the human is summoned only if needed) -->
|
|
425
469
|
<div
|
|
@@ -483,4 +527,18 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
483
527
|
.step-active {
|
|
484
528
|
animation: step-pulse 1.6s ease-in-out infinite;
|
|
485
529
|
}
|
|
530
|
+
/* The Follow-up companion chip blinks (pink) while it has undecided items, drawing the eye
|
|
531
|
+
to forward-looking work surfaced mid-run. */
|
|
532
|
+
@keyframes followup-blink {
|
|
533
|
+
0%,
|
|
534
|
+
100% {
|
|
535
|
+
box-shadow: 0 0 0 0 rgba(244, 114, 182, 0.5);
|
|
536
|
+
}
|
|
537
|
+
50% {
|
|
538
|
+
box-shadow: 0 0 0 5px rgba(244, 114, 182, 0);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
.followup-blink {
|
|
542
|
+
animation: followup-blink 1.4s ease-in-out infinite;
|
|
543
|
+
}
|
|
486
544
|
</style>
|
|
@@ -68,6 +68,7 @@ const draft = reactive({
|
|
|
68
68
|
taskLimitShared: 5 as number,
|
|
69
69
|
perType: {} as Record<CreateTaskType, number>,
|
|
70
70
|
storeAgentContext: true,
|
|
71
|
+
kaizenEnabled: true,
|
|
71
72
|
// Budget: empty string ⇒ "use the built-in default" (null on the wire).
|
|
72
73
|
spendCurrency: '',
|
|
73
74
|
spendMonthlyLimit: '',
|
|
@@ -81,6 +82,7 @@ function hydrate() {
|
|
|
81
82
|
const pt = s.taskLimitPerType ?? {}
|
|
82
83
|
for (const t of TASK_TYPES) draft.perType[t] = pt[t] ?? 3
|
|
83
84
|
draft.storeAgentContext = s.storeAgentContext
|
|
85
|
+
draft.kaizenEnabled = s.kaizenEnabled
|
|
84
86
|
draft.spendCurrency = s.spendCurrency ?? ''
|
|
85
87
|
draft.spendMonthlyLimit = s.spendMonthlyLimit == null ? '' : String(s.spendMonthlyLimit)
|
|
86
88
|
}
|
|
@@ -107,6 +109,7 @@ async function save() {
|
|
|
107
109
|
)
|
|
108
110
|
: null,
|
|
109
111
|
storeAgentContext: draft.storeAgentContext,
|
|
112
|
+
kaizenEnabled: draft.kaizenEnabled,
|
|
110
113
|
})
|
|
111
114
|
toast.add({ title: 'Settings saved', icon: 'i-lucide-check', color: 'success' })
|
|
112
115
|
} catch (e) {
|
|
@@ -237,6 +240,23 @@ async function saveBudget() {
|
|
|
237
240
|
</label>
|
|
238
241
|
</section>
|
|
239
242
|
|
|
243
|
+
<!-- Kaizen agent -->
|
|
244
|
+
<section class="space-y-2">
|
|
245
|
+
<h3 class="text-sm font-semibold text-slate-200">Kaizen agent</h3>
|
|
246
|
+
<p class="text-[11px] text-slate-400">
|
|
247
|
+
After each run completes, the Kaizen agent grades how every agent step went — smooth
|
|
248
|
+
and efficient vs confused and chaotic — and recommends prompt/model improvements. A
|
|
249
|
+
prompt + agent + model combination that grades highly with no recommendations five
|
|
250
|
+
times in a row is marked verified and is no longer graded. Grading runs in the
|
|
251
|
+
background and is shown inside run details and the Kaizen screen. Set the grader's
|
|
252
|
+
model in Model Configuration (the “Kaizen” agent).
|
|
253
|
+
</p>
|
|
254
|
+
<label class="flex items-center gap-2">
|
|
255
|
+
<USwitch v-model="draft.kaizenEnabled" size="sm" />
|
|
256
|
+
<span class="text-sm text-slate-200">Grade agent runs with Kaizen</span>
|
|
257
|
+
</label>
|
|
258
|
+
</section>
|
|
259
|
+
|
|
240
260
|
<div class="flex justify-end">
|
|
241
261
|
<UButton
|
|
242
262
|
color="primary"
|
|
@@ -45,6 +45,7 @@ const routes = reactive<Record<NotificationType, SlackRoute>>({
|
|
|
45
45
|
// In-app only (not in ROUTABLE), but the map is exhaustive over the type.
|
|
46
46
|
decision_required: { enabled: false, channel: '' },
|
|
47
47
|
human_test_ready: { enabled: false, channel: '' },
|
|
48
|
+
followup_pending: { enabled: false, channel: '' },
|
|
48
49
|
})
|
|
49
50
|
const mentionsEnabled = ref(false)
|
|
50
51
|
const mapping = ref<SlackMemberMappingEntry[]>([])
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { FollowUpsStepState } from '~/types/execution'
|
|
2
|
+
import type { ApiContext } from './context'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The Follow-up companion: the Coder surfaces forward-looking items (loose ends /
|
|
6
|
+
* side-tasks / questions) live on its run step; these endpoints decide each one. Every
|
|
7
|
+
* call returns the updated live state; when the run is parked on the follow-up gate and
|
|
8
|
+
* the last item is decided, the backend drives the run forward (loop the Coder for the
|
|
9
|
+
* queued / answered items, else advance).
|
|
10
|
+
*/
|
|
11
|
+
export function followUpsApi({ http, ws }: ApiContext) {
|
|
12
|
+
const base = (workspaceId: string, executionId: string) =>
|
|
13
|
+
`${ws(workspaceId)}/executions/${encodeURIComponent(executionId)}/follow-ups`
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
// The live follow-up state for a run (null when the companion is off / nothing surfaced).
|
|
17
|
+
getFollowUps: (workspaceId: string, executionId: string) =>
|
|
18
|
+
http<FollowUpsStepState | null>(base(workspaceId, executionId)),
|
|
19
|
+
|
|
20
|
+
// File a follow-up as a tracker issue (GitHub Issues / Jira).
|
|
21
|
+
fileFollowUp: (workspaceId: string, executionId: string, itemId: string) =>
|
|
22
|
+
http<FollowUpsStepState>(
|
|
23
|
+
`${base(workspaceId, executionId)}/${encodeURIComponent(itemId)}/file`,
|
|
24
|
+
{
|
|
25
|
+
method: 'POST',
|
|
26
|
+
},
|
|
27
|
+
),
|
|
28
|
+
|
|
29
|
+
// Send a follow-up back to the Coder (queued for its next pass).
|
|
30
|
+
queueFollowUp: (workspaceId: string, executionId: string, itemId: string) =>
|
|
31
|
+
http<FollowUpsStepState>(
|
|
32
|
+
`${base(workspaceId, executionId)}/${encodeURIComponent(itemId)}/queue`,
|
|
33
|
+
{
|
|
34
|
+
method: 'POST',
|
|
35
|
+
},
|
|
36
|
+
),
|
|
37
|
+
|
|
38
|
+
// Answer a question item (folded into the Coder's next pass).
|
|
39
|
+
answerFollowUp: (workspaceId: string, executionId: string, itemId: string, answer: string) =>
|
|
40
|
+
http<FollowUpsStepState>(
|
|
41
|
+
`${base(workspaceId, executionId)}/${encodeURIComponent(itemId)}/answer`,
|
|
42
|
+
{ method: 'POST', body: { answer } },
|
|
43
|
+
),
|
|
44
|
+
|
|
45
|
+
// Dismiss a follow-up / question item without acting on it.
|
|
46
|
+
dismissFollowUp: (workspaceId: string, executionId: string, itemId: string) =>
|
|
47
|
+
http<FollowUpsStepState>(
|
|
48
|
+
`${base(workspaceId, executionId)}/${encodeURIComponent(itemId)}/dismiss`,
|
|
49
|
+
{ method: 'POST' },
|
|
50
|
+
),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { KaizenGrading, KaizenOverview } from '~/types/domain'
|
|
2
|
+
import type { ApiContext } from './context'
|
|
3
|
+
|
|
4
|
+
/** Kaizen (post-run grading) read endpoints: the screen overview + a run's gradings. */
|
|
5
|
+
export function kaizenApi({ http, ws }: ApiContext) {
|
|
6
|
+
return {
|
|
7
|
+
// The Kaizen screen: recent grading history + the verified-combo library.
|
|
8
|
+
getKaizenOverview: (workspaceId: string) => http<KaizenOverview>(`${ws(workspaceId)}/kaizen`),
|
|
9
|
+
|
|
10
|
+
// The gradings recorded for one run (the run-window status surface).
|
|
11
|
+
getKaizenForExecution: (workspaceId: string, executionId: string) =>
|
|
12
|
+
http<{ gradings: KaizenGrading[] }>(
|
|
13
|
+
`${ws(workspaceId)}/executions/${encodeURIComponent(executionId)}/kaizen`,
|
|
14
|
+
),
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { ClarityReview, ResolveClarityExceededChoice } from '~/types/clarity'
|
|
2
|
+
import type {
|
|
3
|
+
BrainstormSession,
|
|
4
|
+
BrainstormStage,
|
|
5
|
+
ResolveBrainstormExceededChoice,
|
|
6
|
+
} from '~/types/brainstorm'
|
|
2
7
|
import type { ConsensusSession } from '~/types/consensus'
|
|
3
8
|
import type {
|
|
4
9
|
RequirementReview,
|
|
@@ -176,5 +181,68 @@ export function reviewsApi({ http, ws }: ApiContext) {
|
|
|
176
181
|
`${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/clarity-review/resolve-exceeded`,
|
|
177
182
|
{ method: 'POST', body: { choice } },
|
|
178
183
|
),
|
|
184
|
+
|
|
185
|
+
// ---- brainstorm (structured-dialogue agent, stage-scoped) ------------
|
|
186
|
+
// The current session for a block + stage (null when none has been run). A 503 means
|
|
187
|
+
// the feature is unconfigured (the panel hides on any error here).
|
|
188
|
+
getBrainstorm: (workspaceId: string, blockId: string, stage: BrainstormStage) =>
|
|
189
|
+
http<BrainstormSession | null>(
|
|
190
|
+
`${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/brainstorm/${stage}`,
|
|
191
|
+
),
|
|
192
|
+
|
|
193
|
+
replyBrainstormItem: (workspaceId: string, sessionId: string, itemId: string, reply: string) =>
|
|
194
|
+
http<BrainstormSession>(
|
|
195
|
+
`${ws(workspaceId)}/brainstorm-sessions/${encodeURIComponent(sessionId)}/items/${encodeURIComponent(itemId)}/reply`,
|
|
196
|
+
{ method: 'POST', body: { reply } },
|
|
197
|
+
),
|
|
198
|
+
|
|
199
|
+
setBrainstormItemStatus: (
|
|
200
|
+
workspaceId: string,
|
|
201
|
+
sessionId: string,
|
|
202
|
+
itemId: string,
|
|
203
|
+
status: ReviewItemStatus,
|
|
204
|
+
) =>
|
|
205
|
+
http<BrainstormSession>(
|
|
206
|
+
`${ws(workspaceId)}/brainstorm-sessions/${encodeURIComponent(sessionId)}/items/${encodeURIComponent(itemId)}`,
|
|
207
|
+
{ method: 'PATCH', body: { status } },
|
|
208
|
+
),
|
|
209
|
+
|
|
210
|
+
// Incorporate the picks ASYNCHRONOUSLY (the durable driver folds + re-runs).
|
|
211
|
+
incorporateBrainstorm: (
|
|
212
|
+
workspaceId: string,
|
|
213
|
+
blockId: string,
|
|
214
|
+
stage: BrainstormStage,
|
|
215
|
+
feedback?: string,
|
|
216
|
+
) =>
|
|
217
|
+
http<BrainstormSession>(
|
|
218
|
+
`${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/brainstorm/${stage}/incorporate`,
|
|
219
|
+
{ method: 'POST', body: feedback ? { feedback } : {} },
|
|
220
|
+
),
|
|
221
|
+
|
|
222
|
+
// Re-run the brainstorm against the converged direction (one more pass).
|
|
223
|
+
reReviewBrainstorm: (workspaceId: string, blockId: string, stage: BrainstormStage) =>
|
|
224
|
+
http<BrainstormSession>(
|
|
225
|
+
`${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/brainstorm/${stage}/re-review`,
|
|
226
|
+
{ method: 'POST' },
|
|
227
|
+
),
|
|
228
|
+
|
|
229
|
+
// Proceed: settle the brainstorm and advance the parked run (all options dismissed).
|
|
230
|
+
proceedBrainstorm: (workspaceId: string, blockId: string, stage: BrainstormStage) =>
|
|
231
|
+
http<BrainstormSession>(
|
|
232
|
+
`${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/brainstorm/${stage}/proceed`,
|
|
233
|
+
{ method: 'POST' },
|
|
234
|
+
),
|
|
235
|
+
|
|
236
|
+
// Resolve a session that hit its iteration cap: extra-round / proceed / stop-reset.
|
|
237
|
+
resolveBrainstormExceeded: (
|
|
238
|
+
workspaceId: string,
|
|
239
|
+
blockId: string,
|
|
240
|
+
stage: BrainstormStage,
|
|
241
|
+
choice: ResolveBrainstormExceededChoice,
|
|
242
|
+
) =>
|
|
243
|
+
http<BrainstormSession>(
|
|
244
|
+
`${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/brainstorm/${stage}/resolve-exceeded`,
|
|
245
|
+
{ method: 'POST', body: { choice } },
|
|
246
|
+
),
|
|
179
247
|
}
|
|
180
248
|
}
|
|
@@ -6,9 +6,11 @@ import { bootstrapApi } from './api/bootstrap'
|
|
|
6
6
|
import { boardApi } from './api/board'
|
|
7
7
|
import { documentsApi } from './api/documents'
|
|
8
8
|
import { executionApi } from './api/execution'
|
|
9
|
+
import { followUpsApi } from './api/followUps'
|
|
9
10
|
import { fragmentsApi } from './api/fragments'
|
|
10
11
|
import { githubApi } from './api/github'
|
|
11
12
|
import { humanTestApi } from './api/humanTest'
|
|
13
|
+
import { kaizenApi } from './api/kaizen'
|
|
12
14
|
import { modelsApi } from './api/models'
|
|
13
15
|
import { notificationsApi } from './api/notifications'
|
|
14
16
|
import { presetsApi } from './api/presets'
|
|
@@ -84,7 +86,9 @@ export function useApi() {
|
|
|
84
86
|
...documentsApi(ctx),
|
|
85
87
|
...tasksApi(ctx),
|
|
86
88
|
...reviewsApi(ctx),
|
|
89
|
+
...followUpsApi(ctx),
|
|
87
90
|
...humanTestApi(ctx),
|
|
91
|
+
...kaizenApi(ctx),
|
|
88
92
|
...specApi(ctx),
|
|
89
93
|
...notificationsApi(ctx),
|
|
90
94
|
...presetsApi(ctx),
|
|
@@ -22,6 +22,8 @@ export function useResultView(viewId: string, opts?: { onOpen?: (blockId: string
|
|
|
22
22
|
const blockId = computed(() => (open.value ? ui.resultView!.blockId : null))
|
|
23
23
|
const instanceId = computed(() => (open.value ? ui.resultView!.instanceId : null))
|
|
24
24
|
const stepIndex = computed(() => (open.value ? ui.resultView!.stepIndex : null))
|
|
25
|
+
// Set only for the brainstorm window (its two stages share one view id).
|
|
26
|
+
const stage = computed(() => (open.value ? (ui.resultView!.stage ?? null) : null))
|
|
25
27
|
|
|
26
28
|
function close() {
|
|
27
29
|
ui.closeResultView()
|
|
@@ -44,5 +46,5 @@ export function useResultView(viewId: string, opts?: { onOpen?: (blockId: string
|
|
|
44
46
|
)
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
return { open, blockId, instanceId, stepIndex, close }
|
|
49
|
+
return { open, blockId, instanceId, stepIndex, stage, close }
|
|
48
50
|
}
|
|
@@ -23,6 +23,8 @@ export function useWorkspaceStream() {
|
|
|
23
23
|
const requirements = useRequirementsStore()
|
|
24
24
|
const consensus = useConsensusStore()
|
|
25
25
|
const clarity = useClarityStore()
|
|
26
|
+
const brainstorm = useBrainstormStore()
|
|
27
|
+
const kaizen = useKaizenStore()
|
|
26
28
|
const api = useApi()
|
|
27
29
|
const apiBase = useRuntimeConfig().public.apiBase
|
|
28
30
|
|
|
@@ -87,6 +89,15 @@ export function useWorkspaceStream() {
|
|
|
87
89
|
// cache so an open review window / inspector reflects it live ("incorporating…" → the
|
|
88
90
|
// next cycle / converged). The summons back, when needed, arrives as a `notification`.
|
|
89
91
|
clarity.upsert(event.review)
|
|
92
|
+
} else if (event.type === 'brainstorm') {
|
|
93
|
+
// The async incorporate + re-run cycle changed a brainstorm session's status — patch the
|
|
94
|
+
// cache so an open brainstorm window / inspector reflects it live.
|
|
95
|
+
brainstorm.upsert(event.session)
|
|
96
|
+
} else if (event.type === 'kaizen') {
|
|
97
|
+
// A post-run Kaizen grading was scheduled, started or completed — fold it into the
|
|
98
|
+
// run cache (so an open run window shows scheduled→running→complete live) and the
|
|
99
|
+
// Kaizen screen history. Never surfaced on the board.
|
|
100
|
+
kaizen.upsert(event.grading)
|
|
90
101
|
}
|
|
91
102
|
}
|
|
92
103
|
|
package/app/pages/index.vue
CHANGED
|
@@ -11,6 +11,7 @@ import DecisionModal from '~/components/panels/DecisionModal.vue'
|
|
|
11
11
|
import AgentStepDetail from '~/components/panels/AgentStepDetail.vue'
|
|
12
12
|
import StepResultViewHost from '~/components/panels/StepResultViewHost.vue'
|
|
13
13
|
import ObservabilityPanel from '~/components/panels/ObservabilityPanel.vue'
|
|
14
|
+
import KaizenPanel from '~/components/kaizen/KaizenPanel.vue'
|
|
14
15
|
import BlockFocusView from '~/components/focus/BlockFocusView.vue'
|
|
15
16
|
import DocumentSourceConnectModal from '~/components/documents/DocumentSourceConnectModal.vue'
|
|
16
17
|
import DocumentImportModal from '~/components/documents/DocumentImportModal.vue'
|
|
@@ -171,6 +172,7 @@ watch(
|
|
|
171
172
|
<AgentStepDetail />
|
|
172
173
|
<StepResultViewHost />
|
|
173
174
|
<ObservabilityPanel />
|
|
175
|
+
<KaizenPanel />
|
|
174
176
|
<DocumentSourceConnectModal />
|
|
175
177
|
<DocumentImportModal />
|
|
176
178
|
<SpawnPreviewModal />
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import type {
|
|
4
|
+
BrainstormSession,
|
|
5
|
+
BrainstormStage,
|
|
6
|
+
ResolveBrainstormExceededChoice,
|
|
7
|
+
ReviewItemStatus,
|
|
8
|
+
} from '~/types/brainstorm'
|
|
9
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Brainstorm (structured-dialogue) state. On the pipeline path a brainstorm runs as an opt-in
|
|
13
|
+
* gate step: the run parks while the human picks / steers / dismisses the proposed options, then
|
|
14
|
+
* asks to incorporate. Incorporation + the re-run run ASYNCHRONOUSLY in the durable driver — the
|
|
15
|
+
* call returns at once (status `incorporating`) and the user goes back to the board; they are
|
|
16
|
+
* summoned again (a notification) only if the re-run yields options or hits the cap. The store is
|
|
17
|
+
* patched both from call responses and from live `brainstorm` stream events (see `upsert`).
|
|
18
|
+
*
|
|
19
|
+
* A block may have one live session per STAGE (`requirements` / `architecture`), so the cache is
|
|
20
|
+
* keyed by `${blockId}:${stage}`. `available` mirrors the backend's opt-in gate (a 503 hides the
|
|
21
|
+
* UI). Per-workspace; nothing is persisted client-side.
|
|
22
|
+
*/
|
|
23
|
+
export const useBrainstormStore = defineStore('brainstorm', () => {
|
|
24
|
+
const api = useApi()
|
|
25
|
+
const workspace = useWorkspaceStore()
|
|
26
|
+
|
|
27
|
+
const key = (blockId: string, stage: BrainstormStage) => `${blockId}:${stage}`
|
|
28
|
+
|
|
29
|
+
/** null = unknown (not probed), true/false = feature on/off. */
|
|
30
|
+
const available = ref<boolean | null>(null)
|
|
31
|
+
/** The current session per `${blockId}:${stage}` (null = fetched, none exists). */
|
|
32
|
+
const sessions = ref<Record<string, BrainstormSession | null>>({})
|
|
33
|
+
/** `${blockId}:${stage}` keys whose agent is currently running (run / re-run). */
|
|
34
|
+
const running = ref<Set<string>>(new Set())
|
|
35
|
+
/** Session ids currently incorporating their picks. */
|
|
36
|
+
const incorporating = ref<Set<string>>(new Set())
|
|
37
|
+
/** `${blockId}:${stage}` keys whose current session is being fetched (the initial `load`). */
|
|
38
|
+
const loadingByKey = ref<Set<string>>(new Set())
|
|
39
|
+
const inFlight = new Map<string, Promise<void>>()
|
|
40
|
+
|
|
41
|
+
function sessionFor(blockId: string, stage: BrainstormStage): BrainstormSession | null {
|
|
42
|
+
return sessions.value[key(blockId, stage)] ?? null
|
|
43
|
+
}
|
|
44
|
+
/** The async background stage a session is in, or null (so the board can show "working"). */
|
|
45
|
+
function backgroundStage(
|
|
46
|
+
blockId: string,
|
|
47
|
+
stage: BrainstormStage,
|
|
48
|
+
): 'incorporating' | 'reviewing' | null {
|
|
49
|
+
const status = sessions.value[key(blockId, stage)]?.status
|
|
50
|
+
return status === 'incorporating' || status === 'reviewing' ? status : null
|
|
51
|
+
}
|
|
52
|
+
function isRunning(blockId: string, stage: BrainstormStage): boolean {
|
|
53
|
+
return running.value.has(key(blockId, stage))
|
|
54
|
+
}
|
|
55
|
+
function isLoading(blockId: string, stage: BrainstormStage): boolean {
|
|
56
|
+
return loadingByKey.value.has(key(blockId, stage))
|
|
57
|
+
}
|
|
58
|
+
function isIncorporating(sessionId: string): boolean {
|
|
59
|
+
return incorporating.value.has(sessionId)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Options still needing a human (status `open`). */
|
|
63
|
+
function openCount(session: BrainstormSession): number {
|
|
64
|
+
return session.items.filter((i) => i.status === 'open').length
|
|
65
|
+
}
|
|
66
|
+
/** Options the human chose (a reply recorded), which the companion folds in. */
|
|
67
|
+
function answeredCount(session: BrainstormSession): number {
|
|
68
|
+
return session.items.filter((i) => i.status === 'answered' || i.status === 'resolved').length
|
|
69
|
+
}
|
|
70
|
+
/** Every option is settled (chosen or dismissed) — none still open. */
|
|
71
|
+
function allSettled(session: BrainstormSession): boolean {
|
|
72
|
+
return openCount(session) === 0
|
|
73
|
+
}
|
|
74
|
+
/** Incorporation is possible: all options settled AND at least one was chosen. */
|
|
75
|
+
function canIncorporate(session: BrainstormSession): boolean {
|
|
76
|
+
return allSettled(session) && answeredCount(session) > 0
|
|
77
|
+
}
|
|
78
|
+
/** Proceed (skip the companion) is possible: all options settled but none chosen. */
|
|
79
|
+
function canProceed(session: BrainstormSession): boolean {
|
|
80
|
+
return allSettled(session) && answeredCount(session) === 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function store(session: BrainstormSession) {
|
|
84
|
+
sessions.value = { ...sessions.value, [key(session.blockId, session.stage)]: session }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function withFlag(set: typeof running, k: string, on: boolean) {
|
|
88
|
+
const next = new Set(set.value)
|
|
89
|
+
if (on) next.add(k)
|
|
90
|
+
else next.delete(k)
|
|
91
|
+
set.value = next
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Fetch the current session for a block + stage (probing the feature's availability). */
|
|
95
|
+
async function load(blockId: string, stage: BrainstormStage) {
|
|
96
|
+
if (!workspace.workspaceId) return
|
|
97
|
+
const k = key(blockId, stage)
|
|
98
|
+
const pending = inFlight.get(k)
|
|
99
|
+
if (pending) return pending
|
|
100
|
+
const promise = (async () => {
|
|
101
|
+
withFlag(loadingByKey, k, true)
|
|
102
|
+
try {
|
|
103
|
+
const session = await api.getBrainstorm(workspace.requireId(), blockId, stage)
|
|
104
|
+
available.value = true
|
|
105
|
+
sessions.value = { ...sessions.value, [k]: session }
|
|
106
|
+
} catch {
|
|
107
|
+
available.value = false
|
|
108
|
+
} finally {
|
|
109
|
+
withFlag(loadingByKey, k, false)
|
|
110
|
+
inFlight.delete(k)
|
|
111
|
+
}
|
|
112
|
+
})()
|
|
113
|
+
inFlight.set(k, promise)
|
|
114
|
+
return promise
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Record a human's choice on one option. */
|
|
118
|
+
async function reply(session: BrainstormSession, itemId: string, text: string) {
|
|
119
|
+
store(await api.replyBrainstormItem(workspace.requireId(), session.id, itemId, text))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Set an option's status (dismiss / reopen). */
|
|
123
|
+
async function setItemStatus(
|
|
124
|
+
session: BrainstormSession,
|
|
125
|
+
itemId: string,
|
|
126
|
+
status: ReviewItemStatus,
|
|
127
|
+
) {
|
|
128
|
+
store(await api.setBrainstormItemStatus(workspace.requireId(), session.id, itemId, status))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Ask the driver to incorporate the picks ASYNCHRONOUSLY. Optional `feedback` is the "do it
|
|
133
|
+
* differently" direction when redoing a merge. Returns at once with the `incorporating`
|
|
134
|
+
* session (the fold + re-run happen in the background).
|
|
135
|
+
*/
|
|
136
|
+
async function incorporate(session: BrainstormSession, feedback?: string) {
|
|
137
|
+
withFlag(incorporating, session.id, true)
|
|
138
|
+
try {
|
|
139
|
+
const updated = await api.incorporateBrainstorm(
|
|
140
|
+
workspace.requireId(),
|
|
141
|
+
session.blockId,
|
|
142
|
+
session.stage,
|
|
143
|
+
feedback,
|
|
144
|
+
)
|
|
145
|
+
store(updated)
|
|
146
|
+
return updated
|
|
147
|
+
} finally {
|
|
148
|
+
withFlag(incorporating, session.id, false)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Re-run the brainstorm against the converged direction (one more pass; may converge/advance). */
|
|
153
|
+
async function reReview(blockId: string, stage: BrainstormStage): Promise<BrainstormSession> {
|
|
154
|
+
withFlag(running, key(blockId, stage), true)
|
|
155
|
+
try {
|
|
156
|
+
const updated = await api.reReviewBrainstorm(workspace.requireId(), blockId, stage)
|
|
157
|
+
store(updated)
|
|
158
|
+
return updated
|
|
159
|
+
} finally {
|
|
160
|
+
withFlag(running, key(blockId, stage), false)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Proceed: settle the brainstorm and advance the parked run. */
|
|
165
|
+
async function proceed(blockId: string, stage: BrainstormStage): Promise<BrainstormSession> {
|
|
166
|
+
const updated = await api.proceedBrainstorm(workspace.requireId(), blockId, stage)
|
|
167
|
+
store(updated)
|
|
168
|
+
return updated
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Resolve a capped session: extra-round / proceed / stop-reset. */
|
|
172
|
+
async function resolveExceeded(
|
|
173
|
+
blockId: string,
|
|
174
|
+
stage: BrainstormStage,
|
|
175
|
+
choice: ResolveBrainstormExceededChoice,
|
|
176
|
+
): Promise<BrainstormSession> {
|
|
177
|
+
const updated = await api.resolveBrainstormExceeded(
|
|
178
|
+
workspace.requireId(),
|
|
179
|
+
blockId,
|
|
180
|
+
stage,
|
|
181
|
+
choice,
|
|
182
|
+
)
|
|
183
|
+
store(updated)
|
|
184
|
+
return updated
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
available,
|
|
189
|
+
sessions,
|
|
190
|
+
sessionFor,
|
|
191
|
+
backgroundStage,
|
|
192
|
+
isRunning,
|
|
193
|
+
isLoading,
|
|
194
|
+
isIncorporating,
|
|
195
|
+
openCount,
|
|
196
|
+
answeredCount,
|
|
197
|
+
allSettled,
|
|
198
|
+
canIncorporate,
|
|
199
|
+
canProceed,
|
|
200
|
+
load,
|
|
201
|
+
reply,
|
|
202
|
+
setItemStatus,
|
|
203
|
+
incorporate,
|
|
204
|
+
reReview,
|
|
205
|
+
proceed,
|
|
206
|
+
resolveExceeded,
|
|
207
|
+
// Patch the cache from a live `brainstorm` stream event.
|
|
208
|
+
upsert: store,
|
|
209
|
+
}
|
|
210
|
+
})
|