@cat-factory/app 0.31.0 → 0.32.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/nodes/BlockNode.vue +76 -66
- 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,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
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { useApi } from '~/composables/useApi'
|
|
4
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
5
|
+
import { useExecutionStore } from '~/stores/execution'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The Follow-up companion action surface. The live item state lives on the run's Coder step
|
|
9
|
+
* (`step.followUps`) and is kept fresh by the execution stream, so the window reads items
|
|
10
|
+
* straight off the execution store — this store only wraps the decide actions (file / send
|
|
11
|
+
* back / answer / dismiss) and tracks which item is mid-action so the window can disable its
|
|
12
|
+
* buttons. The returned state is also pushed back into the execution store so the UI updates
|
|
13
|
+
* immediately even before the stream echoes the change.
|
|
14
|
+
*/
|
|
15
|
+
export const useFollowUpsStore = defineStore('followUps', () => {
|
|
16
|
+
const api = useApi()
|
|
17
|
+
const workspace = useWorkspaceStore()
|
|
18
|
+
const execution = useExecutionStore()
|
|
19
|
+
|
|
20
|
+
/** Item ids with an action in flight (drives per-row spinners / disabled buttons). */
|
|
21
|
+
const acting = ref<Set<string>>(new Set())
|
|
22
|
+
/** The last error message from an action, surfaced inline; cleared on the next action. */
|
|
23
|
+
const error = ref<string | null>(null)
|
|
24
|
+
|
|
25
|
+
function isActing(itemId: string): boolean {
|
|
26
|
+
return acting.value.has(itemId)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function mark(itemId: string, on: boolean) {
|
|
30
|
+
const next = new Set(acting.value)
|
|
31
|
+
if (on) next.add(itemId)
|
|
32
|
+
else next.delete(itemId)
|
|
33
|
+
acting.value = next
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Run one decide action, reflecting the returned state onto the run's Coder step. */
|
|
37
|
+
async function act(
|
|
38
|
+
executionId: string,
|
|
39
|
+
itemId: string,
|
|
40
|
+
call: (ws: string) => Promise<unknown>,
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
error.value = null
|
|
43
|
+
mark(itemId, true)
|
|
44
|
+
try {
|
|
45
|
+
const state = await call(workspace.requireId())
|
|
46
|
+
// Reflect the authoritative state immediately (the stream will also echo it).
|
|
47
|
+
const instance = execution.getInstance(executionId)
|
|
48
|
+
const step = instance?.steps.find((s) => s.followUps?.enabled)
|
|
49
|
+
if (step && state && typeof state === 'object') {
|
|
50
|
+
step.followUps = state as typeof step.followUps
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
error.value = e instanceof Error ? e.message : 'Action failed'
|
|
54
|
+
throw e
|
|
55
|
+
} finally {
|
|
56
|
+
mark(itemId, false)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const fileItem = (executionId: string, itemId: string) =>
|
|
61
|
+
act(executionId, itemId, (ws) => api.fileFollowUp(ws, executionId, itemId))
|
|
62
|
+
|
|
63
|
+
const queueItem = (executionId: string, itemId: string) =>
|
|
64
|
+
act(executionId, itemId, (ws) => api.queueFollowUp(ws, executionId, itemId))
|
|
65
|
+
|
|
66
|
+
const answerItem = (executionId: string, itemId: string, answer: string) =>
|
|
67
|
+
act(executionId, itemId, (ws) => api.answerFollowUp(ws, executionId, itemId, answer))
|
|
68
|
+
|
|
69
|
+
const dismissItem = (executionId: string, itemId: string) =>
|
|
70
|
+
act(executionId, itemId, (ws) => api.dismissFollowUp(ws, executionId, itemId))
|
|
71
|
+
|
|
72
|
+
return { acting, error, isActing, fileItem, queueItem, answerItem, dismissItem }
|
|
73
|
+
})
|
package/app/stores/pipelines.ts
CHANGED
|
@@ -48,6 +48,11 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
48
48
|
const draftConsensus = ref<(ConsensusStepConfig | null)[]>([])
|
|
49
49
|
/** Per-step estimate gating, kept index-aligned with `draft` (null ⇒ always run). */
|
|
50
50
|
const draftGating = ref<(StepGating | null)[]>([])
|
|
51
|
+
/**
|
|
52
|
+
* Per-step Follow-up companion toggle, kept index-aligned with `draft`. Only meaningful on
|
|
53
|
+
* a `coder` step; `false` disables the companion there (default/true ⇒ enabled).
|
|
54
|
+
*/
|
|
55
|
+
const draftFollowUps = ref<(boolean | null)[]>([])
|
|
51
56
|
/** Organizational labels for the pipeline being assembled/edited. */
|
|
52
57
|
const draftLabels = ref<string[]>([])
|
|
53
58
|
const draftName = ref('New pipeline')
|
|
@@ -71,6 +76,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
71
76
|
draftThresholds.value.splice(index, 0, null)
|
|
72
77
|
draftConsensus.value.splice(index, 0, null)
|
|
73
78
|
draftGating.value.splice(index, 0, null)
|
|
79
|
+
draftFollowUps.value.splice(index, 0, null)
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
function addToDraft(kind: AgentKind) {
|
|
@@ -84,6 +90,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
84
90
|
draftThresholds.value.splice(index, 1)
|
|
85
91
|
draftConsensus.value.splice(index, 1)
|
|
86
92
|
draftGating.value.splice(index, 1)
|
|
93
|
+
draftFollowUps.value.splice(index, 1)
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
function moveInDraft(from: number, to: number) {
|
|
@@ -100,6 +107,8 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
100
107
|
draftConsensus.value.splice(to, 0, cons ?? null)
|
|
101
108
|
const [gat] = draftGating.value.splice(from, 1)
|
|
102
109
|
draftGating.value.splice(to, 0, gat ?? null)
|
|
110
|
+
const [fu] = draftFollowUps.value.splice(from, 1)
|
|
111
|
+
draftFollowUps.value.splice(to, 0, fu ?? null)
|
|
103
112
|
}
|
|
104
113
|
|
|
105
114
|
/** Whether the producer step at `index` currently has its companion attached after it. */
|
|
@@ -174,6 +183,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
174
183
|
draftThresholds.value = reorder(draftThresholds.value)
|
|
175
184
|
draftConsensus.value = reorder(draftConsensus.value)
|
|
176
185
|
draftGating.value = reorder(draftGating.value)
|
|
186
|
+
draftFollowUps.value = reorder(draftFollowUps.value)
|
|
177
187
|
}
|
|
178
188
|
|
|
179
189
|
/** Toggle the consensus mechanism on the draft step at `index` (default config / off). */
|
|
@@ -191,6 +201,12 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
191
201
|
draftGates.value[index] = !draftGates.value[index]
|
|
192
202
|
}
|
|
193
203
|
|
|
204
|
+
/** Toggle the Follow-up companion on the draft (coder) step at `index` (default on → off). */
|
|
205
|
+
function toggleDraftFollowUps(index: number) {
|
|
206
|
+
// Default (null/true) is enabled, so the first toggle disables it (false); toggle back to null.
|
|
207
|
+
draftFollowUps.value[index] = draftFollowUps.value[index] === false ? null : false
|
|
208
|
+
}
|
|
209
|
+
|
|
194
210
|
/** Enable/disable the draft step at `index` without removing it. */
|
|
195
211
|
function toggleDraftEnabled(index: number) {
|
|
196
212
|
draftEnabled.value[index] = draftEnabled.value[index] === false
|
|
@@ -203,6 +219,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
203
219
|
draftThresholds.value = []
|
|
204
220
|
draftConsensus.value = []
|
|
205
221
|
draftGating.value = []
|
|
222
|
+
draftFollowUps.value = []
|
|
206
223
|
draftLabels.value = []
|
|
207
224
|
draftName.value = 'New pipeline'
|
|
208
225
|
editingId.value = null
|
|
@@ -216,6 +233,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
216
233
|
draftThresholds.value = pipeline.agentKinds.map((_, i) => pipeline.thresholds?.[i] ?? null)
|
|
217
234
|
draftConsensus.value = pipeline.agentKinds.map((_, i) => pipeline.consensus?.[i] ?? null)
|
|
218
235
|
draftGating.value = pipeline.agentKinds.map((_, i) => pipeline.gating?.[i] ?? null)
|
|
236
|
+
draftFollowUps.value = pipeline.agentKinds.map((_, i) => pipeline.followUps?.[i] ?? null)
|
|
219
237
|
draftLabels.value = [...(pipeline.labels ?? [])]
|
|
220
238
|
draftName.value = pipeline.name
|
|
221
239
|
editingId.value = pipeline.id
|
|
@@ -240,6 +258,11 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
240
258
|
: {}),
|
|
241
259
|
// Only send gating when at least one step has gating enabled.
|
|
242
260
|
...(draftGating.value.some((g) => g?.enabled) ? { gating: [...draftGating.value] } : {}),
|
|
261
|
+
// Only send followUps when at least one step disables it (default is on, so only the
|
|
262
|
+
// explicit `false` opt-outs are worth persisting).
|
|
263
|
+
...(draftFollowUps.value.some((f) => f === false)
|
|
264
|
+
? { followUps: [...draftFollowUps.value] }
|
|
265
|
+
: {}),
|
|
243
266
|
// Only send labels when there are any.
|
|
244
267
|
...(draftLabels.value.length ? { labels: [...draftLabels.value] } : {}),
|
|
245
268
|
}
|
|
@@ -297,6 +320,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
297
320
|
draftThresholds,
|
|
298
321
|
draftConsensus,
|
|
299
322
|
draftGating,
|
|
323
|
+
draftFollowUps,
|
|
300
324
|
draftLabels,
|
|
301
325
|
draftName,
|
|
302
326
|
editingId,
|
|
@@ -311,6 +335,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
311
335
|
toggleCompanion,
|
|
312
336
|
toggleDraftGating,
|
|
313
337
|
toggleDraftGate,
|
|
338
|
+
toggleDraftFollowUps,
|
|
314
339
|
toggleDraftEnabled,
|
|
315
340
|
toggleDraftConsensus,
|
|
316
341
|
setDraftConsensus,
|
package/app/stores/ui.ts
CHANGED
|
@@ -135,6 +135,10 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
135
135
|
blockId: string
|
|
136
136
|
instanceId: string | null
|
|
137
137
|
stepIndex: number | null
|
|
138
|
+
// The brainstorm dialogue stage, set only when `view === 'brainstorm'` (its two agent
|
|
139
|
+
// kinds share one window). Derived from the step's agent kind on the pipeline path, or
|
|
140
|
+
// passed explicitly on an off-path open.
|
|
141
|
+
stage?: 'requirements' | 'architecture'
|
|
138
142
|
} | null>(null)
|
|
139
143
|
|
|
140
144
|
// Agent step-detail overlay: which pipeline step (a run instance + step index)
|
|
@@ -233,7 +237,20 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
233
237
|
? agentKindMeta(step.agentKind).resultView
|
|
234
238
|
: undefined
|
|
235
239
|
if (view && instance) {
|
|
236
|
-
|
|
240
|
+
// The brainstorm window is shared by both stages; carry which one from the step's kind.
|
|
241
|
+
const stage =
|
|
242
|
+
view === 'brainstorm'
|
|
243
|
+
? step?.agentKind === 'architecture-brainstorm'
|
|
244
|
+
? 'architecture'
|
|
245
|
+
: 'requirements'
|
|
246
|
+
: undefined
|
|
247
|
+
resultView.value = {
|
|
248
|
+
view,
|
|
249
|
+
blockId: instance.blockId,
|
|
250
|
+
instanceId,
|
|
251
|
+
stepIndex,
|
|
252
|
+
...(stage ? { stage } : {}),
|
|
253
|
+
}
|
|
237
254
|
return
|
|
238
255
|
}
|
|
239
256
|
stepDetail.value = { instanceId, stepIndex }
|
|
@@ -456,10 +473,41 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
456
473
|
function openClarityReview(blockId: string) {
|
|
457
474
|
resultView.value = { view: 'clarity-review', blockId, instanceId: null, stepIndex: null }
|
|
458
475
|
}
|
|
476
|
+
function openBrainstorm(blockId: string, stage: 'requirements' | 'architecture') {
|
|
477
|
+
resultView.value = { view: 'brainstorm', blockId, instanceId: null, stepIndex: null, stage }
|
|
478
|
+
}
|
|
459
479
|
// Open the service-spec window for a service frame (the inspector's "View Requirements").
|
|
460
480
|
function openServiceSpec(blockId: string) {
|
|
461
481
|
resultView.value = { view: 'service-spec', blockId, instanceId: null, stepIndex: null }
|
|
462
482
|
}
|
|
483
|
+
// Open the Follow-up companion window for a run's Coder step (the blinking chip + the
|
|
484
|
+
// `followup_pending` notification). Resolves the Coder step index from the run when not
|
|
485
|
+
// given, so callers that only know the run can still open it.
|
|
486
|
+
function openFollowUps(instanceId: string, stepIndex: number | null = null) {
|
|
487
|
+
const execution = useExecutionStore()
|
|
488
|
+
const instance = execution.getInstance(instanceId)
|
|
489
|
+
if (!instance) return
|
|
490
|
+
// A pipeline may carry more than one follow-up-enabled Coder step, so don't blindly pick
|
|
491
|
+
// the first when no index is given: prefer the step that still has undecided items (the
|
|
492
|
+
// one the run is parked on), else the current step, else the first enabled one.
|
|
493
|
+
const resolveIdx = () => {
|
|
494
|
+
const pending = instance.steps.findIndex(
|
|
495
|
+
(s) => s.followUps?.enabled && s.followUps.items.some((i) => i.status === 'pending'),
|
|
496
|
+
)
|
|
497
|
+
if (pending >= 0) return pending
|
|
498
|
+
const current = instance.steps[instance.currentStep]
|
|
499
|
+
if (current?.followUps?.enabled) return instance.currentStep
|
|
500
|
+
return instance.steps.findIndex((s) => s.followUps?.enabled)
|
|
501
|
+
}
|
|
502
|
+
const idx = stepIndex ?? resolveIdx()
|
|
503
|
+
if (idx < 0) return
|
|
504
|
+
resultView.value = {
|
|
505
|
+
view: 'follow-ups',
|
|
506
|
+
blockId: instance.blockId,
|
|
507
|
+
instanceId,
|
|
508
|
+
stepIndex: idx,
|
|
509
|
+
}
|
|
510
|
+
}
|
|
463
511
|
function closeResultView() {
|
|
464
512
|
resultView.value = null
|
|
465
513
|
}
|
|
@@ -594,7 +642,9 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
594
642
|
resetAiOnboarding,
|
|
595
643
|
openRequirementReview,
|
|
596
644
|
openClarityReview,
|
|
645
|
+
openBrainstorm,
|
|
597
646
|
openServiceSpec,
|
|
647
|
+
openFollowUps,
|
|
598
648
|
closeRequirementReview,
|
|
599
649
|
openStepDetail,
|
|
600
650
|
closeStepDetail,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Brainstorm (structured-dialogue) wire types. Mirror of `@cat-factory/contracts`'
|
|
2
|
+
// brainstorm.ts, kept in sync by hand like the rest of `~/types/*` (the SPA does not import
|
|
3
|
+
// the backend package directly).
|
|
4
|
+
//
|
|
5
|
+
// A brainstorm agent runs a structured dialogue: it PROPOSES options with explicit
|
|
6
|
+
// trade-offs (raised as review items), a human picks / steers / dismisses, and the picks are
|
|
7
|
+
// folded into ONE converged direction. There are two stages (`requirements`, `architecture`)
|
|
8
|
+
// served by one engine; a block may have one live session per stage.
|
|
9
|
+
//
|
|
10
|
+
// Structurally identical to a requirements review (the items share the same shape), so the
|
|
11
|
+
// per-item types are reused from `~/types/requirements`; only the `stage` discriminator and
|
|
12
|
+
// the converged document (`convergedDirection`) differ.
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
RequirementReviewItem,
|
|
16
|
+
ReviewItemCategory,
|
|
17
|
+
ReviewItemSeverity,
|
|
18
|
+
ReviewItemStatus,
|
|
19
|
+
} from '~/types/requirements'
|
|
20
|
+
|
|
21
|
+
export type { ReviewItemCategory, ReviewItemSeverity, ReviewItemStatus }
|
|
22
|
+
|
|
23
|
+
/** Which dialogue a brainstorm session drives. */
|
|
24
|
+
export type BrainstormStage = 'requirements' | 'architecture'
|
|
25
|
+
|
|
26
|
+
/** A brainstorm option is the same shape as a requirements-review item. */
|
|
27
|
+
export type BrainstormItem = RequirementReviewItem
|
|
28
|
+
|
|
29
|
+
/** Lifecycle of a brainstorm session — identical to the requirements review lifecycle. */
|
|
30
|
+
export type BrainstormStatus =
|
|
31
|
+
| 'ready'
|
|
32
|
+
| 'incorporating'
|
|
33
|
+
| 'reviewing'
|
|
34
|
+
| 'merged'
|
|
35
|
+
| 'exceeded'
|
|
36
|
+
| 'incorporated'
|
|
37
|
+
|
|
38
|
+
/** How a human resolves a session that hit its iteration cap. */
|
|
39
|
+
export type ResolveBrainstormExceededChoice = 'extra-round' | 'proceed' | 'stop-reset'
|
|
40
|
+
|
|
41
|
+
export interface BrainstormSession {
|
|
42
|
+
id: string
|
|
43
|
+
blockId: string
|
|
44
|
+
stage: BrainstormStage
|
|
45
|
+
status: BrainstormStatus
|
|
46
|
+
items: BrainstormItem[]
|
|
47
|
+
model: string | null
|
|
48
|
+
convergedDirection: string | null
|
|
49
|
+
/** Agent passes run so far (initial pass is 1; each re-run adds one). */
|
|
50
|
+
iteration: number
|
|
51
|
+
/** The agent-pass budget (from the task's merge preset; an extra round bumps it). */
|
|
52
|
+
maxIterations: number
|
|
53
|
+
createdAt: number
|
|
54
|
+
updatedAt: number
|
|
55
|
+
}
|
package/app/types/domain.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type { Notification } from './notifications'
|
|
|
20
20
|
import type { RequirementReview } from './requirements'
|
|
21
21
|
import type { ConsensusSession, ConsensusStepConfig, StepGating, TaskEstimate } from './consensus'
|
|
22
22
|
import type { ClarityReview } from './clarity'
|
|
23
|
+
import type { BrainstormSession } from './brainstorm'
|
|
23
24
|
import type { MergeThresholdPreset } from './merge'
|
|
24
25
|
import type { ModelPreset } from './model-presets'
|
|
25
26
|
import type { PipelineSchedule } from './recurring'
|
|
@@ -256,6 +257,11 @@ export interface TestReport {
|
|
|
256
257
|
/** The kinds of agents available in the agent palette. */
|
|
257
258
|
export type AgentKind =
|
|
258
259
|
| 'requirements-review'
|
|
260
|
+
// Brainstorm (structured-dialogue) gates: propose options with trade-offs and let the human
|
|
261
|
+
// converge. `requirements-brainstorm` runs before the requirements review; `architecture-
|
|
262
|
+
// brainstorm` before the architect. Both open the shared brainstorm window.
|
|
263
|
+
| 'requirements-brainstorm'
|
|
264
|
+
| 'architecture-brainstorm'
|
|
259
265
|
| 'architect'
|
|
260
266
|
| 'researcher'
|
|
261
267
|
| 'coder'
|
|
@@ -390,6 +396,12 @@ export interface Pipeline {
|
|
|
390
396
|
* always run. Used to make a companion conditional on the task estimate.
|
|
391
397
|
*/
|
|
392
398
|
gating?: (StepGating | null)[]
|
|
399
|
+
/**
|
|
400
|
+
* Per-step Follow-up companion toggle, parallel to `agentKinds`: governs whether a `coder`
|
|
401
|
+
* step runs the future-looking Follow-up companion. `followUps[i] === false` disables it;
|
|
402
|
+
* `null`/`true`/absent ⇒ enabled (a Coder step gets it by default). Ignored on non-coder steps.
|
|
403
|
+
*/
|
|
404
|
+
followUps?: (boolean | null)[]
|
|
393
405
|
/** Free-form organizational labels for the library (filter/search). */
|
|
394
406
|
labels?: string[]
|
|
395
407
|
/** True when archived: kept but hidden from the default library view. */
|
|
@@ -583,6 +595,7 @@ export type WorkspaceEvent =
|
|
|
583
595
|
| { type: 'requirements'; review: RequirementReview; at: number }
|
|
584
596
|
| { type: 'consensus'; session: ConsensusSession; at: number }
|
|
585
597
|
| { type: 'clarity'; review: ClarityReview; at: number }
|
|
598
|
+
| { type: 'brainstorm'; session: BrainstormSession; at: number }
|
|
586
599
|
| { type: 'kaizen'; grading: KaizenGrading; at: number }
|
|
587
600
|
|
|
588
601
|
/** Level-of-detail buckets driven by the canvas zoom level. Shallow → deep:
|
package/app/types/execution.ts
CHANGED
|
@@ -382,6 +382,47 @@ export interface PipelineStep {
|
|
|
382
382
|
* Mirrors `runEnvironmentSchema`.
|
|
383
383
|
*/
|
|
384
384
|
environment?: RunEnvironment | null
|
|
385
|
+
/**
|
|
386
|
+
* Live Follow-up companion state when this (coder) step has the future-looking companion
|
|
387
|
+
* enabled: the forward-looking items the Coder streamed (loose ends / side-tasks /
|
|
388
|
+
* questions) and the send-back loop budget. The chip blinks while any item is `pending`;
|
|
389
|
+
* the gate holds the pipeline until every item is decided. Mirrors `followUpsStepStateSchema`.
|
|
390
|
+
*/
|
|
391
|
+
followUps?: FollowUpsStepState | null
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** What a streamed item is: a forward-looking follow-up or a clarifying question. */
|
|
395
|
+
export type FollowUpItemKind = 'follow_up' | 'question'
|
|
396
|
+
|
|
397
|
+
/** Lifecycle of a single follow-up / question item (mirrors `followUpItemStatusSchema`). */
|
|
398
|
+
export type FollowUpItemStatus = 'pending' | 'filed' | 'queued' | 'answered' | 'dismissed'
|
|
399
|
+
|
|
400
|
+
/** One forward-looking item the Coder surfaced (mirrors `followUpItemSchema`). */
|
|
401
|
+
export interface FollowUpItem {
|
|
402
|
+
id: string
|
|
403
|
+
kind: FollowUpItemKind
|
|
404
|
+
title: string
|
|
405
|
+
detail: string
|
|
406
|
+
suggestedAction?: string | null
|
|
407
|
+
status: FollowUpItemStatus
|
|
408
|
+
/** The human's answer to a `question` item, or null while unanswered / not a question. */
|
|
409
|
+
answer?: string | null
|
|
410
|
+
/** Canonical external id of the filed ticket (e.g. "owner/repo#123"), when `filed`. */
|
|
411
|
+
ticketExternalId?: string | null
|
|
412
|
+
/** URL of the filed ticket, when `filed`. */
|
|
413
|
+
ticketUrl?: string | null
|
|
414
|
+
/** True once a queued / answered item was folded into a Coder loop-back. */
|
|
415
|
+
sentToCoder?: boolean
|
|
416
|
+
createdAt: number
|
|
417
|
+
updatedAt: number
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Live Follow-up companion state on the Coder step (mirrors `followUpsStepStateSchema`). */
|
|
421
|
+
export interface FollowUpsStepState {
|
|
422
|
+
enabled: boolean
|
|
423
|
+
items: FollowUpItem[]
|
|
424
|
+
loops?: number
|
|
425
|
+
maxLoops?: number
|
|
385
426
|
}
|
|
386
427
|
|
|
387
428
|
/** One failing CI check the gate's precheck saw (mirrors `gateFailingCheckSchema`). */
|
|
@@ -15,6 +15,7 @@ export type NotificationType =
|
|
|
15
15
|
| 'release_regression'
|
|
16
16
|
| 'decision_required'
|
|
17
17
|
| 'human_test_ready'
|
|
18
|
+
| 'followup_pending'
|
|
18
19
|
export type NotificationStatus = 'open' | 'acted' | 'dismissed'
|
|
19
20
|
|
|
20
21
|
/** The on-call agent's recommendation on a `release_regression`. */
|