@cat-factory/app 0.35.0 → 0.37.0
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/auth/UserMenu.vue +11 -1
- package/app/components/brainstorm/BrainstormWindow.vue +2 -1
- package/app/components/clarity/ClarityReviewWindow.vue +2 -1
- package/app/components/gates/GateResultView.vue +107 -12
- package/app/components/layout/IntegrationBackTitle.vue +12 -7
- package/app/components/layout/IntegrationsHub.vue +191 -43
- package/app/components/layout/NotificationsInbox.vue +16 -0
- package/app/components/layout/PersonalSetupModal.vue +141 -0
- package/app/components/pipeline/PipelineBuilder.vue +1 -1
- package/app/components/providers/VendorCredentialsModal.vue +7 -2
- package/app/components/slack/SlackPanel.vue +1 -0
- package/app/composables/api/accounts.ts +36 -51
- package/app/composables/api/auth.ts +20 -19
- package/app/composables/api/board.ts +60 -40
- package/app/composables/api/bootstrap.ts +25 -22
- package/app/composables/api/client.ts +102 -0
- package/app/composables/api/context.ts +25 -6
- package/app/composables/api/documents.ts +36 -34
- package/app/composables/api/execution.ts +65 -48
- package/app/composables/api/followUps.ts +26 -26
- package/app/composables/api/fragments.ts +47 -34
- package/app/composables/api/github.ts +65 -45
- package/app/composables/api/humanReview.ts +19 -0
- package/app/composables/api/humanTest.ts +15 -11
- package/app/composables/api/kaizen.ts +8 -6
- package/app/composables/api/localSettings.ts +5 -4
- package/app/composables/api/models.ts +58 -51
- package/app/composables/api/notifications.ts +13 -7
- package/app/composables/api/presets.ts +34 -28
- package/app/composables/api/providerConnections.ts +68 -26
- package/app/composables/api/recurring.ts +40 -30
- package/app/composables/api/releaseHealth.ts +28 -26
- package/app/composables/api/reviews.ts +136 -114
- package/app/composables/api/sandbox.ts +52 -34
- package/app/composables/api/slack.ts +22 -25
- package/app/composables/api/spec.ts +3 -3
- package/app/composables/api/tasks.ts +42 -41
- package/app/composables/api/userSecrets.ts +12 -17
- package/app/composables/api/workspaces.ts +21 -15
- package/app/composables/useApi.ts +11 -1
- package/app/composables/useIntegrationBack.ts +9 -3
- package/app/pages/index.vue +2 -0
- package/app/stores/auth.ts +2 -1
- package/app/stores/board.ts +2 -1
- package/app/stores/brainstorm.ts +2 -2
- package/app/stores/clarity.ts +6 -2
- package/app/stores/execution.ts +3 -2
- package/app/stores/github.ts +1 -2
- package/app/stores/humanReview.ts +41 -0
- package/app/stores/mergePresets.ts +2 -6
- package/app/stores/pipelines.ts +1 -1
- package/app/stores/recurringPipelines.ts +2 -7
- package/app/stores/sandbox.ts +1 -2
- package/app/stores/ui.ts +62 -19
- package/app/types/accountSettings.ts +11 -36
- package/app/types/accounts.ts +16 -71
- package/app/types/bootstrap.ts +13 -75
- package/app/types/brainstorm.ts +13 -38
- package/app/types/clarity.ts +12 -43
- package/app/types/consensus.ts +16 -89
- package/app/types/documents.ts +19 -94
- package/app/types/domain.ts +54 -582
- package/app/types/execution.ts +48 -499
- package/app/types/fragments.ts +15 -83
- package/app/types/github.ts +25 -161
- package/app/types/incidentEnrichment.ts +10 -25
- package/app/types/localModels.ts +11 -61
- package/app/types/localSettings.ts +9 -26
- package/app/types/merge.ts +10 -68
- package/app/types/model-presets.ts +7 -28
- package/app/types/models.ts +16 -164
- package/app/types/notifications.ts +18 -76
- package/app/types/openrouter.ts +8 -34
- package/app/types/providerConnections.ts +21 -41
- package/app/types/provisioningLogs.ts +9 -29
- package/app/types/recurring.ts +10 -63
- package/app/types/releaseHealth.ts +15 -39
- package/app/types/requirements.ts +14 -84
- package/app/types/sandbox.ts +45 -161
- package/app/types/services.ts +3 -22
- package/app/types/slack.ts +10 -47
- package/app/types/spec.ts +15 -68
- package/app/types/tasks.ts +15 -111
- package/app/types/tracker.ts +9 -24
- package/app/types/userSecrets.ts +12 -47
- package/app/utils/catalog.ts +12 -0
- package/package.json +9 -2
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
3
3
|
|
|
4
|
-
// Signed-in identity +
|
|
4
|
+
// Signed-in identity + per-user actions, shown in the sidebar when auth is enabled.
|
|
5
5
|
const auth = useAuthStore()
|
|
6
|
+
const ui = useUiStore()
|
|
6
7
|
|
|
7
8
|
const items = computed<DropdownMenuItem[][]>(() => [
|
|
9
|
+
[
|
|
10
|
+
{
|
|
11
|
+
// The user-scoped "My setup" hub (personal GitHub token, local runners, personal
|
|
12
|
+
// subscriptions) — kept out of the workspace Integrations hub, reachable here.
|
|
13
|
+
label: 'My setup',
|
|
14
|
+
icon: 'i-lucide-user-cog',
|
|
15
|
+
onSelect: () => ui.openPersonalSetup(),
|
|
16
|
+
},
|
|
17
|
+
],
|
|
8
18
|
[
|
|
9
19
|
{
|
|
10
20
|
label: 'Sign out',
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { parseOutputOutline } from '~/utils/agentOutput'
|
|
11
11
|
import type {
|
|
12
12
|
BrainstormItem,
|
|
13
|
+
BrainstormItemStatus,
|
|
13
14
|
BrainstormSession,
|
|
14
15
|
BrainstormStage,
|
|
15
16
|
ReviewItemCategory,
|
|
@@ -136,7 +137,7 @@ async function submitReply(item: BrainstormItem) {
|
|
|
136
137
|
}
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
async function setStatus(item: BrainstormItem, itemStatus:
|
|
140
|
+
async function setStatus(item: BrainstormItem, itemStatus: BrainstormItemStatus) {
|
|
140
141
|
if (!session.value) return
|
|
141
142
|
try {
|
|
142
143
|
await brainstorm.setItemStatus(session.value, item.id, itemStatus)
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// consumes.
|
|
11
11
|
import { parseOutputOutline } from '~/utils/agentOutput'
|
|
12
12
|
import type {
|
|
13
|
+
ClarityItemStatus,
|
|
13
14
|
ClarityReview,
|
|
14
15
|
ClarityReviewItem,
|
|
15
16
|
ReviewItemCategory,
|
|
@@ -131,7 +132,7 @@ async function submitReply(item: ClarityReviewItem) {
|
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
async function setStatus(item: ClarityReviewItem, itemStatus:
|
|
135
|
+
async function setStatus(item: ClarityReviewItem, itemStatus: ClarityItemStatus) {
|
|
135
136
|
if (!review.value) return
|
|
136
137
|
try {
|
|
137
138
|
await clarity.setItemStatus(review.value, item.id, itemStatus)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// persists on `step.gate`: the precheck verdict, the helper attempt budget, the gated
|
|
6
6
|
// commit, and — for CI — the failing checks behind the failure. One window serves both
|
|
7
7
|
// gates; it branches on the step's `agentKind` for the copy and the failure detail.
|
|
8
|
-
import { computed } from 'vue'
|
|
8
|
+
import { computed, ref } from 'vue'
|
|
9
9
|
import { agentKindMeta } from '~/utils/catalog'
|
|
10
10
|
import type { GateStepState } from '~/types/execution'
|
|
11
11
|
import StepRestartControl from '~/components/panels/StepRestartControl.vue'
|
|
@@ -30,10 +30,37 @@ const step = computed(() => {
|
|
|
30
30
|
const gate = computed<GateStepState | null>(() => step.value?.gate ?? null)
|
|
31
31
|
|
|
32
32
|
const isCi = computed(() => step.value?.agentKind === 'ci')
|
|
33
|
+
const isHumanReview = computed(() => step.value?.agentKind === 'human-review')
|
|
33
34
|
const meta = computed(() => agentKindMeta(step.value?.agentKind ?? 'ci'))
|
|
34
|
-
const helperKind = computed(() =>
|
|
35
|
+
const helperKind = computed(() =>
|
|
36
|
+
isHumanReview.value ? 'fixer' : isCi.value ? 'ci-fixer' : 'conflict-resolver',
|
|
37
|
+
)
|
|
35
38
|
const helperMeta = computed(() => agentKindMeta(helperKind.value))
|
|
36
39
|
|
|
40
|
+
const subtitle = computed(() =>
|
|
41
|
+
isHumanReview.value
|
|
42
|
+
? 'Waits for a human code review on the PR, looping the fixer on comments'
|
|
43
|
+
: isCi.value
|
|
44
|
+
? 'Gates the PR on green CI, looping the CI fixer on failure'
|
|
45
|
+
: 'Gates the PR on a clean merge, looping the resolver on conflicts',
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
// Human-review: approval progress + the freeform "request a fix" control.
|
|
49
|
+
const humanReview = useHumanReviewStore()
|
|
50
|
+
const fixInstructions = ref('')
|
|
51
|
+
const fixBusy = computed(() => (blockId.value ? humanReview.isBusy(blockId.value) : false))
|
|
52
|
+
async function submitFix() {
|
|
53
|
+
const id = blockId.value
|
|
54
|
+
const text = fixInstructions.value.trim()
|
|
55
|
+
if (!id || !text) return
|
|
56
|
+
await humanReview.requestFix(id, text)
|
|
57
|
+
fixInstructions.value = ''
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The displayed "required approvals" is derived from the cached branch-protection count via
|
|
61
|
+
// the gate's effective floor (`max(1, …)`, see review.logic.ts) rather than persisted twice.
|
|
62
|
+
const requiredApprovals = computed(() => Math.max(1, gate.value?.requiredApprovingReviewCount ?? 1))
|
|
63
|
+
|
|
37
64
|
const failingChecks = computed(() => gate.value?.failingChecks ?? [])
|
|
38
65
|
const shortSha = computed(() => (gate.value?.headSha ? gate.value.headSha.slice(0, 7) : null))
|
|
39
66
|
|
|
@@ -130,13 +157,7 @@ const conflictVerdict = computed(() => {
|
|
|
130
157
|
<h2 class="truncate text-sm font-semibold text-slate-100">
|
|
131
158
|
{{ meta.label }}{{ block ? ` — ${block.title}` : '' }}
|
|
132
159
|
</h2>
|
|
133
|
-
<p class="truncate text-[11px] text-slate-400">
|
|
134
|
-
{{
|
|
135
|
-
isCi
|
|
136
|
-
? 'Gates the PR on green CI, looping the CI fixer on failure'
|
|
137
|
-
: 'Gates the PR on a clean merge, looping the resolver on conflicts'
|
|
138
|
-
}}
|
|
139
|
-
</p>
|
|
160
|
+
<p class="truncate text-[11px] text-slate-400">{{ subtitle }}</p>
|
|
140
161
|
</div>
|
|
141
162
|
<UBadge :color="STATUS_META[status].badge" variant="subtle" size="sm">
|
|
142
163
|
{{ STATUS_META[status].label }}
|
|
@@ -186,6 +207,73 @@ const conflictVerdict = computed(() => {
|
|
|
186
207
|
</p>
|
|
187
208
|
</div>
|
|
188
209
|
|
|
210
|
+
<!-- Human review: approval progress, the feedback being fixed, freeform fix box -->
|
|
211
|
+
<template v-else-if="isHumanReview">
|
|
212
|
+
<div
|
|
213
|
+
class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
|
|
214
|
+
>
|
|
215
|
+
<UIcon name="i-lucide-users" class="h-4 w-4 shrink-0 text-violet-300" />
|
|
216
|
+
<span class="text-[13px] text-slate-200">
|
|
217
|
+
{{ gate.lastApprovals ?? 0 }} / {{ requiredApprovals }} approval{{
|
|
218
|
+
requiredApprovals === 1 ? '' : 's'
|
|
219
|
+
}}
|
|
220
|
+
<template v-if="status === 'fixing'"> · fixer addressing comments…</template>
|
|
221
|
+
<template v-else-if="status === 'failing'">
|
|
222
|
+
· review comments to address</template
|
|
223
|
+
>
|
|
224
|
+
<template v-else> · awaiting review</template>
|
|
225
|
+
</span>
|
|
226
|
+
</div>
|
|
227
|
+
<p
|
|
228
|
+
v-if="gate.lastFailureSummary"
|
|
229
|
+
class="mt-2 whitespace-pre-wrap rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2 text-[12px] leading-relaxed text-slate-300"
|
|
230
|
+
>
|
|
231
|
+
{{ gate.lastFailureSummary }}
|
|
232
|
+
</p>
|
|
233
|
+
<a
|
|
234
|
+
v-if="prUrl"
|
|
235
|
+
:href="prUrl"
|
|
236
|
+
target="_blank"
|
|
237
|
+
rel="noopener"
|
|
238
|
+
class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200 hover:underline"
|
|
239
|
+
>
|
|
240
|
+
Review pull request on GitHub
|
|
241
|
+
<UIcon name="i-lucide-external-link" class="h-3 w-3" />
|
|
242
|
+
</a>
|
|
243
|
+
|
|
244
|
+
<!-- Freeform fix request: dispatch the fixer now with these instructions. -->
|
|
245
|
+
<section v-if="status !== 'gave-up'" class="mt-4">
|
|
246
|
+
<h3
|
|
247
|
+
class="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-500"
|
|
248
|
+
>
|
|
249
|
+
Request a fix
|
|
250
|
+
</h3>
|
|
251
|
+
<p class="mb-2 text-[11px] leading-relaxed text-slate-500">
|
|
252
|
+
Describe a change for the fixer to make on the PR branch now (in addition to any
|
|
253
|
+
review comments, which it addresses automatically).
|
|
254
|
+
</p>
|
|
255
|
+
<textarea
|
|
256
|
+
v-model="fixInstructions"
|
|
257
|
+
rows="3"
|
|
258
|
+
:disabled="fixBusy"
|
|
259
|
+
placeholder="e.g. rename the helper and add a unit test for the empty-input case"
|
|
260
|
+
class="w-full resize-y rounded-md border border-slate-800 bg-slate-950/60 px-3 py-2 text-[13px] text-slate-200 placeholder:text-slate-600 focus:border-violet-500/60 focus:outline-none"
|
|
261
|
+
/>
|
|
262
|
+
<div class="mt-2 flex justify-end">
|
|
263
|
+
<UButton
|
|
264
|
+
size="sm"
|
|
265
|
+
color="primary"
|
|
266
|
+
icon="i-lucide-wrench"
|
|
267
|
+
:loading="fixBusy"
|
|
268
|
+
:disabled="fixBusy || fixInstructions.trim().length === 0"
|
|
269
|
+
@click="submitFix"
|
|
270
|
+
>
|
|
271
|
+
Request fix
|
|
272
|
+
</UButton>
|
|
273
|
+
</div>
|
|
274
|
+
</section>
|
|
275
|
+
</template>
|
|
276
|
+
|
|
189
277
|
<!-- CI: failing checks -->
|
|
190
278
|
<template v-else-if="isCi">
|
|
191
279
|
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
@@ -321,9 +409,16 @@ const conflictVerdict = computed(() => {
|
|
|
321
409
|
{{ helperMeta.label }}
|
|
322
410
|
</h4>
|
|
323
411
|
<p class="text-[12px] text-slate-300">
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
412
|
+
<!-- The human-review gate's budget is effectively unbounded (it waits for a human
|
|
413
|
+
indefinitely), so render a plain round count rather than "0/9007199254740991". -->
|
|
414
|
+
<template v-if="isHumanReview">
|
|
415
|
+
{{ gate.attempts }} fix round{{ gate.attempts === 1 ? '' : 's' }}
|
|
416
|
+
</template>
|
|
417
|
+
<template v-else>
|
|
418
|
+
{{ gate.attempts }}/{{ gate.maxAttempts }} attempt{{
|
|
419
|
+
gate.maxAttempts === 1 ? '' : 's'
|
|
420
|
+
}}
|
|
421
|
+
</template>
|
|
327
422
|
<template v-if="gate.phase === 'working'"> · running…</template>
|
|
328
423
|
<template v-else-if="gate.attempts === 0"> · not needed yet</template>
|
|
329
424
|
</p>
|
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
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
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// title with a leading "back" control that returns to the hub the panel was reached
|
|
4
|
+
// from — the workspace Integrations hub (`ui.cameFromIntegrations`) or the user-scoped
|
|
5
|
+
// "My setup" hub (`ui.cameFromPersonal`) — shown only when there is one. Panels opened
|
|
6
|
+
// from the command bar, sidebar, a banner or an inspector link don't grow a dead Back.
|
|
7
|
+
// Dropped into a UModal's #title slot, so it inherits the modal's title styling; it
|
|
8
|
+
// emits `back` and the host panel closes itself + reopens the right hub.
|
|
8
9
|
defineProps<{ title?: string }>()
|
|
9
10
|
const emit = defineEmits<{ back: [] }>()
|
|
10
11
|
const ui = useUiStore()
|
|
12
|
+
const cameFromHub = computed(() => ui.cameFromIntegrations || ui.cameFromPersonal)
|
|
13
|
+
const backLabel = computed(() =>
|
|
14
|
+
ui.cameFromPersonal ? 'Back to My setup' : 'Back to Integrations',
|
|
15
|
+
)
|
|
11
16
|
</script>
|
|
12
17
|
|
|
13
18
|
<template>
|
|
14
19
|
<span class="flex items-center gap-1.5">
|
|
15
20
|
<UButton
|
|
16
|
-
v-if="
|
|
21
|
+
v-if="cameFromHub"
|
|
17
22
|
icon="i-lucide-arrow-left"
|
|
18
23
|
color="neutral"
|
|
19
24
|
variant="ghost"
|
|
20
25
|
size="xs"
|
|
21
26
|
class="-ml-1.5 shrink-0"
|
|
22
|
-
aria-label="
|
|
27
|
+
:aria-label="backLabel"
|
|
23
28
|
@click.stop="emit('back')"
|
|
24
29
|
/>
|
|
25
30
|
<span class="min-w-0 truncate">{{ title }}</span>
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// The Integrations hub: a single modal that lists every external system the
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// opening one closes the hub and reveals that integration's own panel/modal.
|
|
2
|
+
// The Integrations hub: a single modal that lists every external system the WORKSPACE can
|
|
3
|
+
// enable or link in. Each row reuses the existing per-integration panel handlers on the `ui`
|
|
4
|
+
// store (so the integrations themselves are unchanged); opening one closes the hub and
|
|
5
|
+
// reveals that integration's own panel/modal.
|
|
7
6
|
//
|
|
8
|
-
// Sections gate on the same `available` probes the navbar used, so a system that
|
|
9
|
-
//
|
|
7
|
+
// Sections gate on the same `available` probes the navbar used, so a system that the backend
|
|
8
|
+
// has turned off simply doesn't appear here.
|
|
9
|
+
//
|
|
10
|
+
// Scope split: per-USER connections (a personal GitHub token, own-machine runners, personal
|
|
11
|
+
// subscriptions) now live in the "My setup" hub (UserMenu → My setup), NOT here — keeping
|
|
12
|
+
// this hub purely workspace-scoped. When auth is disabled there is no UserMenu to host them,
|
|
13
|
+
// so a "Personal (only you)" group falls back into this hub so they stay reachable.
|
|
10
14
|
const ui = useUiStore()
|
|
11
15
|
const auth = useAuthStore()
|
|
12
16
|
const github = useGitHubStore()
|
|
@@ -20,6 +24,11 @@ const userSecrets = useUserSecretsStore()
|
|
|
20
24
|
const apiKeys = useApiKeysStore()
|
|
21
25
|
const workspace = useWorkspaceStore()
|
|
22
26
|
|
|
27
|
+
// True when the per-user "My setup" hub is reachable (UserMenu renders only when signed in).
|
|
28
|
+
// When false (auth disabled / local mode) we fold the personal rows back into this hub so
|
|
29
|
+
// nothing becomes unreachable.
|
|
30
|
+
const personalHubReachable = computed(() => !!auth.user)
|
|
31
|
+
|
|
23
32
|
// The selected filing tracker, as a badge label ("GitHub Issues" / "Jira").
|
|
24
33
|
const trackerLabel = computed(() => {
|
|
25
34
|
if (tracker.settings.tracker === 'github') return 'GitHub Issues'
|
|
@@ -27,12 +36,17 @@ const trackerLabel = computed(() => {
|
|
|
27
36
|
return undefined
|
|
28
37
|
})
|
|
29
38
|
|
|
39
|
+
// Free-text filter over the rows (label + description), so a workspace with many enabled
|
|
40
|
+
// systems stays scannable. Reset when the hub re-opens.
|
|
41
|
+
const query = ref('')
|
|
42
|
+
|
|
30
43
|
// The observability connection status drives the hub's connected badge. Load it
|
|
31
44
|
// lazily when the hub opens (the secret-less connection view is cheap).
|
|
32
45
|
watch(
|
|
33
46
|
() => ui.integrationsOpen,
|
|
34
47
|
(isOpen) => {
|
|
35
48
|
if (isOpen) {
|
|
49
|
+
query.value = ''
|
|
36
50
|
void releaseHealth.ensureLoaded().catch(() => {})
|
|
37
51
|
void providerConnections.ensureLoaded().catch(() => {})
|
|
38
52
|
void userSecrets.load().catch(() => {})
|
|
@@ -47,8 +61,10 @@ const open = computed({
|
|
|
47
61
|
set: (v: boolean) => (v ? ui.openIntegrations() : ui.closeIntegrations()),
|
|
48
62
|
})
|
|
49
63
|
|
|
50
|
-
// One integration row. `
|
|
51
|
-
//
|
|
64
|
+
// One integration row. `connected` drives the green badge (`status` is its line — an
|
|
65
|
+
// account/team name or "Connected"); `attention` drives an amber badge (e.g. a source that
|
|
66
|
+
// is available but turned off) with `attentionLabel`. `recommended` tags an essential row
|
|
67
|
+
// with a "Recommended" chip while the workspace has nothing connected yet.
|
|
52
68
|
interface IntegrationItem {
|
|
53
69
|
key: string
|
|
54
70
|
icon: string
|
|
@@ -56,12 +72,27 @@ interface IntegrationItem {
|
|
|
56
72
|
description: string
|
|
57
73
|
status?: string
|
|
58
74
|
connected?: boolean
|
|
75
|
+
attention?: boolean
|
|
76
|
+
attentionLabel?: string
|
|
77
|
+
recommended?: boolean
|
|
78
|
+
onClick: () => void
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// A group may carry a small de-emphasised footer LINK (workspace config that isn't itself an
|
|
82
|
+
// integration, e.g. the issue-tracker settings) rendered under its rows rather than as a
|
|
83
|
+
// full row competing with the connections.
|
|
84
|
+
interface IntegrationFooterLink {
|
|
85
|
+
key: string
|
|
86
|
+
icon: string
|
|
87
|
+
label: string
|
|
88
|
+
status?: string
|
|
59
89
|
onClick: () => void
|
|
60
90
|
}
|
|
61
91
|
|
|
62
92
|
interface IntegrationGroup {
|
|
63
93
|
title: string
|
|
64
94
|
items: IntegrationItem[]
|
|
95
|
+
footerLink?: IntegrationFooterLink
|
|
65
96
|
}
|
|
66
97
|
|
|
67
98
|
// Run an integration's open handler, then dismiss the hub so its panel takes over.
|
|
@@ -87,22 +118,16 @@ const groups = computed<IntegrationGroup[]>(() => {
|
|
|
87
118
|
description: 'One gateway to 300+ models — add your key and enable models in one place.',
|
|
88
119
|
status: openRouterKeyConnected ? 'Key connected' : undefined,
|
|
89
120
|
connected: openRouterKeyConnected,
|
|
121
|
+
recommended: true,
|
|
90
122
|
onClick: () => go(ui.openOpenRouter),
|
|
91
123
|
},
|
|
92
124
|
{
|
|
93
125
|
key: 'vendors',
|
|
94
126
|
icon: 'i-lucide-key-round',
|
|
95
127
|
label: 'Vendors & keys',
|
|
96
|
-
description: 'LLM
|
|
128
|
+
description: 'Workspace LLM subscriptions and provider API keys.',
|
|
97
129
|
onClick: () => go(ui.openVendorCredentials),
|
|
98
130
|
},
|
|
99
|
-
{
|
|
100
|
-
key: 'local-runners',
|
|
101
|
-
icon: 'i-lucide-server',
|
|
102
|
-
label: 'My local runners',
|
|
103
|
-
description: 'Your own-machine model runners (Ollama, LM Studio, vLLM…).',
|
|
104
|
-
onClick: () => go(ui.openLocalModels),
|
|
105
|
-
},
|
|
106
131
|
],
|
|
107
132
|
})
|
|
108
133
|
|
|
@@ -116,23 +141,10 @@ const groups = computed<IntegrationGroup[]>(() => {
|
|
|
116
141
|
description: 'Connect the workspace’s GitHub App, browse repos, PRs and issues.',
|
|
117
142
|
status: github.connected ? github.connection?.accountLogin : undefined,
|
|
118
143
|
connected: github.connected,
|
|
144
|
+
recommended: true,
|
|
119
145
|
onClick: () => go(ui.openGitHub),
|
|
120
146
|
})
|
|
121
147
|
}
|
|
122
|
-
// Per-user GitHub PAT — works on every runtime (used for runs you initiate). Always
|
|
123
|
-
// offered; the badge reflects whether the signed-in user has stored one.
|
|
124
|
-
{
|
|
125
|
-
const pat = userSecrets.statusFor('github_pat')
|
|
126
|
-
code.push({
|
|
127
|
-
key: 'github-pat',
|
|
128
|
-
icon: 'i-lucide-key-round',
|
|
129
|
-
label: 'My GitHub token',
|
|
130
|
-
description: 'A personal access token used for runs you start (pushes, PRs, CI, merge).',
|
|
131
|
-
status: pat ? 'Connected' : undefined,
|
|
132
|
-
connected: !!pat,
|
|
133
|
-
onClick: () => go(ui.openUserSecrets),
|
|
134
|
-
})
|
|
135
|
-
}
|
|
136
148
|
if (code.length) out.push({ title: 'Source control', items: code })
|
|
137
149
|
|
|
138
150
|
// --- Communication ---------------------------------------------------------
|
|
@@ -180,10 +192,11 @@ const groups = computed<IntegrationGroup[]>(() => {
|
|
|
180
192
|
icon: src.icon,
|
|
181
193
|
label: src.label,
|
|
182
194
|
description: `Link ${src.label} to import and reference tracker issues.`,
|
|
183
|
-
// Available + enabled ⇒ offered (green); available + off ⇒ "Disabled";
|
|
195
|
+
// Available + enabled ⇒ offered (green); available + off ⇒ "Disabled" (amber);
|
|
184
196
|
// not available ⇒ no badge (Jira needs connecting; GitHub needs its App).
|
|
185
|
-
status: src.available ? (src.enabled ? undefined : 'Disabled') : undefined,
|
|
186
197
|
connected: src.available && src.enabled,
|
|
198
|
+
attention: src.available && !src.enabled,
|
|
199
|
+
attentionLabel: 'Disabled',
|
|
187
200
|
onClick: () => go(() => ui.openTaskConnect(src.source)),
|
|
188
201
|
}))
|
|
189
202
|
if (tasks.anyOffered) {
|
|
@@ -195,16 +208,19 @@ const groups = computed<IntegrationGroup[]>(() => {
|
|
|
195
208
|
onClick: () => go(() => ui.openTaskImport(null)),
|
|
196
209
|
})
|
|
197
210
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
211
|
+
// Choosing the filing tracker / writeback is workspace CONFIG, not an integration, so it
|
|
212
|
+
// sits as a quiet footer link under the sources rather than a competing row.
|
|
213
|
+
out.push({
|
|
214
|
+
title: 'Task trackers',
|
|
215
|
+
items: trackers,
|
|
216
|
+
footerLink: {
|
|
217
|
+
key: 'task:tracker',
|
|
218
|
+
icon: 'i-lucide-list-checks',
|
|
219
|
+
label: 'Issue tracker settings',
|
|
220
|
+
status: trackerLabel.value,
|
|
221
|
+
onClick: () => go(() => ui.openWorkspaceSettings('tracker')),
|
|
222
|
+
},
|
|
206
223
|
})
|
|
207
|
-
out.push({ title: 'Task trackers', items: trackers })
|
|
208
224
|
}
|
|
209
225
|
|
|
210
226
|
// --- Observability ---------------------------------------------------------
|
|
@@ -271,8 +287,73 @@ const groups = computed<IntegrationGroup[]>(() => {
|
|
|
271
287
|
}
|
|
272
288
|
if (infra.length) out.push({ title: 'Infrastructure', items: infra })
|
|
273
289
|
|
|
290
|
+
// --- Personal (only you) — fallback when there is no UserMenu to host "My setup" -------
|
|
291
|
+
// Per-user connections normally live in the My-setup hub; with auth disabled they fold in
|
|
292
|
+
// here so they stay reachable. (The badge reflects the signed-in user's stored secret.)
|
|
293
|
+
if (!personalHubReachable.value) {
|
|
294
|
+
const pat = !!userSecrets.statusFor('github_pat')
|
|
295
|
+
out.push({
|
|
296
|
+
title: 'Personal (only you)',
|
|
297
|
+
items: [
|
|
298
|
+
{
|
|
299
|
+
key: 'github-pat',
|
|
300
|
+
icon: 'i-lucide-key-round',
|
|
301
|
+
label: 'My GitHub token',
|
|
302
|
+
description: 'A personal access token used for runs you start (pushes, PRs, CI, merge).',
|
|
303
|
+
status: pat ? 'Connected' : undefined,
|
|
304
|
+
connected: pat,
|
|
305
|
+
onClick: () => go(ui.openUserSecrets),
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
key: 'local-runners',
|
|
309
|
+
icon: 'i-lucide-server',
|
|
310
|
+
label: 'My local runners',
|
|
311
|
+
description: 'Your own-machine model runners (Ollama, LM Studio, vLLM…).',
|
|
312
|
+
onClick: () => go(ui.openLocalModels),
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
|
|
274
318
|
return out
|
|
275
319
|
})
|
|
320
|
+
|
|
321
|
+
// Sort connected rows first, then amber "attention", then idle — a stable rank so each
|
|
322
|
+
// group reads "what's live" top-down without reshuffling unrelated rows.
|
|
323
|
+
function stateRank(item: IntegrationItem): number {
|
|
324
|
+
if (item.connected) return 0
|
|
325
|
+
if (item.attention) return 1
|
|
326
|
+
return 2
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const allItems = computed(() => groups.value.flatMap((g) => g.items))
|
|
330
|
+
const anyConnected = computed(() => allItems.value.some((i) => i.connected))
|
|
331
|
+
// Essential rows still unconnected — surfaced as the empty-workspace get-started shortcuts.
|
|
332
|
+
const recommendedActions = computed(() =>
|
|
333
|
+
allItems.value.filter((i) => i.recommended && !i.connected),
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
function matches(text: string, q: string): boolean {
|
|
337
|
+
return text.toLowerCase().includes(q)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Groups after the search filter + connected-first sort. A footer link is kept only when it
|
|
341
|
+
// also matches the query (or the query is empty); a group with no surviving rows/link drops.
|
|
342
|
+
const filteredGroups = computed<IntegrationGroup[]>(() => {
|
|
343
|
+
const q = query.value.trim().toLowerCase()
|
|
344
|
+
return groups.value
|
|
345
|
+
.map((g) => {
|
|
346
|
+
const items = (
|
|
347
|
+
q ? g.items.filter((i) => matches(i.label, q) || matches(i.description, q)) : g.items
|
|
348
|
+
)
|
|
349
|
+
.slice()
|
|
350
|
+
.sort((a, b) => stateRank(a) - stateRank(b))
|
|
351
|
+
const footerLink =
|
|
352
|
+
g.footerLink && (!q || matches(g.footerLink.label, q)) ? g.footerLink : undefined
|
|
353
|
+
return { ...g, items, footerLink }
|
|
354
|
+
})
|
|
355
|
+
.filter((g) => g.items.length || g.footerLink)
|
|
356
|
+
})
|
|
276
357
|
</script>
|
|
277
358
|
|
|
278
359
|
<template>
|
|
@@ -283,7 +364,47 @@ const groups = computed<IntegrationGroup[]>(() => {
|
|
|
283
364
|
Connect and manage the external systems this workspace can link in.
|
|
284
365
|
</p>
|
|
285
366
|
|
|
286
|
-
|
|
367
|
+
<!-- Get-started cue: an empty workspace gets the two essentials up front so the first
|
|
368
|
+
run isn't blocked on hunting for them. Hidden once anything is connected. -->
|
|
369
|
+
<div
|
|
370
|
+
v-if="!anyConnected && recommendedActions.length"
|
|
371
|
+
class="rounded-lg border border-primary-500/40 bg-primary-500/10 p-3"
|
|
372
|
+
>
|
|
373
|
+
<div class="mb-2 flex items-center gap-2 text-sm font-medium text-primary-200">
|
|
374
|
+
<UIcon name="i-lucide-rocket" class="h-4 w-4 shrink-0" />
|
|
375
|
+
<span>Get started</span>
|
|
376
|
+
</div>
|
|
377
|
+
<p class="mb-3 text-xs text-slate-300">
|
|
378
|
+
Connect a code source and a model provider to run your first pipeline.
|
|
379
|
+
</p>
|
|
380
|
+
<div class="flex flex-wrap gap-2">
|
|
381
|
+
<UButton
|
|
382
|
+
v-for="item in recommendedActions"
|
|
383
|
+
:key="`rec:${item.key}`"
|
|
384
|
+
size="xs"
|
|
385
|
+
color="primary"
|
|
386
|
+
variant="soft"
|
|
387
|
+
:icon="item.icon"
|
|
388
|
+
@click="item.onClick()"
|
|
389
|
+
>
|
|
390
|
+
{{ item.label }}
|
|
391
|
+
</UButton>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
<UInput
|
|
396
|
+
v-model="query"
|
|
397
|
+
icon="i-lucide-search"
|
|
398
|
+
size="sm"
|
|
399
|
+
placeholder="Search integrations…"
|
|
400
|
+
class="w-full"
|
|
401
|
+
/>
|
|
402
|
+
|
|
403
|
+
<p v-if="!filteredGroups.length" class="px-1 py-6 text-center text-sm text-slate-500">
|
|
404
|
+
No integrations match “{{ query }}”.
|
|
405
|
+
</p>
|
|
406
|
+
|
|
407
|
+
<section v-for="group in filteredGroups" :key="group.title">
|
|
287
408
|
<h3 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
288
409
|
{{ group.title }}
|
|
289
410
|
</h3>
|
|
@@ -302,12 +423,39 @@ const groups = computed<IntegrationGroup[]>(() => {
|
|
|
302
423
|
<UBadge v-if="item.connected" color="success" variant="subtle" size="sm">
|
|
303
424
|
{{ item.status || 'Connected' }}
|
|
304
425
|
</UBadge>
|
|
426
|
+
<UBadge v-else-if="item.attention" color="warning" variant="subtle" size="sm">
|
|
427
|
+
{{ item.attentionLabel || 'Needs attention' }}
|
|
428
|
+
</UBadge>
|
|
429
|
+
<span v-else class="text-[11px] text-slate-500">Not connected</span>
|
|
430
|
+
<UBadge
|
|
431
|
+
v-if="!anyConnected && item.recommended && !item.connected"
|
|
432
|
+
color="primary"
|
|
433
|
+
variant="subtle"
|
|
434
|
+
size="sm"
|
|
435
|
+
>
|
|
436
|
+
Recommended
|
|
437
|
+
</UBadge>
|
|
305
438
|
</div>
|
|
306
439
|
<p class="truncate text-xs text-slate-400">{{ item.description }}</p>
|
|
307
440
|
</div>
|
|
308
441
|
<UIcon name="i-lucide-chevron-right" class="h-4 w-4 shrink-0 text-slate-500" />
|
|
309
442
|
</button>
|
|
310
443
|
</div>
|
|
444
|
+
|
|
445
|
+
<!-- De-emphasised workspace-config link (e.g. issue tracker settings). -->
|
|
446
|
+
<button
|
|
447
|
+
v-if="group.footerLink"
|
|
448
|
+
type="button"
|
|
449
|
+
class="mt-1.5 flex w-full items-center gap-2 rounded-md px-3 py-1.5 text-left text-xs text-slate-400 transition hover:bg-slate-900/60 hover:text-slate-200"
|
|
450
|
+
@click="group.footerLink.onClick()"
|
|
451
|
+
>
|
|
452
|
+
<UIcon :name="group.footerLink.icon" class="h-3.5 w-3.5 shrink-0" />
|
|
453
|
+
<span class="flex-1 truncate">{{ group.footerLink.label }}</span>
|
|
454
|
+
<span v-if="group.footerLink.status" class="shrink-0 text-slate-500">{{
|
|
455
|
+
group.footerLink.status
|
|
456
|
+
}}</span>
|
|
457
|
+
<UIcon name="i-lucide-chevron-right" class="h-3.5 w-3.5 shrink-0 text-slate-600" />
|
|
458
|
+
</button>
|
|
311
459
|
</section>
|
|
312
460
|
</div>
|
|
313
461
|
</template>
|
|
@@ -36,6 +36,9 @@ const META: Record<Notification['type'], { icon: string; color: Accent; action:
|
|
|
36
36
|
// Clicking the title opens the human-testing window for the task (see `reveal`); "act" just
|
|
37
37
|
// marks it read (the gate is resolved in that window — confirm / request a fix — not here).
|
|
38
38
|
human_test_ready: { icon: 'i-lucide-user-check', color: 'primary', action: 'Mark read' },
|
|
39
|
+
// Clicking the title opens the task's gate window (where the human can request a freeform
|
|
40
|
+
// fix); "act" just marks it read (approval happens on GitHub, not here).
|
|
41
|
+
human_review: { icon: 'i-lucide-users', color: 'primary', action: 'Mark read' },
|
|
39
42
|
// Clicking the title opens the Follow-up companion window for the run (see `reveal`); "act"
|
|
40
43
|
// just marks it read (items are decided in that window — file / send back / answer — not here).
|
|
41
44
|
followup_pending: { icon: 'i-lucide-compass', color: 'warning', action: 'Mark read' },
|
|
@@ -84,10 +87,23 @@ function reveal(n: Notification) {
|
|
|
84
87
|
else if (n.type === 'clarity_review') ui.openClarityReview(n.blockId)
|
|
85
88
|
else if (n.type === 'decision_required') revealDecision(n)
|
|
86
89
|
else if (n.type === 'human_test_ready') revealHumanTest(n)
|
|
90
|
+
else if (n.type === 'human_review') revealHumanReview(n)
|
|
87
91
|
else if (n.type === 'followup_pending') revealFollowUps(n)
|
|
88
92
|
else ui.select(n.blockId)
|
|
89
93
|
}
|
|
90
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Open the gate window for a parked `human-review` gate: find the run's human-review step and
|
|
97
|
+
* open it through the universal step dispatch (its archetype declares the `gate` result view,
|
|
98
|
+
* where the human can request a freeform fix). Falls back to focusing the block.
|
|
99
|
+
*/
|
|
100
|
+
function revealHumanReview(n: Notification) {
|
|
101
|
+
const instance = n.executionId ? execution.getInstance(n.executionId) : undefined
|
|
102
|
+
const idx = instance?.steps.findIndex((s) => s.agentKind === 'human-review') ?? -1
|
|
103
|
+
if (instance && idx >= 0) ui.openStepDetail(instance.id, idx)
|
|
104
|
+
else if (n.blockId) ui.select(n.blockId)
|
|
105
|
+
}
|
|
106
|
+
|
|
91
107
|
/**
|
|
92
108
|
* Open the Follow-up companion window for a run whose Coder parked on undecided items.
|
|
93
109
|
* Falls back to focusing the block when the run isn't loaded.
|