@cat-factory/app 0.28.0 → 0.28.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.
|
@@ -214,10 +214,24 @@ async function setStatus(item: RequirementReviewItem, itemStatus: ReviewItemStat
|
|
|
214
214
|
const recommending = computed(() =>
|
|
215
215
|
blockId.value ? requirements.isRecommending(blockId.value) : false,
|
|
216
216
|
)
|
|
217
|
-
// Recommendations still
|
|
218
|
-
const
|
|
217
|
+
// Recommendations the Writer has produced that still await a human decision (`ready`).
|
|
218
|
+
const readyRecommendations = computed<RequirementRecommendation[]>(() =>
|
|
219
219
|
(review.value?.recommendations ?? []).filter((r) => r.status === 'ready'),
|
|
220
220
|
)
|
|
221
|
+
// Placeholders the Requirement Writer is still producing in the background (`pending`).
|
|
222
|
+
const generatingRecommendations = computed<RequirementRecommendation[]>(() =>
|
|
223
|
+
(review.value?.recommendations ?? []).filter((r) => r.status === 'pending'),
|
|
224
|
+
)
|
|
225
|
+
// "ready / total" progress for the in-flight batch (null when nothing is generating). Scoped to
|
|
226
|
+
// the current wave via `createdAt` (all placeholders in one request share the timestamp), so
|
|
227
|
+
// stale `ready` recommendations the human hasn't acted on from an earlier batch don't inflate it.
|
|
228
|
+
const recommendationProgress = computed(() => {
|
|
229
|
+
const generating = generatingRecommendations.value
|
|
230
|
+
if (generating.length === 0) return null
|
|
231
|
+
const batchTimes = new Set(generating.map((r) => r.createdAt))
|
|
232
|
+
const ready = readyRecommendations.value.filter((r) => batchTimes.has(r.createdAt)).length
|
|
233
|
+
return { ready, total: ready + generating.length }
|
|
234
|
+
})
|
|
221
235
|
function isMarkedForRecommend(item: RequirementReviewItem): boolean {
|
|
222
236
|
return markedForRecommend.value.has(item.id)
|
|
223
237
|
}
|
|
@@ -228,18 +242,37 @@ function toggleRecommend(item: RequirementReviewItem) {
|
|
|
228
242
|
markedForRecommend.value = next
|
|
229
243
|
}
|
|
230
244
|
|
|
231
|
-
// Fire the Writer over the whole marked batch
|
|
232
|
-
//
|
|
245
|
+
// Fire the Writer over the whole marked batch (grounded on the project's best-practice
|
|
246
|
+
// standards, specs/tech-specs and web search). ASYNCHRONOUS: it returns at once with `pending`
|
|
247
|
+
// placeholders that fill in live; the user can close the window and is notified when the batch
|
|
248
|
+
// is ready. Flush any typed-but-unblurred answers first so nothing the human entered is lost.
|
|
233
249
|
async function requestRecommendations() {
|
|
234
250
|
if (!blockId.value || markedForRecommend.value.size === 0) return
|
|
235
251
|
const ids = [...markedForRecommend.value]
|
|
236
252
|
try {
|
|
237
|
-
await
|
|
253
|
+
await flushDrafts()
|
|
254
|
+
const updated = await requirements.requestRecommendations(blockId.value, ids)
|
|
238
255
|
markedForRecommend.value = new Set()
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
256
|
+
const n = ids.length
|
|
257
|
+
const plural = n === 1 ? '' : 's'
|
|
258
|
+
// On a parked run the request returns at once with `pending` placeholders the durable driver
|
|
259
|
+
// fills in the background; off-path (no active pipeline) there is no driver, so the Writer
|
|
260
|
+
// ran inline and the recommendations are already settled. Tell the human which actually
|
|
261
|
+
// happened rather than always promising a background callback.
|
|
262
|
+
const stillGenerating = (updated?.recommendations ?? []).some((r) => r.status === 'pending')
|
|
263
|
+
toast.add(
|
|
264
|
+
stillGenerating
|
|
265
|
+
? {
|
|
266
|
+
title: `Preparing ${n} recommendation${plural} in the background`,
|
|
267
|
+
description:
|
|
268
|
+
"Your answers are saved — close this if you like; we'll notify you when they're ready.",
|
|
269
|
+
icon: 'i-lucide-sparkles',
|
|
270
|
+
}
|
|
271
|
+
: {
|
|
272
|
+
title: `${n} recommendation${plural} ready`,
|
|
273
|
+
icon: 'i-lucide-sparkles',
|
|
274
|
+
},
|
|
275
|
+
)
|
|
243
276
|
} catch (e) {
|
|
244
277
|
notifyError('Could not request recommendations', e)
|
|
245
278
|
}
|
|
@@ -567,18 +600,47 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
567
600
|
</div>
|
|
568
601
|
</div>
|
|
569
602
|
|
|
570
|
-
<!-- Requirement-Writer recommendations awaiting a human decision
|
|
603
|
+
<!-- Requirement-Writer recommendations: awaiting a human decision (`ready`) and/or
|
|
604
|
+
still generating in the background (`pending`) -->
|
|
571
605
|
<section
|
|
572
|
-
v-if="
|
|
606
|
+
v-if="readyRecommendations.length || generatingRecommendations.length"
|
|
573
607
|
class="mt-6 border-t border-slate-800 pt-5"
|
|
574
608
|
>
|
|
575
|
-
<div class="mb-3 flex items-center gap-
|
|
609
|
+
<div class="mb-3 flex items-center gap-2 text-[11px] text-indigo-300">
|
|
576
610
|
<UIcon name="i-lucide-wand-2" class="h-3.5 w-3.5" />
|
|
577
611
|
<span class="font-semibold uppercase tracking-wide">Recommended answers</span>
|
|
612
|
+
<span
|
|
613
|
+
v-if="recommendationProgress"
|
|
614
|
+
class="ml-auto flex items-center gap-1.5 normal-case text-indigo-300/80"
|
|
615
|
+
>
|
|
616
|
+
<UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 animate-spin" />
|
|
617
|
+
{{ recommendationProgress.ready }} / {{ recommendationProgress.total }} ready
|
|
618
|
+
</span>
|
|
578
619
|
</div>
|
|
620
|
+
|
|
621
|
+
<!-- still-generating placeholders (one per requested finding) -->
|
|
622
|
+
<div v-if="generatingRecommendations.length" class="mb-3 flex flex-col gap-3">
|
|
623
|
+
<div
|
|
624
|
+
v-for="rec in generatingRecommendations"
|
|
625
|
+
:key="rec.id"
|
|
626
|
+
class="flex items-start gap-2 rounded-lg border border-dashed border-indigo-900/50 bg-indigo-950/10 p-3"
|
|
627
|
+
>
|
|
628
|
+
<UIcon
|
|
629
|
+
name="i-lucide-loader-circle"
|
|
630
|
+
class="mt-0.5 h-4 w-4 shrink-0 animate-spin text-indigo-300"
|
|
631
|
+
/>
|
|
632
|
+
<div class="min-w-0">
|
|
633
|
+
<span class="text-sm font-medium text-white">{{
|
|
634
|
+
rec.sourceFinding.title
|
|
635
|
+
}}</span>
|
|
636
|
+
<p class="text-xs text-indigo-300/70">Generating a grounded suggestion…</p>
|
|
637
|
+
</div>
|
|
638
|
+
</div>
|
|
639
|
+
</div>
|
|
640
|
+
|
|
579
641
|
<div class="flex flex-col gap-3">
|
|
580
642
|
<div
|
|
581
|
-
v-for="rec in
|
|
643
|
+
v-for="rec in readyRecommendations"
|
|
582
644
|
:key="rec.id"
|
|
583
645
|
class="rounded-lg border border-indigo-900/50 bg-indigo-950/20 p-3"
|
|
584
646
|
>
|
|
@@ -83,11 +83,17 @@ export function reviewsApi({ http, ws }: ApiContext) {
|
|
|
83
83
|
),
|
|
84
84
|
|
|
85
85
|
// Ask the Requirement Writer to recommend grounded answers for a batch of findings (by
|
|
86
|
-
// item id). Returns the review with `
|
|
87
|
-
|
|
86
|
+
// item id). Returns the review with `pending` placeholder recommendations; they fill in
|
|
87
|
+
// (`ready`) asynchronously via the `requirements` stream as the Writer produces each.
|
|
88
|
+
requestRecommendations: (
|
|
89
|
+
workspaceId: string,
|
|
90
|
+
blockId: string,
|
|
91
|
+
itemIds: string[],
|
|
92
|
+
note?: string,
|
|
93
|
+
) =>
|
|
88
94
|
http<RequirementReview | null>(
|
|
89
95
|
`${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/requirement-review/recommend`,
|
|
90
|
-
{ method: 'POST', body: { itemIds } },
|
|
96
|
+
{ method: 'POST', body: { itemIds, ...(note ? { note } : {}) } },
|
|
91
97
|
),
|
|
92
98
|
|
|
93
99
|
// Accept a recommendation (becomes the finding's answer), reject it, or re-request it
|
|
@@ -44,14 +44,21 @@ export const useRequirementsStore = defineStore('requirements', () => {
|
|
|
44
44
|
function reviewFor(blockId: string): RequirementReview | null {
|
|
45
45
|
return reviews.value[blockId] ?? null
|
|
46
46
|
}
|
|
47
|
+
/** Whether the Requirement Writer is still producing recommendations for a block (a `pending`
|
|
48
|
+
* placeholder exists). Server-derived, so the "Recommending…" state survives the window closing
|
|
49
|
+
* and a page reload — the client-local `recommending` set only covers the request round-trip. */
|
|
50
|
+
function hasPendingRecommendations(blockId: string): boolean {
|
|
51
|
+
return (reviews.value[blockId]?.recommendations ?? []).some((r) => r.status === 'pending')
|
|
52
|
+
}
|
|
47
53
|
/**
|
|
48
54
|
* The async background stage a block's review is in, or null. While the driver folds the
|
|
49
|
-
* answers (`incorporating`) then re-reviews the document (`reviewing`),
|
|
50
|
-
*
|
|
51
|
-
*
|
|
55
|
+
* answers (`incorporating`) then re-reviews the document (`reviewing`), or the Requirement
|
|
56
|
+
* Writer is producing recommendations (`recommending`), NO human action is needed — so the
|
|
57
|
+
* board suppresses the "Approval needed" gate and shows this working state instead, with copy
|
|
58
|
+
* that names which stage is running.
|
|
52
59
|
*/
|
|
53
60
|
function backgroundStage(blockId: string): 'incorporating' | 'reviewing' | 'recommending' | null {
|
|
54
|
-
if (recommending.value.has(blockId)) return 'recommending'
|
|
61
|
+
if (recommending.value.has(blockId) || hasPendingRecommendations(blockId)) return 'recommending'
|
|
55
62
|
const status = reviews.value[blockId]?.status
|
|
56
63
|
return status === 'incorporating' || status === 'reviewing' ? status : null
|
|
57
64
|
}
|
|
@@ -176,19 +183,26 @@ export const useRequirementsStore = defineStore('requirements', () => {
|
|
|
176
183
|
}
|
|
177
184
|
|
|
178
185
|
function isRecommending(blockId: string): boolean {
|
|
179
|
-
return recommending.value.has(blockId)
|
|
186
|
+
return recommending.value.has(blockId) || hasPendingRecommendations(blockId)
|
|
180
187
|
}
|
|
181
188
|
|
|
182
189
|
/**
|
|
183
190
|
* Ask the Requirement Writer to recommend answers for a batch of findings (by item id).
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
191
|
+
* ASYNCHRONOUS: returns at once with `pending` placeholder recommendations (the Writer runs
|
|
192
|
+
* per finding in the durable driver), which fill in (`ready`) via live `requirements` stream
|
|
193
|
+
* events; a notification calls the user back when the batch is ready. The board shows the
|
|
194
|
+
* `recommending` background stage while any placeholder is pending. Optional `note` steers the
|
|
195
|
+
* whole batch.
|
|
187
196
|
*/
|
|
188
|
-
async function requestRecommendations(blockId: string, itemIds: string[]) {
|
|
197
|
+
async function requestRecommendations(blockId: string, itemIds: string[], note?: string) {
|
|
189
198
|
withFlag(recommending, blockId, true)
|
|
190
199
|
try {
|
|
191
|
-
const updated = await api.requestRecommendations(
|
|
200
|
+
const updated = await api.requestRecommendations(
|
|
201
|
+
workspace.requireId(),
|
|
202
|
+
blockId,
|
|
203
|
+
itemIds,
|
|
204
|
+
note,
|
|
205
|
+
)
|
|
192
206
|
if (updated) store(updated)
|
|
193
207
|
return updated
|
|
194
208
|
} finally {
|
|
@@ -50,8 +50,12 @@ export type RequirementReviewStatus =
|
|
|
50
50
|
/** How a human resolves a review that hit its iteration cap. */
|
|
51
51
|
export type ResolveRequirementsExceededChoice = 'extra-round' | 'proceed' | 'stop-reset'
|
|
52
52
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Lifecycle of a Requirement-Writer recommendation. `pending` is a placeholder created the
|
|
55
|
+
* moment the human requests it — the Writer is still producing the suggestion in the background
|
|
56
|
+
* (the async story); it fills in to `ready` via the `requirements` stream.
|
|
57
|
+
*/
|
|
58
|
+
export type RecommendationStatus = 'pending' | 'ready' | 'accepted' | 'rejected'
|
|
55
59
|
|
|
56
60
|
/**
|
|
57
61
|
* A Requirement-Writer suggestion for one finding. First-class on the review (survives the
|
|
@@ -60,7 +64,7 @@ export type RecommendationStatus = 'ready' | 'accepted' | 'rejected'
|
|
|
60
64
|
*/
|
|
61
65
|
export interface RequirementRecommendation {
|
|
62
66
|
id: string
|
|
63
|
-
sourceFinding: { title: string; detail: string }
|
|
67
|
+
sourceFinding: { title: string; detail: string; itemId?: string }
|
|
64
68
|
recommendedText: string
|
|
65
69
|
status: RecommendationStatus
|
|
66
70
|
note: string | null
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.1",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|