@cat-factory/app 0.121.0 → 0.121.2
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/composables/useUpsertList.spec.ts +13 -0
- package/app/stores/accounts.ts +9 -9
- package/app/stores/agentRuns.spec.ts +54 -1
- package/app/stores/agentRuns.ts +10 -19
- package/app/stores/bootstrap.ts +9 -5
- package/app/stores/github.ts +15 -8
- package/app/stores/pipelines.ts +12 -10
- package/app/stores/releaseHealth.ts +8 -5
- package/app/stores/sharedStacks.ts +3 -8
- package/app/stores/ui/modals.ts +810 -0
- package/app/stores/ui/navigation.ts +63 -0
- package/app/stores/ui/resultViews.ts +246 -0
- package/app/stores/ui.spec.ts +30 -0
- package/app/stores/ui.ts +19 -1072
- package/package.json +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { computed, ref } from 'vue'
|
|
2
|
+
import type { LodLevel } from '~/types/domain'
|
|
3
|
+
import { zoomToLod } from '~/composables/useSemanticZoom'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The board-navigation slice of the UI store: selection / focus, canvas zoom + the derived
|
|
7
|
+
* level-of-detail, and the (retained) expanded-frame set. Hot paths (zoom/pan/select) live here,
|
|
8
|
+
* isolated from the modal + result-view state, per refactoring candidate #4. Composed into
|
|
9
|
+
* {@link useUiStore}; the returned refs/actions keep their names, so consumers are unchanged.
|
|
10
|
+
*/
|
|
11
|
+
export function createUiNavigation() {
|
|
12
|
+
const selectedBlockId = ref<string | null>(null)
|
|
13
|
+
const focusBlockId = ref<string | null>(null)
|
|
14
|
+
|
|
15
|
+
/** Current canvas zoom (driven by Vue Flow viewport). */
|
|
16
|
+
const zoom = ref(1)
|
|
17
|
+
|
|
18
|
+
const lod = computed<LodLevel>(() => zoomToLod(zoom.value))
|
|
19
|
+
|
|
20
|
+
/** Frames the user has manually expanded to reveal their tasks. */
|
|
21
|
+
const expandedFrames = ref<Set<string>>(new Set())
|
|
22
|
+
|
|
23
|
+
function toggleFrame(id: string) {
|
|
24
|
+
const next = new Set(expandedFrames.value)
|
|
25
|
+
if (next.has(id)) next.delete(id)
|
|
26
|
+
else next.add(id)
|
|
27
|
+
expandedFrames.value = next
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function expandFrame(id: string) {
|
|
31
|
+
if (expandedFrames.value.has(id)) return
|
|
32
|
+
expandedFrames.value = new Set(expandedFrames.value).add(id)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Services are always expanded to their task canvas, at every zoom level, so the
|
|
36
|
+
* board layout is fixed: panning never changes it and zooming has no expand/collapse
|
|
37
|
+
* transition to snap on. (`expandedFrames`/`toggleFrame` are retained for callers but
|
|
38
|
+
* no longer gate rendering.) */
|
|
39
|
+
function isFrameExpanded(_id: string) {
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function select(id: string | null) {
|
|
44
|
+
selectedBlockId.value = id
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function focus(id: string | null) {
|
|
48
|
+
focusBlockId.value = id
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
selectedBlockId,
|
|
53
|
+
focusBlockId,
|
|
54
|
+
zoom,
|
|
55
|
+
lod,
|
|
56
|
+
expandedFrames,
|
|
57
|
+
toggleFrame,
|
|
58
|
+
expandFrame,
|
|
59
|
+
isFrameExpanded,
|
|
60
|
+
select,
|
|
61
|
+
focus,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
import { useExecutionStore } from '~/stores/execution'
|
|
3
|
+
import { agentKindMeta } from '~/utils/catalog'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The step-inspection / result-view slice of the UI store: the dedicated result-view overlay
|
|
7
|
+
* (`resultView`, driven by the universal `dispatchStepView` seam), the generic step-detail
|
|
8
|
+
* panel (`stepDetail`), the LLM per-call observability panel, and the Kaizen screen — plus the
|
|
9
|
+
* open/close actions every board + inspector entry point uses. Split out of the modal + nav
|
|
10
|
+
* state per refactoring candidate #4; the `dispatchStepView`/`ui.resultView` seam is preserved
|
|
11
|
+
* intact, so adding a bespoke window for a new agent is still just declaring `resultView` +
|
|
12
|
+
* registering a component. Composed into {@link useUiStore} with the same public names.
|
|
13
|
+
*/
|
|
14
|
+
export function createUiResultViews() {
|
|
15
|
+
// Dedicated result-view overlay: a step whose agent kind declares a bespoke
|
|
16
|
+
// visualization (via the archetype's `resultView`) opens here instead of the generic
|
|
17
|
+
// prose step-detail panel. `view` is the registry id (e.g. 'requirements-review');
|
|
18
|
+
// `blockId` is always set; `instanceId`/`stepIndex` are present on the pipeline path and
|
|
19
|
+
// null for an off-path open (e.g. the inspector's pre-start requirements review).
|
|
20
|
+
const resultView = ref<{
|
|
21
|
+
view: string
|
|
22
|
+
blockId: string
|
|
23
|
+
instanceId: string | null
|
|
24
|
+
stepIndex: number | null
|
|
25
|
+
// The brainstorm dialogue stage, set only when `view === 'brainstorm'` (its two agent
|
|
26
|
+
// kinds share one window). Derived from the step's agent kind on the pipeline path, or
|
|
27
|
+
// passed explicitly on an off-path open.
|
|
28
|
+
stage?: 'requirements' | 'architecture'
|
|
29
|
+
} | null>(null)
|
|
30
|
+
|
|
31
|
+
// Agent step-detail overlay: which pipeline step (a run instance + step index)
|
|
32
|
+
// a human is inspecting, or null when closed. The overlay resolves the step
|
|
33
|
+
// from the execution store so it stays live; it shows the step's metadata
|
|
34
|
+
// (model, state, progress, subtasks, …) and — when the agent produced prose —
|
|
35
|
+
// a reader for it (ToC + collapsible sections).
|
|
36
|
+
const stepDetail = ref<{ instanceId: string; stepIndex: number } | null>(null)
|
|
37
|
+
|
|
38
|
+
// LLM observability panel: which run (execution instance) a human is inspecting
|
|
39
|
+
// the per-call model activity for, or null when closed. The panel loads the full
|
|
40
|
+
// per-call detail from the observability store on open.
|
|
41
|
+
const observabilityInstanceId = ref<string | null>(null)
|
|
42
|
+
|
|
43
|
+
// The Kaizen screen (grading history + verified-combo library), a full-panel overlay
|
|
44
|
+
// opened from the sidebar. Distinct from the per-run grading status shown in run details.
|
|
45
|
+
const kaizenScreenOpen = ref(false)
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Open a pending approval gate in the conclusions reader (approval mode). Resolves
|
|
49
|
+
* the step index from the gate id so every board/inspector entry point can keep
|
|
50
|
+
* passing the approval id it already has.
|
|
51
|
+
*/
|
|
52
|
+
function openApprovalDetail(instanceId: string, approvalId: string) {
|
|
53
|
+
const execution = useExecutionStore()
|
|
54
|
+
const instance = execution.getInstance(instanceId)
|
|
55
|
+
const idx = instance?.steps.findIndex((s) => s.approval?.id === approvalId) ?? -1
|
|
56
|
+
if (idx >= 0) dispatchStepView(instanceId, idx)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Open a pipeline step: route it to its agent kind's DEDICATED result window when the
|
|
61
|
+
* archetype declares one (the universal `resultView` seam), else the generic prose
|
|
62
|
+
* step-detail panel. This is the single dispatch every board/inspector entry point uses,
|
|
63
|
+
* so adding a bespoke window for a new agent is just declaring `resultView` + registering
|
|
64
|
+
* a component — no caller changes.
|
|
65
|
+
*/
|
|
66
|
+
function dispatchStepView(instanceId: string, stepIndex: number) {
|
|
67
|
+
const execution = useExecutionStore()
|
|
68
|
+
const instance = execution.getInstance(instanceId)
|
|
69
|
+
const step = instance?.steps[stepIndex]
|
|
70
|
+
// A step that actually ran the consensus mechanism opens the dedicated Consensus
|
|
71
|
+
// Session window, regardless of its kind's normal result view — consensus is an
|
|
72
|
+
// execution MODE on a kind, not a kind, so it can't be a static archetype `resultView`.
|
|
73
|
+
const view = step?.consensus?.enabled
|
|
74
|
+
? 'consensus-session'
|
|
75
|
+
: step
|
|
76
|
+
? agentKindMeta(step.agentKind).resultView
|
|
77
|
+
: undefined
|
|
78
|
+
if (view && instance) {
|
|
79
|
+
// The brainstorm window is shared by both stages; carry which one from the step's kind.
|
|
80
|
+
const stage =
|
|
81
|
+
view === 'brainstorm'
|
|
82
|
+
? step?.agentKind === 'architecture-brainstorm'
|
|
83
|
+
? 'architecture'
|
|
84
|
+
: 'requirements'
|
|
85
|
+
: undefined
|
|
86
|
+
resultView.value = {
|
|
87
|
+
view,
|
|
88
|
+
blockId: instance.blockId,
|
|
89
|
+
instanceId,
|
|
90
|
+
stepIndex,
|
|
91
|
+
...(stage ? { stage } : {}),
|
|
92
|
+
}
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
stepDetail.value = { instanceId, stepIndex }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function openRequirementReview(blockId: string) {
|
|
99
|
+
resultView.value = { view: 'requirements-review', blockId, instanceId: null, stepIndex: null }
|
|
100
|
+
}
|
|
101
|
+
function openClarityReview(blockId: string) {
|
|
102
|
+
resultView.value = { view: 'clarity-review', blockId, instanceId: null, stepIndex: null }
|
|
103
|
+
}
|
|
104
|
+
function openBrainstorm(blockId: string, stage: 'requirements' | 'architecture') {
|
|
105
|
+
resultView.value = { view: 'brainstorm', blockId, instanceId: null, stepIndex: null, stage }
|
|
106
|
+
}
|
|
107
|
+
// Open the service-spec window for a service frame (the inspector's "View Requirements").
|
|
108
|
+
function openServiceSpec(blockId: string) {
|
|
109
|
+
resultView.value = { view: 'service-spec', blockId, instanceId: null, stepIndex: null }
|
|
110
|
+
}
|
|
111
|
+
// Open the initiative tracker window for an initiative block (board card / inspector).
|
|
112
|
+
function openInitiativeTracker(blockId: string) {
|
|
113
|
+
resultView.value = { view: 'initiative-tracker', blockId, instanceId: null, stepIndex: null }
|
|
114
|
+
}
|
|
115
|
+
// Open the interactive-planning Q&A window for an initiative block (inspector / card,
|
|
116
|
+
// when the interviewer has parked the planning run with pending questions).
|
|
117
|
+
function openInitiativePlanning(blockId: string) {
|
|
118
|
+
resultView.value = { view: 'initiative-planning', blockId, instanceId: null, stepIndex: null }
|
|
119
|
+
}
|
|
120
|
+
// Open the Follow-up companion window for a run's Coder step (the blinking chip + the
|
|
121
|
+
// `followup_pending` notification). Resolves the Coder step index from the run when not
|
|
122
|
+
// given, so callers that only know the run can still open it.
|
|
123
|
+
function openFollowUps(instanceId: string, stepIndex: number | null = null) {
|
|
124
|
+
const execution = useExecutionStore()
|
|
125
|
+
const instance = execution.getInstance(instanceId)
|
|
126
|
+
if (!instance) return
|
|
127
|
+
// A pipeline may carry more than one follow-up-enabled Coder step, so don't blindly pick
|
|
128
|
+
// the first when no index is given: prefer the step that still has undecided items (the
|
|
129
|
+
// one the run is parked on), else the current step, else the first enabled one.
|
|
130
|
+
const resolveIdx = () => {
|
|
131
|
+
const pending = instance.steps.findIndex(
|
|
132
|
+
(s) => s.followUps?.enabled && s.followUps.items.some((i) => i.status === 'pending'),
|
|
133
|
+
)
|
|
134
|
+
if (pending >= 0) return pending
|
|
135
|
+
const current = instance.steps[instance.currentStep]
|
|
136
|
+
if (current?.followUps?.enabled) return instance.currentStep
|
|
137
|
+
return instance.steps.findIndex((s) => s.followUps?.enabled)
|
|
138
|
+
}
|
|
139
|
+
const idx = stepIndex ?? resolveIdx()
|
|
140
|
+
if (idx < 0) return
|
|
141
|
+
resultView.value = {
|
|
142
|
+
view: 'follow-ups',
|
|
143
|
+
blockId: instance.blockId,
|
|
144
|
+
instanceId,
|
|
145
|
+
stepIndex: idx,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Open the implementation-fork decision window for a run's coder step (from the inspector /
|
|
149
|
+
// pipeline chip / `fork_decision_pending` notification). Resolves the coder step index from
|
|
150
|
+
// the run when not given, preferring the step parked awaiting a choice.
|
|
151
|
+
function openForkDecision(instanceId: string, stepIndex: number | null = null) {
|
|
152
|
+
const execution = useExecutionStore()
|
|
153
|
+
const instance = execution.getInstance(instanceId)
|
|
154
|
+
if (!instance) return
|
|
155
|
+
const resolveIdx = () => {
|
|
156
|
+
const awaiting = instance.steps.findIndex(
|
|
157
|
+
(s) => s.agentKind === 'coder' && s.forkDecision?.status === 'awaiting_choice',
|
|
158
|
+
)
|
|
159
|
+
if (awaiting >= 0) return awaiting
|
|
160
|
+
const current = instance.steps[instance.currentStep]
|
|
161
|
+
if (current?.agentKind === 'coder' && current.forkDecision) return instance.currentStep
|
|
162
|
+
return instance.steps.findIndex((s) => s.agentKind === 'coder' && s.forkDecision)
|
|
163
|
+
}
|
|
164
|
+
const idx = stepIndex ?? resolveIdx()
|
|
165
|
+
if (idx < 0) return
|
|
166
|
+
resultView.value = {
|
|
167
|
+
view: 'fork-decision',
|
|
168
|
+
blockId: instance.blockId,
|
|
169
|
+
instanceId,
|
|
170
|
+
stepIndex: idx,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Open the PR deep-review window for a run's `pr-reviewer` step (from the `pr_review_ready`
|
|
174
|
+
// notification / the step). Resolves the step index from the run when not given, preferring
|
|
175
|
+
// the step parked awaiting a finding selection.
|
|
176
|
+
function openPrReview(instanceId: string, stepIndex: number | null = null) {
|
|
177
|
+
const execution = useExecutionStore()
|
|
178
|
+
const instance = execution.getInstance(instanceId)
|
|
179
|
+
if (!instance) return
|
|
180
|
+
const resolveIdx = () => {
|
|
181
|
+
const awaiting = instance.steps.findIndex(
|
|
182
|
+
(s) => s.agentKind === 'pr-reviewer' && s.prReview?.status === 'awaiting_selection',
|
|
183
|
+
)
|
|
184
|
+
if (awaiting >= 0) return awaiting
|
|
185
|
+
const current = instance.steps[instance.currentStep]
|
|
186
|
+
if (current?.agentKind === 'pr-reviewer' && current.prReview) return instance.currentStep
|
|
187
|
+
return instance.steps.findIndex((s) => s.agentKind === 'pr-reviewer' && s.prReview)
|
|
188
|
+
}
|
|
189
|
+
const idx = stepIndex ?? resolveIdx()
|
|
190
|
+
if (idx < 0) return
|
|
191
|
+
resultView.value = {
|
|
192
|
+
view: 'pr-review',
|
|
193
|
+
blockId: instance.blockId,
|
|
194
|
+
instanceId,
|
|
195
|
+
stepIndex: idx,
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function closeResultView() {
|
|
199
|
+
resultView.value = null
|
|
200
|
+
}
|
|
201
|
+
// Kept name for the requirements window's close handler.
|
|
202
|
+
const closeRequirementReview = closeResultView
|
|
203
|
+
function openStepDetail(instanceId: string, stepIndex: number) {
|
|
204
|
+
dispatchStepView(instanceId, stepIndex)
|
|
205
|
+
}
|
|
206
|
+
function closeStepDetail() {
|
|
207
|
+
stepDetail.value = null
|
|
208
|
+
}
|
|
209
|
+
function openObservability(instanceId: string) {
|
|
210
|
+
observabilityInstanceId.value = instanceId
|
|
211
|
+
}
|
|
212
|
+
function closeObservability() {
|
|
213
|
+
observabilityInstanceId.value = null
|
|
214
|
+
}
|
|
215
|
+
function openKaizen() {
|
|
216
|
+
kaizenScreenOpen.value = true
|
|
217
|
+
}
|
|
218
|
+
function closeKaizen() {
|
|
219
|
+
kaizenScreenOpen.value = false
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
resultView,
|
|
224
|
+
stepDetail,
|
|
225
|
+
observabilityInstanceId,
|
|
226
|
+
kaizenScreenOpen,
|
|
227
|
+
openApprovalDetail,
|
|
228
|
+
openRequirementReview,
|
|
229
|
+
openClarityReview,
|
|
230
|
+
openBrainstorm,
|
|
231
|
+
openServiceSpec,
|
|
232
|
+
openInitiativeTracker,
|
|
233
|
+
openInitiativePlanning,
|
|
234
|
+
openFollowUps,
|
|
235
|
+
openForkDecision,
|
|
236
|
+
openPrReview,
|
|
237
|
+
closeResultView,
|
|
238
|
+
closeRequirementReview,
|
|
239
|
+
openStepDetail,
|
|
240
|
+
closeStepDetail,
|
|
241
|
+
openObservability,
|
|
242
|
+
closeObservability,
|
|
243
|
+
openKaizen,
|
|
244
|
+
closeKaizen,
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { useUiStore } from '~/stores/ui'
|
|
3
|
+
import { createUiNavigation } from '~/stores/ui/navigation'
|
|
4
|
+
import { createUiResultViews } from '~/stores/ui/resultViews'
|
|
5
|
+
import { createUiModals } from '~/stores/ui/modals'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `ui.ts` is a thin facade composing three slices (`navigation` / `resultViews` / `modals`)
|
|
9
|
+
* behind ONE unchanged public surface (refactoring candidate #4). These tests pin that
|
|
10
|
+
* invariant so a future slice edit can't silently drop, shadow, or duplicate a key that a
|
|
11
|
+
* `useUiStore()` consumer depends on — the split must stay purely internal.
|
|
12
|
+
*/
|
|
13
|
+
describe('ui store — facade composes the slices with no surface drift', () => {
|
|
14
|
+
it('exposes exactly the union of the three slices, with no cross-slice key collisions', () => {
|
|
15
|
+
const nav = Object.keys(createUiNavigation())
|
|
16
|
+
const results = Object.keys(createUiResultViews())
|
|
17
|
+
const modals = Object.keys(createUiModals())
|
|
18
|
+
const sliceKeys = [...nav, ...results, ...modals]
|
|
19
|
+
|
|
20
|
+
// No two slices declare the same key — a collision would silently drop one on spread.
|
|
21
|
+
expect(new Set(sliceKeys).size).toBe(sliceKeys.length)
|
|
22
|
+
|
|
23
|
+
// The store surface is precisely the union of the slices — nothing added, nothing lost.
|
|
24
|
+
// (Filter out Pinia's own `$`/`_`-prefixed API, which no slice key uses.)
|
|
25
|
+
const storeKeys = Object.keys(useUiStore()).filter(
|
|
26
|
+
(k) => !k.startsWith('$') && !k.startsWith('_'),
|
|
27
|
+
)
|
|
28
|
+
expect(storeKeys.sort()).toEqual([...new Set(sliceKeys)].sort())
|
|
29
|
+
})
|
|
30
|
+
})
|