@cat-factory/app 0.144.1 → 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/observability/StepMetricsBar.vue +8 -1
- package/app/components/panels/ObservabilityPanel.vue +20 -3
- package/app/components/panels/inspector/TaskExecution.vue +23 -0
- package/app/components/pipeline/PipelineProgress.vue +33 -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/app/stores/ui/resultViews.ts +8 -3
- package/app/stores/ui.dispatch.spec.ts +99 -0
- package/app/utils/catalog.spec.ts +1 -0
- package/app/utils/catalog.ts +18 -0
- package/app/utils/observability.spec.ts +28 -0
- package/app/utils/observability.ts +28 -0
- package/i18n/locales/de.json +9 -4
- package/i18n/locales/en.json +9 -4
- package/i18n/locales/es.json +9 -4
- package/i18n/locales/fr.json +9 -4
- package/i18n/locales/he.json +9 -4
- package/i18n/locales/it.json +9 -4
- package/i18n/locales/ja.json +9 -4
- package/i18n/locales/pl.json +9 -4
- package/i18n/locales/tr.json +9 -4
- package/i18n/locales/uk.json +9 -4
- 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))
|
|
@@ -4,6 +4,7 @@ import type { StepMetrics } from '~/types/execution'
|
|
|
4
4
|
import {
|
|
5
5
|
formatMs,
|
|
6
6
|
formatTokens,
|
|
7
|
+
freshPromptTokens,
|
|
7
8
|
headroomColor,
|
|
8
9
|
headroomRatio,
|
|
9
10
|
pct,
|
|
@@ -22,6 +23,12 @@ defineEmits<{ inspect: [] }>()
|
|
|
22
23
|
const { t } = useI18n()
|
|
23
24
|
|
|
24
25
|
const m = computed(() => props.metrics)
|
|
26
|
+
// The headline "↑" is FRESH (uncached) input, not the raw prompt-token sum: a long agentic
|
|
27
|
+
// run re-sends its whole transcript every turn, so the raw sum is ~all cache reads and reads
|
|
28
|
+
// as a blow-up. The cached prefix is shown separately (the green chip) so the two are distinct.
|
|
29
|
+
const freshPrompt = computed(() =>
|
|
30
|
+
freshPromptTokens(m.value.promptTokens, m.value.cachedPromptTokens ?? 0),
|
|
31
|
+
)
|
|
25
32
|
const headroom = computed(() => headroomRatio(m.value))
|
|
26
33
|
const transport = computed(() => transportRatio(m.value))
|
|
27
34
|
const headroomTone = computed(() => headroomColor(headroom.value, m.value.truncatedCalls > 0))
|
|
@@ -48,7 +55,7 @@ const headroomTone = computed(() => headroomColor(headroom.value, m.value.trunca
|
|
|
48
55
|
class="tabular-nums text-slate-400"
|
|
49
56
|
:title="t('observability.metricsBar.promptCompletionTokens')"
|
|
50
57
|
>
|
|
51
|
-
{{ formatTokens(
|
|
58
|
+
{{ formatTokens(freshPrompt) }}↑ {{ formatTokens(m.completionTokens) }}↓
|
|
52
59
|
</span>
|
|
53
60
|
<span
|
|
54
61
|
v-if="(m.cachedPromptTokens ?? 0) > 0"
|
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
WebSearchProvider,
|
|
9
9
|
} from '~/types/execution'
|
|
10
10
|
import { agentKindMeta } from '~/utils/catalog'
|
|
11
|
-
import { formatMs, formatTokens, pct } from '~/utils/observability'
|
|
11
|
+
import { formatMs, formatTokens, freshPromptTokens, pct } from '~/utils/observability'
|
|
12
12
|
|
|
13
13
|
// Drill-down overlay for a run's LLM activity. Opened via
|
|
14
14
|
// `ui.openObservability(instanceId)` from a step surface; loads the full per-call
|
|
@@ -126,9 +126,15 @@ const totals = computed(() => {
|
|
|
126
126
|
const upstreamMs = sum(c, (x) => x.upstreamMs)
|
|
127
127
|
const overheadMs = sum(c, (x) => x.overheadMs)
|
|
128
128
|
const total = upstreamMs + overheadMs
|
|
129
|
+
const promptTokens = sum(c, (x) => x.promptTokens)
|
|
130
|
+
const cachedPromptTokens = sum(c, (x) => x.cachedPromptTokens)
|
|
129
131
|
return {
|
|
130
132
|
calls: c.length,
|
|
131
|
-
promptTokens
|
|
133
|
+
promptTokens,
|
|
134
|
+
cachedPromptTokens,
|
|
135
|
+
// Fresh (uncached) input — the raw prompt-token sum is dominated by per-turn cache reads
|
|
136
|
+
// on a long agentic run, so surface fresh vs cached rather than the misleading raw total.
|
|
137
|
+
freshPromptTokens: freshPromptTokens(promptTokens, cachedPromptTokens),
|
|
132
138
|
completionTokens: sum(c, (x) => x.completionTokens),
|
|
133
139
|
upstreamMs,
|
|
134
140
|
overheadMs,
|
|
@@ -284,8 +290,19 @@ function exportJson() {
|
|
|
284
290
|
{{ t('observability.summary.tokensInOut') }}
|
|
285
291
|
</dt>
|
|
286
292
|
<dd class="mt-0.5 tabular-nums text-slate-200">
|
|
287
|
-
{{ formatTokens(totals.
|
|
293
|
+
{{ formatTokens(totals.freshPromptTokens) }} /
|
|
288
294
|
{{ formatTokens(totals.completionTokens) }}
|
|
295
|
+
<span
|
|
296
|
+
v-if="totals.cachedPromptTokens > 0"
|
|
297
|
+
class="ms-1 text-[11px] text-emerald-400/80"
|
|
298
|
+
>
|
|
299
|
+
·
|
|
300
|
+
{{
|
|
301
|
+
t('observability.summary.cached', {
|
|
302
|
+
tokens: formatTokens(totals.cachedPromptTokens),
|
|
303
|
+
})
|
|
304
|
+
}}
|
|
305
|
+
</span>
|
|
289
306
|
</dd>
|
|
290
307
|
</div>
|
|
291
308
|
<div>
|
|
@@ -151,6 +151,12 @@ function openForkFor(i: number) {
|
|
|
151
151
|
if (instance.value) ui.openForkDecision(instance.value.id, i)
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
// Open the PR deep-review findings-selection window for a pr-reviewer step parked awaiting
|
|
155
|
+
// a selection (its dedicated chip, mirroring the fork-decision one above).
|
|
156
|
+
function openPrReviewFor(i: number) {
|
|
157
|
+
if (instance.value) ui.openPrReview(instance.value.id, i)
|
|
158
|
+
}
|
|
159
|
+
|
|
154
160
|
// Stop the run WITHOUT deleting it: halts the container + driver and records a
|
|
155
161
|
// `cancelled` failure, leaving the run readable + retryable (the block goes
|
|
156
162
|
// `blocked`). The destructive reset (delete the run, return the task to `planned`)
|
|
@@ -393,6 +399,23 @@ async function mergePr() {
|
|
|
393
399
|
>
|
|
394
400
|
{{ t('inspector.execution.chooseApproach') }}
|
|
395
401
|
</UButton>
|
|
402
|
+
<!-- A pr-reviewer step parked awaiting a finding selection: open the dedicated
|
|
403
|
+
findings-selection window, not the generic approval gate. -->
|
|
404
|
+
<UButton
|
|
405
|
+
v-else-if="
|
|
406
|
+
s.approval &&
|
|
407
|
+
s.approval.status === 'pending' &&
|
|
408
|
+
s.prReview?.status === 'awaiting_selection'
|
|
409
|
+
"
|
|
410
|
+
color="primary"
|
|
411
|
+
variant="soft"
|
|
412
|
+
size="xs"
|
|
413
|
+
icon="i-lucide-clipboard-check"
|
|
414
|
+
data-testid="pr-review-open"
|
|
415
|
+
@click="openPrReviewFor(i)"
|
|
416
|
+
>
|
|
417
|
+
{{ t('inspector.execution.reviewFindings') }}
|
|
418
|
+
</UButton>
|
|
396
419
|
<UButton
|
|
397
420
|
v-else-if="s.approval && s.approval.status === 'pending'"
|
|
398
421
|
color="warning"
|
|
@@ -75,6 +75,15 @@ function forkPhase(step: PipelineStep): 'proposing' | 'awaiting_choice' | null {
|
|
|
75
75
|
return status === 'proposing' || status === 'awaiting_choice' ? status : null
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Whether a `pr-reviewer` step is parked awaiting a human finding-selection — it carries a
|
|
80
|
+
* pending approval too, so we render a purpose-built "Review findings" chip (opening the
|
|
81
|
+
* PR-review window) ahead of the generic approval gate, mirroring the fork-decision chip.
|
|
82
|
+
*/
|
|
83
|
+
function prReviewAwaiting(step: PipelineStep): boolean {
|
|
84
|
+
return step.prReview?.status === 'awaiting_selection'
|
|
85
|
+
}
|
|
86
|
+
|
|
78
87
|
// --- restart from a step -----------------------------------------------------
|
|
79
88
|
// Re-run the pipeline from a chosen step onward: the server resets that step +
|
|
80
89
|
// every later step's iteration counters and re-drives a fresh run, keeping the
|
|
@@ -607,6 +616,26 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
607
616
|
</span>
|
|
608
617
|
</button>
|
|
609
618
|
|
|
619
|
+
<!-- PR deep-review parked for a human to select which findings matter: a
|
|
620
|
+
purpose-built chip opening the findings-selection window, ahead of the
|
|
621
|
+
generic approval gate (mirrors the fork-decision chip above). -->
|
|
622
|
+
<button
|
|
623
|
+
v-if="prReviewAwaiting(s)"
|
|
624
|
+
type="button"
|
|
625
|
+
data-testid="pr-review-open"
|
|
626
|
+
class="mt-3 flex w-full items-center gap-2 rounded-lg border border-dashed border-indigo-500/50 bg-indigo-500/10 px-2.5 py-1.5 text-start transition followup-blink hover:border-indigo-400/60"
|
|
627
|
+
@click="ui.openPrReview(instance.id, i)"
|
|
628
|
+
>
|
|
629
|
+
<span
|
|
630
|
+
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-md border border-indigo-500/40 bg-indigo-500/15"
|
|
631
|
+
>
|
|
632
|
+
<UIcon name="i-lucide-clipboard-check" class="h-3 w-3 text-indigo-300" />
|
|
633
|
+
</span>
|
|
634
|
+
<span class="min-w-0 flex-1 truncate text-[12px] text-slate-300">
|
|
635
|
+
{{ t('pipeline.progress.prReview.review') }}
|
|
636
|
+
</span>
|
|
637
|
+
</button>
|
|
638
|
+
|
|
610
639
|
<!-- reviewer gate folding/re-reviewing in the background: a working indicator,
|
|
611
640
|
NOT a "Review & approve" gate (the human is summoned only if needed) -->
|
|
612
641
|
<div
|
|
@@ -618,7 +647,10 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
618
647
|
</div>
|
|
619
648
|
|
|
620
649
|
<!-- approval gate: review (and edit) the proposal before continuing -->
|
|
621
|
-
<div
|
|
650
|
+
<div
|
|
651
|
+
v-else-if="s.approval && s.approval.status === 'pending' && !prReviewAwaiting(s)"
|
|
652
|
+
class="mt-3"
|
|
653
|
+
>
|
|
622
654
|
<UButton
|
|
623
655
|
color="warning"
|
|
624
656
|
variant="soft"
|
|
@@ -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
|
/**
|