@cat-factory/app 0.28.0 → 0.28.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/documents/DocumentImportModal.vue +10 -0
- package/app/components/documents/DocumentSourceConnectModal.vue +11 -0
- package/app/components/github/GitHubPanel.vue +10 -0
- package/app/components/layout/IntegrationBackTitle.vue +27 -0
- package/app/components/layout/IntegrationsHub.vue +3 -2
- package/app/components/providers/VendorCredentialsModal.vue +10 -0
- package/app/components/requirements/RequirementsReviewWindow.vue +75 -13
- package/app/components/settings/LocalModelEndpointsPanel.vue +10 -0
- package/app/components/settings/ObservabilityConnectionPanel.vue +10 -0
- package/app/components/settings/OpenRouterCatalogPanel.vue +10 -0
- package/app/components/settings/ProviderConnectionPanel.vue +10 -0
- package/app/components/settings/UserSecretsSection.vue +10 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +10 -0
- package/app/components/slack/SlackPanel.vue +10 -0
- package/app/components/tasks/TaskImportModal.vue +10 -0
- package/app/components/tasks/TaskSourceConnectModal.vue +11 -0
- package/app/composables/api/reviews.ts +9 -3
- package/app/stores/requirements.ts +24 -10
- package/app/stores/ui.ts +31 -0
- package/app/types/requirements.ts +7 -3
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { DocumentSourceKind } from '~/types/domain'
|
|
3
|
+
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
3
4
|
|
|
4
5
|
// Import pages from a connected document source and pick one to expand into
|
|
5
6
|
// board structure. A source selector lets the user choose which connected source
|
|
@@ -74,6 +75,15 @@ function preview(externalId: string) {
|
|
|
74
75
|
|
|
75
76
|
<template>
|
|
76
77
|
<UModal v-model:open="open" title="Import from a document source">
|
|
78
|
+
<template #title>
|
|
79
|
+
<IntegrationBackTitle
|
|
80
|
+
title="Import from a document source"
|
|
81
|
+
@back="
|
|
82
|
+
open = false
|
|
83
|
+
ui.openIntegrations()
|
|
84
|
+
"
|
|
85
|
+
/>
|
|
86
|
+
</template>
|
|
77
87
|
<template #body>
|
|
78
88
|
<div v-if="!documents.anyConnected" class="space-y-3 text-center">
|
|
79
89
|
<UIcon name="i-lucide-plug" class="mx-auto h-8 w-8 text-slate-500" />
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// same modal serves Confluence, Notion and any future source. Secret credentials
|
|
5
5
|
// are write-only — the backend never returns them, so on reload we show
|
|
6
6
|
// "Connected" with empty fields.
|
|
7
|
+
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
8
|
+
|
|
7
9
|
const ui = useUiStore()
|
|
8
10
|
const documents = useDocumentsStore()
|
|
9
11
|
const toast = useToast()
|
|
@@ -77,6 +79,15 @@ async function disconnect() {
|
|
|
77
79
|
|
|
78
80
|
<template>
|
|
79
81
|
<UModal v-model:open="open" :title="descriptor?.label ?? 'Connect source'">
|
|
82
|
+
<template #title>
|
|
83
|
+
<IntegrationBackTitle
|
|
84
|
+
:title="descriptor?.label ?? 'Connect source'"
|
|
85
|
+
@back="
|
|
86
|
+
open = false
|
|
87
|
+
ui.openIntegrations()
|
|
88
|
+
"
|
|
89
|
+
/>
|
|
90
|
+
</template>
|
|
80
91
|
<template #body>
|
|
81
92
|
<div v-if="descriptor" class="space-y-4">
|
|
82
93
|
<p class="text-sm text-slate-400">
|
|
@@ -10,6 +10,7 @@ import type { GitHubPullRequest, GitHubRepo } from '~/types/domain'
|
|
|
10
10
|
// tag, so it silently renders as an empty element. Importing it directly binds
|
|
11
11
|
// the tag unambiguously.
|
|
12
12
|
import GitHubConnect from './GitHubConnect.vue'
|
|
13
|
+
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
13
14
|
|
|
14
15
|
const ui = useUiStore()
|
|
15
16
|
const github = useGitHubStore()
|
|
@@ -199,6 +200,15 @@ async function merge(pr: GitHubPullRequest) {
|
|
|
199
200
|
|
|
200
201
|
<template>
|
|
201
202
|
<UModal v-model:open="open" title="GitHub" :ui="{ content: 'max-w-2xl' }">
|
|
203
|
+
<template #title>
|
|
204
|
+
<IntegrationBackTitle
|
|
205
|
+
title="GitHub"
|
|
206
|
+
@back="
|
|
207
|
+
open = false
|
|
208
|
+
ui.openIntegrations()
|
|
209
|
+
"
|
|
210
|
+
/>
|
|
211
|
+
</template>
|
|
202
212
|
<template #body>
|
|
203
213
|
<div class="space-y-5">
|
|
204
214
|
<!-- not connected: connect -->
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Title content for an integration sub-panel's modal header. Renders the panel
|
|
3
|
+
// title with a leading "back" control that returns to the Integrations hub, shown
|
|
4
|
+
// only when the panel was actually reached from there (`ui.cameFromIntegrations`).
|
|
5
|
+
// Panels opened from the command bar, sidebar, a banner or an inspector link don't
|
|
6
|
+
// grow a dead Back. Dropped into a UModal's #title slot, so it inherits the modal's
|
|
7
|
+
// title styling; it emits `back` and the host panel closes itself + reopens the hub.
|
|
8
|
+
defineProps<{ title?: string }>()
|
|
9
|
+
const emit = defineEmits<{ back: [] }>()
|
|
10
|
+
const ui = useUiStore()
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<span class="flex items-center gap-1.5">
|
|
15
|
+
<UButton
|
|
16
|
+
v-if="ui.cameFromIntegrations"
|
|
17
|
+
icon="i-lucide-arrow-left"
|
|
18
|
+
color="neutral"
|
|
19
|
+
variant="ghost"
|
|
20
|
+
size="xs"
|
|
21
|
+
class="-ml-1.5 shrink-0"
|
|
22
|
+
aria-label="Back to Integrations"
|
|
23
|
+
@click.stop="emit('back')"
|
|
24
|
+
/>
|
|
25
|
+
<span class="min-w-0 truncate">{{ title }}</span>
|
|
26
|
+
</span>
|
|
27
|
+
</template>
|
|
@@ -64,9 +64,10 @@ interface IntegrationGroup {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// Run an integration's open handler, then dismiss the hub so its panel takes over.
|
|
67
|
+
// `openFromIntegrations` also marks that the panel was reached from here, so the panel
|
|
68
|
+
// renders a "Back to Integrations" control (see IntegrationBackTitle).
|
|
67
69
|
function go(fn: () => void) {
|
|
68
|
-
fn
|
|
69
|
-
ui.closeIntegrations()
|
|
70
|
+
ui.openFromIntegrations(fn)
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
const groups = computed<IntegrationGroup[]>(() => {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// (Claude, GLM, ChatGPT/Codex) are connected per-user in the Personal subscriptions section.
|
|
8
8
|
import { computed, ref, watch } from 'vue'
|
|
9
9
|
import type { SubscriptionVendor } from '~/types/domain'
|
|
10
|
+
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
10
11
|
|
|
11
12
|
const ui = useUiStore()
|
|
12
13
|
const workspace = useWorkspaceStore()
|
|
@@ -116,6 +117,15 @@ function vendorLabel(v: SubscriptionVendor): string {
|
|
|
116
117
|
|
|
117
118
|
<template>
|
|
118
119
|
<UModal v-model:open="open" title="LLM Vendors" :ui="{ content: 'max-w-2xl' }">
|
|
120
|
+
<template #title>
|
|
121
|
+
<IntegrationBackTitle
|
|
122
|
+
title="LLM Vendors"
|
|
123
|
+
@back="
|
|
124
|
+
open = false
|
|
125
|
+
ui.openIntegrations()
|
|
126
|
+
"
|
|
127
|
+
/>
|
|
128
|
+
</template>
|
|
119
129
|
<template #body>
|
|
120
130
|
<UTabs
|
|
121
131
|
v-model="activeTab"
|
|
@@ -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
|
>
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// automatically in the per-workspace model picker. One endpoint per runner type.
|
|
8
8
|
import { computed, ref, watch } from 'vue'
|
|
9
9
|
import { LOCAL_RUNNER_DEFAULTS, LOCAL_RUNNER_LABELS, type LocalRunner } from '~/types/localModels'
|
|
10
|
+
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
10
11
|
|
|
11
12
|
const ui = useUiStore()
|
|
12
13
|
const store = useLocalModelsStore()
|
|
@@ -152,6 +153,15 @@ async function remove(p: LocalRunner) {
|
|
|
152
153
|
|
|
153
154
|
<template>
|
|
154
155
|
<UModal v-model:open="open" title="My local runners" :ui="{ content: 'max-w-2xl' }">
|
|
156
|
+
<template #title>
|
|
157
|
+
<IntegrationBackTitle
|
|
158
|
+
title="My local runners"
|
|
159
|
+
@back="
|
|
160
|
+
open = false
|
|
161
|
+
ui.openIntegrations()
|
|
162
|
+
"
|
|
163
|
+
/>
|
|
164
|
+
</template>
|
|
155
165
|
<template #body>
|
|
156
166
|
<div class="space-y-4">
|
|
157
167
|
<p class="text-xs text-slate-400">
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// block-id entry here. Opened from the Integrations hub.
|
|
7
7
|
import { computed, reactive, ref, watch } from 'vue'
|
|
8
8
|
import type { ObservabilityProviderKind } from '~/types/releaseHealth'
|
|
9
|
+
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
9
10
|
|
|
10
11
|
const ui = useUiStore()
|
|
11
12
|
const store = useReleaseHealthStore()
|
|
@@ -128,6 +129,15 @@ const connectedLabel = computed(() => {
|
|
|
128
129
|
|
|
129
130
|
<template>
|
|
130
131
|
<UModal v-model:open="open" title="Post-release health" :ui="{ content: 'max-w-lg' }">
|
|
132
|
+
<template #title>
|
|
133
|
+
<IntegrationBackTitle
|
|
134
|
+
title="Post-release health"
|
|
135
|
+
@back="
|
|
136
|
+
open = false
|
|
137
|
+
ui.openIntegrations()
|
|
138
|
+
"
|
|
139
|
+
/>
|
|
140
|
+
</template>
|
|
131
141
|
<template #body>
|
|
132
142
|
<div class="space-y-4">
|
|
133
143
|
<p class="text-sm text-slate-400">
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// entry point for the key; this panel just makes OpenRouter self-sufficient.
|
|
10
10
|
import { computed, ref, watch } from 'vue'
|
|
11
11
|
import type { OpenRouterModelMeta } from '~/types/openrouter'
|
|
12
|
+
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
12
13
|
|
|
13
14
|
const ui = useUiStore()
|
|
14
15
|
const workspace = useWorkspaceStore()
|
|
@@ -193,6 +194,15 @@ function manageKeys() {
|
|
|
193
194
|
|
|
194
195
|
<template>
|
|
195
196
|
<UModal v-model:open="open" title="OpenRouter" :ui="{ content: 'max-w-2xl' }">
|
|
197
|
+
<template #title>
|
|
198
|
+
<IntegrationBackTitle
|
|
199
|
+
title="OpenRouter"
|
|
200
|
+
@back="
|
|
201
|
+
open = false
|
|
202
|
+
ui.openIntegrations()
|
|
203
|
+
"
|
|
204
|
+
/>
|
|
205
|
+
</template>
|
|
196
206
|
<template #body>
|
|
197
207
|
<div class="space-y-4">
|
|
198
208
|
<p class="text-xs text-slate-400">
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// with a `default` is optional — left blank it falls back to that default.
|
|
10
10
|
import { computed, ref, watch } from 'vue'
|
|
11
11
|
import type { ProviderConnectionKind } from '~/types/providerConnections'
|
|
12
|
+
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
12
13
|
|
|
13
14
|
const ui = useUiStore()
|
|
14
15
|
const store = useProviderConnectionsStore()
|
|
@@ -213,6 +214,15 @@ function fieldHelp(key: string): string | undefined {
|
|
|
213
214
|
|
|
214
215
|
<template>
|
|
215
216
|
<UModal v-model:open="open" :title="meta?.title ?? 'Provider'" :ui="{ content: 'max-w-xl' }">
|
|
217
|
+
<template #title>
|
|
218
|
+
<IntegrationBackTitle
|
|
219
|
+
:title="meta?.title ?? 'Provider'"
|
|
220
|
+
@back="
|
|
221
|
+
open = false
|
|
222
|
+
ui.openIntegrations()
|
|
223
|
+
"
|
|
224
|
+
/>
|
|
225
|
+
</template>
|
|
216
226
|
<template #body>
|
|
217
227
|
<div v-if="descriptor" class="space-y-4">
|
|
218
228
|
<p class="text-xs text-slate-400">{{ meta?.blurb }}</p>
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// access); the secret is write-only server-side and never shown again.
|
|
7
7
|
import { computed, ref, watch } from 'vue'
|
|
8
8
|
import type { ProviderConfigField, UserSecretKind } from '~/types/userSecrets'
|
|
9
|
+
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
9
10
|
|
|
10
11
|
const ui = useUiStore()
|
|
11
12
|
const store = useUserSecretsStore()
|
|
@@ -123,6 +124,15 @@ async function remove() {
|
|
|
123
124
|
|
|
124
125
|
<template>
|
|
125
126
|
<UModal v-model:open="open" title="My GitHub token" :ui="{ content: 'max-w-xl' }">
|
|
127
|
+
<template #title>
|
|
128
|
+
<IntegrationBackTitle
|
|
129
|
+
title="My GitHub token"
|
|
130
|
+
@back="
|
|
131
|
+
open = false
|
|
132
|
+
ui.openIntegrations()
|
|
133
|
+
"
|
|
134
|
+
/>
|
|
135
|
+
</template>
|
|
126
136
|
<template #body>
|
|
127
137
|
<div class="space-y-4">
|
|
128
138
|
<p class="text-xs text-slate-400">
|
|
@@ -12,6 +12,7 @@ import type { CreateTaskType, TaskLimitMode, WorkspaceSettings } from '~/types/d
|
|
|
12
12
|
import MergeThresholdsPanel from '~/components/settings/MergeThresholdsPanel.vue'
|
|
13
13
|
import IssueTrackerPanel from '~/components/settings/IssueTrackerPanel.vue'
|
|
14
14
|
import ServiceFragmentDefaultsPanel from '~/components/settings/ServiceFragmentDefaultsPanel.vue'
|
|
15
|
+
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
15
16
|
|
|
16
17
|
const ui = useUiStore()
|
|
17
18
|
const store = useWorkspaceSettingsStore()
|
|
@@ -160,6 +161,15 @@ async function saveBudget() {
|
|
|
160
161
|
|
|
161
162
|
<template>
|
|
162
163
|
<UModal v-model:open="open" title="Workspace settings" :ui="{ content: 'max-w-3xl' }">
|
|
164
|
+
<template #title>
|
|
165
|
+
<IntegrationBackTitle
|
|
166
|
+
title="Workspace settings"
|
|
167
|
+
@back="
|
|
168
|
+
open = false
|
|
169
|
+
ui.openIntegrations()
|
|
170
|
+
"
|
|
171
|
+
/>
|
|
172
|
+
</template>
|
|
163
173
|
<template #body>
|
|
164
174
|
<UTabs
|
|
165
175
|
v-model="activeTab"
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { computed, reactive, ref, watch } from 'vue'
|
|
8
8
|
import type { NotificationType } from '~/types/notifications'
|
|
9
9
|
import type { SlackMemberMappingEntry, SlackMemberRole, SlackRoute } from '~/types/slack'
|
|
10
|
+
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
10
11
|
|
|
11
12
|
const ui = useUiStore()
|
|
12
13
|
const slack = useSlackStore()
|
|
@@ -141,6 +142,15 @@ async function saveMapping() {
|
|
|
141
142
|
|
|
142
143
|
<template>
|
|
143
144
|
<UModal v-model:open="open" title="Slack notifications" :ui="{ content: 'max-w-2xl' }">
|
|
145
|
+
<template #title>
|
|
146
|
+
<IntegrationBackTitle
|
|
147
|
+
title="Slack notifications"
|
|
148
|
+
@back="
|
|
149
|
+
open = false
|
|
150
|
+
ui.openIntegrations()
|
|
151
|
+
"
|
|
152
|
+
/>
|
|
153
|
+
</template>
|
|
144
154
|
<template #body>
|
|
145
155
|
<div class="space-y-5">
|
|
146
156
|
<p class="text-xs text-slate-400">
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// each row opens the issue on GitHub.
|
|
9
9
|
import type { TaskSearchResult, TaskSourceKind } from '~/types/domain'
|
|
10
10
|
import type { AddTaskPrefill } from '~/stores/ui'
|
|
11
|
+
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
11
12
|
|
|
12
13
|
const ui = useUiStore()
|
|
13
14
|
const tasks = useTasksStore()
|
|
@@ -195,6 +196,15 @@ async function doSpawnEpic() {
|
|
|
195
196
|
|
|
196
197
|
<template>
|
|
197
198
|
<UModal v-model:open="open" :title="title">
|
|
199
|
+
<template #title>
|
|
200
|
+
<IntegrationBackTitle
|
|
201
|
+
:title="title"
|
|
202
|
+
@back="
|
|
203
|
+
open = false
|
|
204
|
+
ui.openIntegrations()
|
|
205
|
+
"
|
|
206
|
+
/>
|
|
207
|
+
</template>
|
|
198
208
|
<template #body>
|
|
199
209
|
<!-- Empty state: no source offered (none connected/installed, or all disabled) -->
|
|
200
210
|
<div v-if="!tasks.anyOffered" class="space-y-3 text-center">
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
// connected Jira without disconnecting it. The toggle only applies once a source is
|
|
12
12
|
// available (Jira connected / the GitHub App installed) — there is nothing to offer
|
|
13
13
|
// before that.
|
|
14
|
+
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
15
|
+
|
|
14
16
|
const ui = useUiStore()
|
|
15
17
|
const tasks = useTasksStore()
|
|
16
18
|
const toast = useToast()
|
|
@@ -104,6 +106,15 @@ async function toggleEnabled(enabled: boolean) {
|
|
|
104
106
|
|
|
105
107
|
<template>
|
|
106
108
|
<UModal v-model:open="open" :title="descriptor?.label ?? 'Task source'">
|
|
109
|
+
<template #title>
|
|
110
|
+
<IntegrationBackTitle
|
|
111
|
+
:title="descriptor?.label ?? 'Task source'"
|
|
112
|
+
@back="
|
|
113
|
+
open = false
|
|
114
|
+
ui.openIntegrations()
|
|
115
|
+
"
|
|
116
|
+
/>
|
|
117
|
+
</template>
|
|
107
118
|
<template #body>
|
|
108
119
|
<div v-if="descriptor" class="space-y-4">
|
|
109
120
|
<p class="text-sm text-slate-400">
|
|
@@ -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 {
|
package/app/stores/ui.ts
CHANGED
|
@@ -85,6 +85,11 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
85
85
|
// local runners, OpenRouter). Replaces the per-integration navbar buttons; each
|
|
86
86
|
// row inside it opens that integration's own panel via the handlers below.
|
|
87
87
|
const integrationsOpen = ref(false)
|
|
88
|
+
// True while an integration's own panel is showing AND it was reached from the hub
|
|
89
|
+
// (not the command bar, sidebar, a banner or an inspector link). Drives the "Back to
|
|
90
|
+
// Integrations" control those panels render: it only offers a return path when there
|
|
91
|
+
// is one. Every direct `open*` below resets it; `openFromIntegrations` sets it.
|
|
92
|
+
const cameFromIntegrations = ref(false)
|
|
88
93
|
|
|
89
94
|
// Workspace-settings modal: a single tabbed window gathering the workspace-wide
|
|
90
95
|
// config (workspace / merge thresholds / issue writeback / service best practices).
|
|
@@ -231,6 +236,7 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
231
236
|
}
|
|
232
237
|
|
|
233
238
|
function openDocumentConnect(source: DocumentSourceKind) {
|
|
239
|
+
cameFromIntegrations.value = false
|
|
234
240
|
documentConnect.value = { source }
|
|
235
241
|
}
|
|
236
242
|
function closeDocumentConnect() {
|
|
@@ -240,6 +246,7 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
240
246
|
targetFrameId: string | null = null,
|
|
241
247
|
source: DocumentSourceKind | null = null,
|
|
242
248
|
) {
|
|
249
|
+
cameFromIntegrations.value = false
|
|
243
250
|
documentImport.value = { source, targetFrameId }
|
|
244
251
|
}
|
|
245
252
|
function closeDocumentImport() {
|
|
@@ -256,12 +263,14 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
256
263
|
spawnPreview.value = null
|
|
257
264
|
}
|
|
258
265
|
function openTaskConnect(source: TaskSourceKind) {
|
|
266
|
+
cameFromIntegrations.value = false
|
|
259
267
|
taskConnect.value = { source }
|
|
260
268
|
}
|
|
261
269
|
function closeTaskConnect() {
|
|
262
270
|
taskConnect.value = null
|
|
263
271
|
}
|
|
264
272
|
function openTaskImport(source: TaskSourceKind | null = null, containerId: string | null = null) {
|
|
273
|
+
cameFromIntegrations.value = false
|
|
265
274
|
taskImport.value = { source, containerId }
|
|
266
275
|
}
|
|
267
276
|
function closeTaskImport() {
|
|
@@ -294,12 +303,14 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
294
303
|
addServiceOpen.value = false
|
|
295
304
|
}
|
|
296
305
|
function openGitHub() {
|
|
306
|
+
cameFromIntegrations.value = false
|
|
297
307
|
githubOpen.value = true
|
|
298
308
|
}
|
|
299
309
|
function closeGitHub() {
|
|
300
310
|
githubOpen.value = false
|
|
301
311
|
}
|
|
302
312
|
function openSlack() {
|
|
313
|
+
cameFromIntegrations.value = false
|
|
303
314
|
slackOpen.value = true
|
|
304
315
|
}
|
|
305
316
|
function closeSlack() {
|
|
@@ -321,12 +332,24 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
321
332
|
commandBarOpen.value = !commandBarOpen.value
|
|
322
333
|
}
|
|
323
334
|
function openIntegrations() {
|
|
335
|
+
// Reaching the hub itself (fresh, or via a panel's Back control) clears the
|
|
336
|
+
// came-from marker — we're at the hub, not inside a hub-spawned panel.
|
|
337
|
+
cameFromIntegrations.value = false
|
|
324
338
|
integrationsOpen.value = true
|
|
325
339
|
}
|
|
326
340
|
function closeIntegrations() {
|
|
327
341
|
integrationsOpen.value = false
|
|
328
342
|
}
|
|
343
|
+
// Open an integration's own panel FROM the hub: run its open handler (which resets
|
|
344
|
+
// `cameFromIntegrations`), then mark that we came from the hub and dismiss it. The
|
|
345
|
+
// panel reads `cameFromIntegrations` to show its Back control.
|
|
346
|
+
function openFromIntegrations(open: () => void) {
|
|
347
|
+
open()
|
|
348
|
+
cameFromIntegrations.value = true
|
|
349
|
+
integrationsOpen.value = false
|
|
350
|
+
}
|
|
329
351
|
function openWorkspaceSettings(tab = 'workspace') {
|
|
352
|
+
cameFromIntegrations.value = false
|
|
330
353
|
workspaceSettingsTab.value = tab
|
|
331
354
|
workspaceSettingsOpen.value = true
|
|
332
355
|
}
|
|
@@ -337,12 +360,14 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
337
360
|
workspaceSettingsTab.value = tab
|
|
338
361
|
}
|
|
339
362
|
function openObservabilityConnection() {
|
|
363
|
+
cameFromIntegrations.value = false
|
|
340
364
|
observabilityConnectionOpen.value = true
|
|
341
365
|
}
|
|
342
366
|
function closeObservabilityConnection() {
|
|
343
367
|
observabilityConnectionOpen.value = false
|
|
344
368
|
}
|
|
345
369
|
function openProviderConnection(kind: 'environment' | 'runner-pool') {
|
|
370
|
+
cameFromIntegrations.value = false
|
|
346
371
|
providerConnectionKind.value = kind
|
|
347
372
|
}
|
|
348
373
|
function closeProviderConnection() {
|
|
@@ -355,12 +380,14 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
355
380
|
modelConfigOpen.value = false
|
|
356
381
|
}
|
|
357
382
|
function openVendorCredentials() {
|
|
383
|
+
cameFromIntegrations.value = false
|
|
358
384
|
vendorCredentialsOpen.value = true
|
|
359
385
|
}
|
|
360
386
|
function closeVendorCredentials() {
|
|
361
387
|
vendorCredentialsOpen.value = false
|
|
362
388
|
}
|
|
363
389
|
function openLocalModels() {
|
|
390
|
+
cameFromIntegrations.value = false
|
|
364
391
|
localModelsOpen.value = true
|
|
365
392
|
}
|
|
366
393
|
function closeLocalModels() {
|
|
@@ -373,12 +400,14 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
373
400
|
sandboxOpen.value = false
|
|
374
401
|
}
|
|
375
402
|
function openUserSecrets() {
|
|
403
|
+
cameFromIntegrations.value = false
|
|
376
404
|
userSecretsOpen.value = true
|
|
377
405
|
}
|
|
378
406
|
function closeUserSecrets() {
|
|
379
407
|
userSecretsOpen.value = false
|
|
380
408
|
}
|
|
381
409
|
function openOpenRouter() {
|
|
410
|
+
cameFromIntegrations.value = false
|
|
382
411
|
openRouterOpen.value = true
|
|
383
412
|
}
|
|
384
413
|
function closeOpenRouter() {
|
|
@@ -465,6 +494,7 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
465
494
|
fragmentLibraryOpen,
|
|
466
495
|
commandBarOpen,
|
|
467
496
|
integrationsOpen,
|
|
497
|
+
cameFromIntegrations,
|
|
468
498
|
workspaceSettingsOpen,
|
|
469
499
|
workspaceSettingsTab,
|
|
470
500
|
observabilityConnectionOpen,
|
|
@@ -524,6 +554,7 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
524
554
|
toggleCommandBar,
|
|
525
555
|
openIntegrations,
|
|
526
556
|
closeIntegrations,
|
|
557
|
+
openFromIntegrations,
|
|
527
558
|
openWorkspaceSettings,
|
|
528
559
|
closeWorkspaceSettings,
|
|
529
560
|
setWorkspaceSettingsTab,
|
|
@@ -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.2",
|
|
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",
|