@cat-factory/app 0.145.0 → 0.145.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/components/board/TaskDependencyEdges.vue +37 -73
- package/app/components/brainstorm/BrainstormWindow.vue +2 -7
- package/app/components/clarity/ClarityReviewWindow.vue +2 -2
- package/app/components/consensus/ConsensusSessionWindow.vue +2 -2
- package/app/components/docs/DocInterviewWindow.vue +1 -1
- package/app/components/forkDecision/ForkDecisionWindow.vue +1 -1
- package/app/components/initiative/InitiativePlanningWindow.vue +1 -1
- package/app/components/initiative/InitiativeTrackerWindow.vue +1 -1
- package/app/components/prReview/PrReviewWindow.vue +2 -2
- package/app/components/requirements/RequirementsReviewWindow.vue +2 -2
- package/app/components/spec/ServiceSpecWindow.vue +2 -2
- package/app/composables/useResultView.ts +26 -3
- package/app/stores/pipelines.ts +50 -19
- package/app/stores/ui/modals.ts +599 -532
- package/package.json +1 -1
|
@@ -108,88 +108,52 @@ function border(cx: number, cy: number, hw: number, hh: number, tx: number, ty:
|
|
|
108
108
|
return { x: cx + dx * t, y: cy + dy * t }
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/** Resolve the on-screen, origin-relative border-to-border segment between two blocks,
|
|
112
|
+
* or null when either end is missing or both collapsed into the same frame. */
|
|
113
|
+
function segmentBetween(sourceId: string, targetId: string, origin: DOMRect) {
|
|
114
|
+
const a = anchorEl(sourceId)
|
|
115
|
+
const b = anchorEl(targetId)
|
|
116
|
+
if (!a || !b || a === b) return null // missing, or both collapsed into the same frame
|
|
117
|
+
const ra = a.getBoundingClientRect()
|
|
118
|
+
const rb = b.getBoundingClientRect()
|
|
119
|
+
const ax = ra.left + ra.width / 2 - origin.left
|
|
120
|
+
const ay = ra.top + ra.height / 2 - origin.top
|
|
121
|
+
const bx = rb.left + rb.width / 2 - origin.left
|
|
122
|
+
const by = rb.top + rb.height / 2 - origin.top
|
|
123
|
+
const start = border(ax, ay, ra.width / 2, ra.height / 2, bx, by)
|
|
124
|
+
const end = border(bx, by, rb.width / 2, rb.height / 2, ax, ay)
|
|
125
|
+
return { x1: start.x, y1: start.y, x2: end.x, y2: end.y }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Map a list of {id, source, target} links to their drawable (arrowhead-agnostic) segments. */
|
|
129
|
+
function linkSegments(
|
|
130
|
+
links: { id: string; source: string; target: string }[],
|
|
131
|
+
origin: DOMRect,
|
|
132
|
+
): { id: string; x1: number; y1: number; x2: number; y2: number }[] {
|
|
133
|
+
const out: { id: string; x1: number; y1: number; x2: number; y2: number }[] = []
|
|
134
|
+
for (const link of links) {
|
|
135
|
+
const seg = segmentBetween(link.source, link.target, origin)
|
|
136
|
+
if (seg) out.push({ id: link.id, ...seg })
|
|
137
|
+
}
|
|
138
|
+
return out
|
|
139
|
+
}
|
|
140
|
+
|
|
111
141
|
function recompute() {
|
|
112
142
|
const el = svg.value
|
|
113
143
|
if (!el) return
|
|
114
144
|
const origin = el.getBoundingClientRect()
|
|
115
|
-
const next: Seg[] = []
|
|
116
145
|
|
|
146
|
+
const next: Seg[] = []
|
|
117
147
|
for (const d of taskDeps.value) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const ra = a.getBoundingClientRect()
|
|
123
|
-
const rb = b.getBoundingClientRect()
|
|
124
|
-
const ax = ra.left + ra.width / 2 - origin.left
|
|
125
|
-
const ay = ra.top + ra.height / 2 - origin.top
|
|
126
|
-
const bx = rb.left + rb.width / 2 - origin.left
|
|
127
|
-
const by = rb.top + rb.height / 2 - origin.top
|
|
128
|
-
|
|
129
|
-
const start = border(ax, ay, ra.width / 2, ra.height / 2, bx, by)
|
|
130
|
-
const end = border(bx, by, rb.width / 2, rb.height / 2, ax, ay)
|
|
131
|
-
|
|
132
|
-
next.push({
|
|
133
|
-
id: d.id,
|
|
134
|
-
x1: start.x,
|
|
135
|
-
y1: start.y,
|
|
136
|
-
x2: end.x,
|
|
137
|
-
y2: end.y,
|
|
138
|
-
done: board.getBlock(d.source)?.status === 'done',
|
|
139
|
-
})
|
|
148
|
+
const seg = segmentBetween(d.source, d.target, origin)
|
|
149
|
+
if (!seg) continue
|
|
150
|
+
next.push({ id: d.id, ...seg, done: board.getBlock(d.source)?.status === 'done' })
|
|
140
151
|
}
|
|
141
152
|
segments.value = next
|
|
142
153
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const b = anchorEl(link.target)
|
|
147
|
-
if (!a || !b || a === b) continue
|
|
148
|
-
const ra = a.getBoundingClientRect()
|
|
149
|
-
const rb = b.getBoundingClientRect()
|
|
150
|
-
const ax = ra.left + ra.width / 2 - origin.left
|
|
151
|
-
const ay = ra.top + ra.height / 2 - origin.top
|
|
152
|
-
const bx = rb.left + rb.width / 2 - origin.left
|
|
153
|
-
const by = rb.top + rb.height / 2 - origin.top
|
|
154
|
-
const start = border(ax, ay, ra.width / 2, ra.height / 2, bx, by)
|
|
155
|
-
const end = border(bx, by, rb.width / 2, rb.height / 2, ax, ay)
|
|
156
|
-
members.push({ id: link.id, x1: start.x, y1: start.y, x2: end.x, y2: end.y })
|
|
157
|
-
}
|
|
158
|
-
memberSegments.value = members
|
|
159
|
-
|
|
160
|
-
const fes: FrontendSeg[] = []
|
|
161
|
-
for (const link of frontendLinks.value) {
|
|
162
|
-
const a = anchorEl(link.source)
|
|
163
|
-
const b = anchorEl(link.target)
|
|
164
|
-
if (!a || !b || a === b) continue
|
|
165
|
-
const ra = a.getBoundingClientRect()
|
|
166
|
-
const rb = b.getBoundingClientRect()
|
|
167
|
-
const ax = ra.left + ra.width / 2 - origin.left
|
|
168
|
-
const ay = ra.top + ra.height / 2 - origin.top
|
|
169
|
-
const bx = rb.left + rb.width / 2 - origin.left
|
|
170
|
-
const by = rb.top + rb.height / 2 - origin.top
|
|
171
|
-
const start = border(ax, ay, ra.width / 2, ra.height / 2, bx, by)
|
|
172
|
-
const end = border(bx, by, rb.width / 2, rb.height / 2, ax, ay)
|
|
173
|
-
fes.push({ id: link.id, x1: start.x, y1: start.y, x2: end.x, y2: end.y })
|
|
174
|
-
}
|
|
175
|
-
frontendSegments.value = fes
|
|
176
|
-
|
|
177
|
-
const conns: ConnectionSeg[] = []
|
|
178
|
-
for (const link of connectionLinks.value) {
|
|
179
|
-
const a = anchorEl(link.source)
|
|
180
|
-
const b = anchorEl(link.target)
|
|
181
|
-
if (!a || !b || a === b) continue
|
|
182
|
-
const ra = a.getBoundingClientRect()
|
|
183
|
-
const rb = b.getBoundingClientRect()
|
|
184
|
-
const ax = ra.left + ra.width / 2 - origin.left
|
|
185
|
-
const ay = ra.top + ra.height / 2 - origin.top
|
|
186
|
-
const bx = rb.left + rb.width / 2 - origin.left
|
|
187
|
-
const by = rb.top + rb.height / 2 - origin.top
|
|
188
|
-
const start = border(ax, ay, ra.width / 2, ra.height / 2, bx, by)
|
|
189
|
-
const end = border(bx, by, rb.width / 2, rb.height / 2, ax, ay)
|
|
190
|
-
conns.push({ id: link.id, x1: start.x, y1: start.y, x2: end.x, y2: end.y })
|
|
191
|
-
}
|
|
192
|
-
connectionSegments.value = conns
|
|
154
|
+
memberSegments.value = linkSegments(epicLinks.value, origin)
|
|
155
|
+
frontendSegments.value = linkSegments(frontendLinks.value, origin)
|
|
156
|
+
connectionSegments.value = linkSegments(connectionLinks.value, origin)
|
|
193
157
|
}
|
|
194
158
|
|
|
195
159
|
const { pause, resume } = useRafFn(recompute, { immediate: false })
|
|
@@ -22,7 +22,6 @@ import type {
|
|
|
22
22
|
|
|
23
23
|
const board = useBoardStore()
|
|
24
24
|
const brainstorm = useBrainstormStore()
|
|
25
|
-
const ui = useUiStore()
|
|
26
25
|
const toast = useToast()
|
|
27
26
|
const { t } = useI18n()
|
|
28
27
|
const access = useWorkspaceAccess()
|
|
@@ -32,15 +31,11 @@ const redoComment = ref('')
|
|
|
32
31
|
const showRedo = ref(false)
|
|
33
32
|
|
|
34
33
|
const { open, blockId, stage, close } = useResultView('brainstorm', {
|
|
35
|
-
|
|
36
|
-
// const below is initialised — so read the stage straight off the store here (referencing
|
|
37
|
-
// `stage.value` would hit its temporal dead zone and throw on every open).
|
|
38
|
-
onOpen: (id) => {
|
|
34
|
+
onOpen: ({ blockId, stage }) => {
|
|
39
35
|
drafts.value = {}
|
|
40
36
|
redoComment.value = ''
|
|
41
37
|
showRedo.value = false
|
|
42
|
-
|
|
43
|
-
if (openStage) void brainstorm.load(id, openStage)
|
|
38
|
+
if (stage) void brainstorm.load(blockId, stage)
|
|
44
39
|
},
|
|
45
40
|
})
|
|
46
41
|
const activeStage = computed<BrainstormStage>(() => stage.value ?? 'requirements')
|
|
@@ -43,12 +43,12 @@ const showRedo = ref(false)
|
|
|
43
43
|
// fresh each open, so a non-immediate per-window watch used to leave it empty for whichever
|
|
44
44
|
// route (a pipeline step / "Review & approve") didn't warm the cache by selecting the block.
|
|
45
45
|
const { open, blockId, close } = useResultView('clarity-review', {
|
|
46
|
-
onOpen: (
|
|
46
|
+
onOpen: ({ blockId }) => {
|
|
47
47
|
drafts.value = {}
|
|
48
48
|
seededReply.value = {}
|
|
49
49
|
redoComment.value = ''
|
|
50
50
|
showRedo.value = false
|
|
51
|
-
void clarity.load(
|
|
51
|
+
void clarity.load(blockId)
|
|
52
52
|
},
|
|
53
53
|
// Flush any typed-but-unblurred answer before the view tears down (X, backdrop, Escape) so
|
|
54
54
|
// closing the window never silently drops it (UX-33).
|
|
@@ -20,8 +20,8 @@ const consensus = useConsensusStore()
|
|
|
20
20
|
const models = useModelsStore()
|
|
21
21
|
|
|
22
22
|
const { open, blockId, close } = useResultView('consensus-session', {
|
|
23
|
-
onOpen: (
|
|
24
|
-
void consensus.load(
|
|
23
|
+
onOpen: ({ blockId }) => {
|
|
24
|
+
void consensus.load(blockId)
|
|
25
25
|
},
|
|
26
26
|
})
|
|
27
27
|
|
|
@@ -16,7 +16,7 @@ const { t } = useI18n()
|
|
|
16
16
|
const access = useWorkspaceAccess()
|
|
17
17
|
|
|
18
18
|
const { open, blockId, close } = useResultView('doc-interview', {
|
|
19
|
-
onOpen: (
|
|
19
|
+
onOpen: ({ blockId }) => void docInterview.load(blockId),
|
|
20
20
|
})
|
|
21
21
|
|
|
22
22
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
@@ -26,7 +26,7 @@ const { t } = useI18n()
|
|
|
26
26
|
// Hybrid: state rides the coder step (like follow-ups), but warm it from the GET on open too.
|
|
27
27
|
// No `stepRef`: this is a pre-run decision, so there's no "restart from here".
|
|
28
28
|
const { open, blockId, instanceId, stepIndex, close } = useResultView('fork-decision', {
|
|
29
|
-
onOpen: (
|
|
29
|
+
onOpen: ({ blockId }) => void forkDecision.load(blockId),
|
|
30
30
|
})
|
|
31
31
|
|
|
32
32
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
@@ -17,7 +17,7 @@ const initiatives = useInitiativesStore()
|
|
|
17
17
|
const { t } = useI18n()
|
|
18
18
|
|
|
19
19
|
const { open, blockId, close } = useResultView('initiative-planning', {
|
|
20
|
-
onOpen: (
|
|
20
|
+
onOpen: ({ blockId }) => void initiatives.load(blockId),
|
|
21
21
|
})
|
|
22
22
|
|
|
23
23
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
@@ -26,7 +26,7 @@ const { t } = useI18n()
|
|
|
26
26
|
const toast = useToast()
|
|
27
27
|
|
|
28
28
|
const { open, blockId, close } = useResultView('initiative-tracker', {
|
|
29
|
-
onOpen: (
|
|
29
|
+
onOpen: ({ blockId }) => void initiatives.load(blockId),
|
|
30
30
|
})
|
|
31
31
|
|
|
32
32
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
@@ -30,8 +30,8 @@ const access = useWorkspaceAccess()
|
|
|
30
30
|
const { t } = useI18n()
|
|
31
31
|
|
|
32
32
|
const { open, blockId, instanceId, stepIndex, close } = useResultView('pr-review', {
|
|
33
|
-
onOpen: (
|
|
34
|
-
if (instanceId
|
|
33
|
+
onOpen: ({ instanceId }) => {
|
|
34
|
+
if (instanceId) void prReview.load(instanceId)
|
|
35
35
|
},
|
|
36
36
|
})
|
|
37
37
|
|
|
@@ -55,7 +55,7 @@ const docCollapsedOverride = ref<boolean | null>(null)
|
|
|
55
55
|
// fresh each open, so a non-immediate per-window watch used to leave it empty for whichever
|
|
56
56
|
// route (a pipeline step / "Review & approve") didn't warm the cache by selecting the block.
|
|
57
57
|
const { open, blockId, instanceId, stepIndex, close } = useResultView('requirements-review', {
|
|
58
|
-
onOpen: (
|
|
58
|
+
onOpen: ({ blockId }) => {
|
|
59
59
|
drafts.value = {}
|
|
60
60
|
seededReply.value = {}
|
|
61
61
|
recommendMode.value = new Set()
|
|
@@ -64,7 +64,7 @@ const { open, blockId, instanceId, stepIndex, close } = useResultView('requireme
|
|
|
64
64
|
redoComment.value = ''
|
|
65
65
|
showRedo.value = false
|
|
66
66
|
docCollapsedOverride.value = null
|
|
67
|
-
void requirements.load(
|
|
67
|
+
void requirements.load(blockId)
|
|
68
68
|
},
|
|
69
69
|
// Closing the window (X, backdrop, Escape) must not silently drop an answer the user typed
|
|
70
70
|
// but never blurred out of. Flush before the view tears down; flushDrafts captures the
|
|
@@ -25,10 +25,10 @@ const mode = ref<ViewMode>('structured')
|
|
|
25
25
|
const selected = ref<{ m: number; g: number } | null>(null)
|
|
26
26
|
|
|
27
27
|
const { open, blockId, close } = useResultView('service-spec', {
|
|
28
|
-
onOpen: (
|
|
28
|
+
onOpen: ({ blockId }) => {
|
|
29
29
|
mode.value = 'structured'
|
|
30
30
|
selected.value = null
|
|
31
|
-
void serviceSpec.load(
|
|
31
|
+
void serviceSpec.load(blockId)
|
|
32
32
|
},
|
|
33
33
|
})
|
|
34
34
|
|
|
@@ -24,9 +24,24 @@
|
|
|
24
24
|
* overlay stack (top overlay closes first, focus/scroll managed too). A listener here would
|
|
25
25
|
* double-fire `close`, so it was removed once the last window converted onto the shell.
|
|
26
26
|
*/
|
|
27
|
+
/**
|
|
28
|
+
* The fully-resolved view context handed to `onOpen`. Every field is already initialised by
|
|
29
|
+
* the time `onOpen` fires, so a loader takes exactly what it needs from here and never reaches
|
|
30
|
+
* back into the store or the composable's own return refs. That matters because `onOpen` fires
|
|
31
|
+
* synchronously from the `immediate` watch below — DURING the caller's `setup`, before the
|
|
32
|
+
* `const { … } = useResultView(…)` destructure has been assigned — so any callback that closed
|
|
33
|
+
* over those refs would hit their temporal dead zone and throw on every open.
|
|
34
|
+
*/
|
|
35
|
+
export interface OpenResultView {
|
|
36
|
+
blockId: string
|
|
37
|
+
instanceId: string | null
|
|
38
|
+
stepIndex: number | null
|
|
39
|
+
stage: 'requirements' | 'architecture' | null
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
export function useResultView(
|
|
28
43
|
viewId: string,
|
|
29
|
-
opts?: { onOpen?: (
|
|
44
|
+
opts?: { onOpen?: (view: OpenResultView) => void; onClose?: () => void },
|
|
30
45
|
) {
|
|
31
46
|
const ui = useUiStore()
|
|
32
47
|
|
|
@@ -44,12 +59,20 @@ export function useResultView(
|
|
|
44
59
|
ui.closeResultView()
|
|
45
60
|
}
|
|
46
61
|
|
|
47
|
-
// The load-on-open contract: fire immediately on mount and on any later block switch.
|
|
62
|
+
// The load-on-open contract: fire immediately on mount and on any later block switch. The
|
|
63
|
+
// callback receives the fully-resolved context (see OpenResultView) rather than the return
|
|
64
|
+
// refs, which aren't assigned yet at the initial synchronous fire.
|
|
48
65
|
if (opts?.onOpen) {
|
|
49
66
|
watch(
|
|
50
67
|
blockId,
|
|
51
68
|
(id) => {
|
|
52
|
-
if (id)
|
|
69
|
+
if (id)
|
|
70
|
+
opts.onOpen!({
|
|
71
|
+
blockId: id,
|
|
72
|
+
instanceId: instanceId.value,
|
|
73
|
+
stepIndex: stepIndex.value,
|
|
74
|
+
stage: stage.value,
|
|
75
|
+
})
|
|
53
76
|
},
|
|
54
77
|
{ immediate: true },
|
|
55
78
|
)
|
package/app/stores/pipelines.ts
CHANGED
|
@@ -24,26 +24,11 @@ function defaultConsensusConfig(): ConsensusStepConfig {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* custom pipeline can be loaded into it (`loadForEdit`) and saved back in place,
|
|
31
|
-
* while a built-in is cloned first (`clonePipeline`) into an editable copy.
|
|
27
|
+
* The per-step, index-aligned draft arrays. Every one is kept the same length as `draft` and
|
|
28
|
+
* spliced/reordered in lockstep (see `insertAt` / `removeFromDraft` / `moveUnit`), so grouping
|
|
29
|
+
* their construction keeps that alignment contract — and its documentation — in one place.
|
|
32
30
|
*/
|
|
33
|
-
|
|
34
|
-
const api = useApi()
|
|
35
|
-
const {
|
|
36
|
-
items: pipelines,
|
|
37
|
-
upsert: upsertPipeline,
|
|
38
|
-
remove: dropPipeline,
|
|
39
|
-
} = useUpsertList<Pipeline>({ key: (p) => p.id })
|
|
40
|
-
/**
|
|
41
|
-
* Current built-in catalog versions (`seedPipelines()`), keyed by pipeline id, from the
|
|
42
|
-
* workspace snapshot. A built-in whose stored `version` is below its catalog value here has
|
|
43
|
-
* a newer definition available (see `usePipelineHealth`).
|
|
44
|
-
*/
|
|
45
|
-
const catalogVersions = ref<Record<string, number>>({})
|
|
46
|
-
|
|
31
|
+
function createDraftStepState() {
|
|
47
32
|
/** The chain currently being assembled in the builder. */
|
|
48
33
|
const draft = ref<AgentKind[]>([])
|
|
49
34
|
/** Per-step approval gates, kept index-aligned with `draft`. */
|
|
@@ -79,6 +64,52 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
79
64
|
* default, so an entry exists only when a step opts out of something.
|
|
80
65
|
*/
|
|
81
66
|
const draftStepOptions = ref<(StepOptions | null)[]>([])
|
|
67
|
+
return {
|
|
68
|
+
draft,
|
|
69
|
+
draftGates,
|
|
70
|
+
draftEnabled,
|
|
71
|
+
draftThresholds,
|
|
72
|
+
draftConsensus,
|
|
73
|
+
draftGating,
|
|
74
|
+
draftFollowUps,
|
|
75
|
+
draftTesterQuality,
|
|
76
|
+
draftStepOptions,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Saved, reusable pipelines (the pipeline palette) plus the in-progress draft
|
|
82
|
+
* being assembled in the pipeline builder. Saved pipelines live on the backend;
|
|
83
|
+
* the draft is transient client state. The draft doubles as the EDIT surface: a
|
|
84
|
+
* custom pipeline can be loaded into it (`loadForEdit`) and saved back in place,
|
|
85
|
+
* while a built-in is cloned first (`clonePipeline`) into an editable copy.
|
|
86
|
+
*/
|
|
87
|
+
export const usePipelinesStore = defineStore('pipelines', () => {
|
|
88
|
+
const api = useApi()
|
|
89
|
+
const {
|
|
90
|
+
items: pipelines,
|
|
91
|
+
upsert: upsertPipeline,
|
|
92
|
+
remove: dropPipeline,
|
|
93
|
+
} = useUpsertList<Pipeline>({ key: (p) => p.id })
|
|
94
|
+
/**
|
|
95
|
+
* Current built-in catalog versions (`seedPipelines()`), keyed by pipeline id, from the
|
|
96
|
+
* workspace snapshot. A built-in whose stored `version` is below its catalog value here has
|
|
97
|
+
* a newer definition available (see `usePipelineHealth`).
|
|
98
|
+
*/
|
|
99
|
+
const catalogVersions = ref<Record<string, number>>({})
|
|
100
|
+
|
|
101
|
+
// The per-step, index-aligned draft arrays (kept in lockstep — see `createDraftStepState`).
|
|
102
|
+
const {
|
|
103
|
+
draft,
|
|
104
|
+
draftGates,
|
|
105
|
+
draftEnabled,
|
|
106
|
+
draftThresholds,
|
|
107
|
+
draftConsensus,
|
|
108
|
+
draftGating,
|
|
109
|
+
draftFollowUps,
|
|
110
|
+
draftTesterQuality,
|
|
111
|
+
draftStepOptions,
|
|
112
|
+
} = createDraftStepState()
|
|
82
113
|
/** Organizational labels for the pipeline being assembled/edited. */
|
|
83
114
|
const draftLabels = ref<string[]>([])
|
|
84
115
|
/**
|