@cat-factory/app 0.9.0 → 0.10.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/board/AddTaskModal.vue +209 -21
- package/app/components/board/nodes/BlockNode.vue +2 -2
- package/app/components/focus/BlockFocusView.vue +2 -2
- package/app/components/layout/CommandBar.vue +7 -33
- package/app/components/layout/IntegrationsHub.vue +230 -0
- package/app/components/layout/SideBar.vue +8 -170
- package/app/components/panels/GenericStructuredResultView.vue +131 -0
- package/app/components/panels/InspectorPanel.vue +6 -2
- package/app/components/panels/StepResultViewHost.vue +4 -0
- package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +148 -0
- package/app/components/settings/IssueTrackerWritebackPanel.vue +45 -57
- package/app/components/settings/MergeThresholdsPanel.vue +189 -226
- package/app/components/settings/ObservabilityConnectionPanel.vue +151 -0
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +46 -61
- package/app/components/settings/WorkspaceSettingsPanel.vue +136 -63
- package/app/composables/api/releaseHealth.ts +11 -10
- package/app/pages/index.vue +4 -8
- package/app/stores/agents.ts +27 -2
- package/app/stores/releaseHealth.ts +48 -12
- package/app/stores/ui.ts +34 -42
- package/app/stores/workspace.ts +4 -0
- package/app/types/domain.ts +33 -1
- package/app/types/execution.ts +6 -0
- package/app/types/releaseHealth.ts +19 -11
- package/app/utils/catalog.spec.ts +10 -0
- package/app/utils/catalog.ts +20 -6
- package/package.json +2 -2
- package/app/components/board/ContextPicker.vue +0 -367
- package/app/components/settings/DatadogPanel.vue +0 -213
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
// task lands in `planned` state; it is never launched here. The user starts a
|
|
5
5
|
// pipeline on it explicitly (and can keep editing it until they do).
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
7
|
+
// The form also shows ungated "Context documents" / "Context issues" sections
|
|
8
|
+
// (mirroring the task inspector): pick already-imported docs/issues — or open the
|
|
9
|
+
// import flow — to attach as agent context. When the relevant integration isn't
|
|
10
|
+
// connected the Attach button is disabled with a hint. Linking needs the block id,
|
|
11
|
+
// so chosen items are staged locally and import-and-linked once the task is created
|
|
12
|
+
// (see useContextLinking) — the same context the agents see for every step of the run.
|
|
13
|
+
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
14
14
|
import type { CreateTaskType, TaskTypeFields } from '~/types/domain'
|
|
15
15
|
|
|
16
16
|
const ui = useUiStore()
|
|
@@ -179,13 +179,83 @@ function setConfig(id: string, value: string) {
|
|
|
179
179
|
agentConfigValues.value = { ...agentConfigValues.value, [id]: value }
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
// Context the user chose to attach to the new task (
|
|
183
|
-
//
|
|
182
|
+
// Context the user chose to attach to the new task (already-imported items + the
|
|
183
|
+
// import flow), committed once the block exists (see add() → linkPending).
|
|
184
184
|
const pendingContext = ref<PendingContext[]>([])
|
|
185
185
|
|
|
186
|
-
// The
|
|
187
|
-
//
|
|
188
|
-
|
|
186
|
+
// The Context documents / Context issues sections mirror the task inspector but are
|
|
187
|
+
// always shown (ungated): when the relevant integration isn't connected the Attach
|
|
188
|
+
// button is disabled with a tooltip rather than the section being hidden.
|
|
189
|
+
const docsConnected = computed(() => documents.available && documents.anyConnected)
|
|
190
|
+
const issuesConnected = computed(() => tasks.available && tasks.anyConnected)
|
|
191
|
+
const pendingDocs = computed(() => pendingContext.value.filter((c) => c.kind === 'document'))
|
|
192
|
+
const pendingIssues = computed(() => pendingContext.value.filter((c) => c.kind === 'task'))
|
|
193
|
+
|
|
194
|
+
function addPending(item: PendingContext) {
|
|
195
|
+
if (pendingContext.value.some((c) => contextKey(c) === contextKey(item))) return
|
|
196
|
+
pendingContext.value = [...pendingContext.value, item]
|
|
197
|
+
}
|
|
198
|
+
function removePending(item: PendingContext) {
|
|
199
|
+
pendingContext.value = pendingContext.value.filter((c) => contextKey(c) !== contextKey(item))
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Attach menus: already-imported items not yet chosen, plus the import entry — the
|
|
203
|
+
// same affordances the inspector offers. Picking one stages it locally (it links
|
|
204
|
+
// after the task is created).
|
|
205
|
+
const docAttachMenu = computed<DropdownMenuItem[][]>(() => {
|
|
206
|
+
const chosen = new Set(pendingContext.value.map(contextKey))
|
|
207
|
+
const items: DropdownMenuItem[] = documents.documents
|
|
208
|
+
.filter(
|
|
209
|
+
(d) =>
|
|
210
|
+
!chosen.has(contextKey({ kind: 'document', source: d.source, externalId: d.externalId })),
|
|
211
|
+
)
|
|
212
|
+
.map((d) => ({
|
|
213
|
+
label: d.title,
|
|
214
|
+
icon: documents.descriptorFor(d.source)?.icon ?? 'i-lucide-file-text',
|
|
215
|
+
onSelect: () =>
|
|
216
|
+
addPending({
|
|
217
|
+
kind: 'document',
|
|
218
|
+
source: d.source,
|
|
219
|
+
externalId: d.externalId,
|
|
220
|
+
title: d.title,
|
|
221
|
+
icon: documents.descriptorFor(d.source)?.icon ?? 'i-lucide-file-text',
|
|
222
|
+
needsImport: false,
|
|
223
|
+
}),
|
|
224
|
+
}))
|
|
225
|
+
items.push({
|
|
226
|
+
label: 'Import a page…',
|
|
227
|
+
icon: 'i-lucide-file-down',
|
|
228
|
+
onSelect: () => ui.openDocumentImport(null),
|
|
229
|
+
})
|
|
230
|
+
return [items]
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
const issueAttachMenu = computed<DropdownMenuItem[][]>(() => {
|
|
234
|
+
const chosen = new Set(pendingContext.value.map(contextKey))
|
|
235
|
+
const items: DropdownMenuItem[] = tasks.tasks
|
|
236
|
+
.filter(
|
|
237
|
+
(t) => !chosen.has(contextKey({ kind: 'task', source: t.source, externalId: t.externalId })),
|
|
238
|
+
)
|
|
239
|
+
.map((t) => ({
|
|
240
|
+
label: `${t.externalId} · ${t.title}`,
|
|
241
|
+
icon: tasks.descriptorFor(t.source)?.icon ?? 'i-lucide-square-check',
|
|
242
|
+
onSelect: () =>
|
|
243
|
+
addPending({
|
|
244
|
+
kind: 'task',
|
|
245
|
+
source: t.source,
|
|
246
|
+
externalId: t.externalId,
|
|
247
|
+
title: `${t.externalId} · ${t.title}`,
|
|
248
|
+
icon: tasks.descriptorFor(t.source)?.icon ?? 'i-lucide-square-check',
|
|
249
|
+
needsImport: false,
|
|
250
|
+
}),
|
|
251
|
+
}))
|
|
252
|
+
items.push({
|
|
253
|
+
label: 'Import an issue…',
|
|
254
|
+
icon: 'i-lucide-file-down',
|
|
255
|
+
onSelect: () => ui.openTaskImport(),
|
|
256
|
+
})
|
|
257
|
+
return [items]
|
|
258
|
+
})
|
|
189
259
|
|
|
190
260
|
// Reset the form whenever the modal opens for a (new) container, and refresh the
|
|
191
261
|
// imported docs/issues so the quick-pick list is current.
|
|
@@ -450,16 +520,134 @@ async function add() {
|
|
|
450
520
|
</div>
|
|
451
521
|
</div>
|
|
452
522
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
523
|
+
<!-- Context documents (ungated; Attach disabled until a source is connected). -->
|
|
524
|
+
<div class="space-y-2">
|
|
525
|
+
<div class="flex items-center justify-between">
|
|
526
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
527
|
+
Context documents
|
|
528
|
+
</span>
|
|
529
|
+
<UDropdownMenu
|
|
530
|
+
v-if="docsConnected"
|
|
531
|
+
:items="docAttachMenu"
|
|
532
|
+
:content="{ side: 'bottom', align: 'end' }"
|
|
533
|
+
>
|
|
534
|
+
<UButton color="neutral" variant="soft" size="xs" icon="i-lucide-plus">
|
|
535
|
+
Attach
|
|
536
|
+
</UButton>
|
|
537
|
+
</UDropdownMenu>
|
|
538
|
+
<UButton
|
|
539
|
+
v-else
|
|
540
|
+
color="neutral"
|
|
541
|
+
variant="soft"
|
|
542
|
+
size="xs"
|
|
543
|
+
icon="i-lucide-plus"
|
|
544
|
+
disabled
|
|
545
|
+
:title="
|
|
546
|
+
documents.available
|
|
547
|
+
? 'Connect a document source first (Integrations)'
|
|
548
|
+
: 'Enable the documents integration first'
|
|
549
|
+
"
|
|
550
|
+
>
|
|
551
|
+
Attach
|
|
552
|
+
</UButton>
|
|
553
|
+
</div>
|
|
554
|
+
<div v-if="pendingDocs.length" class="space-y-1">
|
|
555
|
+
<div
|
|
556
|
+
v-for="item in pendingDocs"
|
|
557
|
+
:key="contextKey(item)"
|
|
558
|
+
class="flex items-center gap-1.5 rounded-md border border-slate-800 bg-slate-900/60 px-2 py-1.5 text-xs text-slate-300"
|
|
559
|
+
>
|
|
560
|
+
<UIcon
|
|
561
|
+
:name="item.icon ?? 'i-lucide-file-text'"
|
|
562
|
+
class="h-3.5 w-3.5 shrink-0 text-indigo-400"
|
|
563
|
+
/>
|
|
564
|
+
<span class="truncate">{{ item.title }}</span>
|
|
565
|
+
<UBadge
|
|
566
|
+
v-if="item.needsImport"
|
|
567
|
+
color="neutral"
|
|
568
|
+
variant="soft"
|
|
569
|
+
size="xs"
|
|
570
|
+
class="ml-1 shrink-0"
|
|
571
|
+
>
|
|
572
|
+
imports on add
|
|
573
|
+
</UBadge>
|
|
574
|
+
<button
|
|
575
|
+
type="button"
|
|
576
|
+
class="ml-auto shrink-0 text-slate-400 hover:text-slate-200"
|
|
577
|
+
@click="removePending(item)"
|
|
578
|
+
>
|
|
579
|
+
<UIcon name="i-lucide-x" class="h-3.5 w-3.5" />
|
|
580
|
+
</button>
|
|
581
|
+
</div>
|
|
582
|
+
</div>
|
|
583
|
+
<p v-else class="text-[11px] text-slate-500">
|
|
584
|
+
Attach a requirement, RFC or PRD so agents see it while implementing this task.
|
|
585
|
+
</p>
|
|
586
|
+
</div>
|
|
459
587
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
588
|
+
<!-- Context issues (ungated; Attach disabled until a tracker is connected). -->
|
|
589
|
+
<div class="space-y-2">
|
|
590
|
+
<div class="flex items-center justify-between">
|
|
591
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
592
|
+
Context issues
|
|
593
|
+
</span>
|
|
594
|
+
<UDropdownMenu
|
|
595
|
+
v-if="issuesConnected"
|
|
596
|
+
:items="issueAttachMenu"
|
|
597
|
+
:content="{ side: 'bottom', align: 'end' }"
|
|
598
|
+
>
|
|
599
|
+
<UButton color="neutral" variant="soft" size="xs" icon="i-lucide-plus">
|
|
600
|
+
Attach
|
|
601
|
+
</UButton>
|
|
602
|
+
</UDropdownMenu>
|
|
603
|
+
<UButton
|
|
604
|
+
v-else
|
|
605
|
+
color="neutral"
|
|
606
|
+
variant="soft"
|
|
607
|
+
size="xs"
|
|
608
|
+
icon="i-lucide-plus"
|
|
609
|
+
disabled
|
|
610
|
+
:title="
|
|
611
|
+
tasks.available
|
|
612
|
+
? 'Connect an issue tracker first (Integrations)'
|
|
613
|
+
: 'Enable the issue-tracker integration first'
|
|
614
|
+
"
|
|
615
|
+
>
|
|
616
|
+
Attach
|
|
617
|
+
</UButton>
|
|
618
|
+
</div>
|
|
619
|
+
<div v-if="pendingIssues.length" class="space-y-1">
|
|
620
|
+
<div
|
|
621
|
+
v-for="item in pendingIssues"
|
|
622
|
+
:key="contextKey(item)"
|
|
623
|
+
class="flex items-center gap-1.5 rounded-md border border-slate-800 bg-slate-900/60 px-2 py-1.5 text-xs text-slate-300"
|
|
624
|
+
>
|
|
625
|
+
<UIcon
|
|
626
|
+
:name="item.icon ?? 'i-lucide-square-check'"
|
|
627
|
+
class="h-3.5 w-3.5 shrink-0 text-indigo-400"
|
|
628
|
+
/>
|
|
629
|
+
<span class="truncate">{{ item.title }}</span>
|
|
630
|
+
<UBadge
|
|
631
|
+
v-if="item.needsImport"
|
|
632
|
+
color="neutral"
|
|
633
|
+
variant="soft"
|
|
634
|
+
size="xs"
|
|
635
|
+
class="ml-1 shrink-0"
|
|
636
|
+
>
|
|
637
|
+
imports on add
|
|
638
|
+
</UBadge>
|
|
639
|
+
<button
|
|
640
|
+
type="button"
|
|
641
|
+
class="ml-auto shrink-0 text-slate-400 hover:text-slate-200"
|
|
642
|
+
@click="removePending(item)"
|
|
643
|
+
>
|
|
644
|
+
<UIcon name="i-lucide-x" class="h-3.5 w-3.5" />
|
|
645
|
+
</button>
|
|
646
|
+
</div>
|
|
647
|
+
</div>
|
|
648
|
+
<p v-else class="text-[11px] text-slate-500">
|
|
649
|
+
Attach a tracker issue so agents see its description and comments while implementing
|
|
650
|
+
this task.
|
|
463
651
|
</p>
|
|
464
652
|
</div>
|
|
465
653
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { Block, BlockStatus } from '~/types/domain'
|
|
3
|
-
import {
|
|
3
|
+
import { blockTypeMeta, STATUS_META } from '~/utils/catalog'
|
|
4
4
|
import DecisionBadge from './DecisionBadge.vue'
|
|
5
5
|
import DraggableTask from './DraggableTask.vue'
|
|
6
6
|
import ModuleFrame from './ModuleFrame.vue'
|
|
@@ -24,7 +24,7 @@ const { lod } = useSemanticZoom()
|
|
|
24
24
|
const block = computed<Block | undefined>(() => board.getBlock(props.id))
|
|
25
25
|
/** This service frame is mounted on more than one board in the org. */
|
|
26
26
|
const isShared = computed(() => services.isSharedFrame(props.id))
|
|
27
|
-
const typeMeta = computed(() => (block.value ?
|
|
27
|
+
const typeMeta = computed(() => (block.value ? blockTypeMeta(block.value.type) : null))
|
|
28
28
|
|
|
29
29
|
// ---- this service's children (tasks + modules) -----------------------------
|
|
30
30
|
const directTasks = computed(() => board.tasksOf(props.id))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { onKeyStroke } from '@vueuse/core'
|
|
3
3
|
import type { Block } from '~/types/domain'
|
|
4
|
-
import {
|
|
4
|
+
import { blockTypeMeta, STATUS_META } from '~/utils/catalog'
|
|
5
5
|
import PipelineProgress from '~/components/pipeline/PipelineProgress.vue'
|
|
6
6
|
|
|
7
7
|
const board = useBoardStore()
|
|
@@ -18,7 +18,7 @@ const block = computed<Block | undefined>(() =>
|
|
|
18
18
|
)
|
|
19
19
|
const instance = computed(() => execution.getInstance(block.value?.executionId))
|
|
20
20
|
const statusMeta = computed(() => (block.value ? STATUS_META[block.value.status] : null))
|
|
21
|
-
const typeMeta = computed(() => (block.value ?
|
|
21
|
+
const typeMeta = computed(() => (block.value ? blockTypeMeta(block.value.type) : null))
|
|
22
22
|
|
|
23
23
|
const deps = computed(() =>
|
|
24
24
|
(block.value?.dependsOn ?? []).map((id) => board.getBlock(id)).filter((b): b is Block => !!b),
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// The command bar (⌘K / Ctrl+K) — a searchable launcher for every action that
|
|
3
|
-
// used to live as a button or draggable in the left panel. It is
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// the
|
|
7
|
-
|
|
8
|
-
import { BLOCK_TYPE_META } from '~/utils/catalog'
|
|
3
|
+
// used to live as a button or draggable in the left panel. It is a fast path to
|
|
4
|
+
// pipelines, repositories, every integration and the settings surfaces. (Raw
|
|
5
|
+
// block creation is gone — services come from Bootstrap / Add-from-repo and tasks
|
|
6
|
+
// from the add-task flow.) Commands are assembled from the live stores so only
|
|
7
|
+
// available actions (connected integrations, etc.) show.
|
|
9
8
|
|
|
10
9
|
interface Command {
|
|
11
10
|
id: string
|
|
@@ -18,7 +17,6 @@ interface Command {
|
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
const ui = useUiStore()
|
|
21
|
-
const board = useBoardStore()
|
|
22
20
|
const github = useGitHubStore()
|
|
23
21
|
const slack = useSlackStore()
|
|
24
22
|
const documents = useDocumentsStore()
|
|
@@ -33,19 +31,6 @@ const open = computed({
|
|
|
33
31
|
const query = ref('')
|
|
34
32
|
const activeIndex = ref(0)
|
|
35
33
|
|
|
36
|
-
// New top-level blocks are created without a drop position now, so stagger each
|
|
37
|
-
// one slightly off the canvas origin to keep them from stacking exactly.
|
|
38
|
-
let spawnCount = 0
|
|
39
|
-
function spawnPosition() {
|
|
40
|
-
const offset = (spawnCount++ % 6) * 28
|
|
41
|
-
return { x: 160 + offset, y: 160 + offset }
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function addBlock(type: BlockType) {
|
|
45
|
-
const block = await board.addBlock(type, spawnPosition())
|
|
46
|
-
ui.select(block.id)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
34
|
const commands = computed<Command[]>(() => {
|
|
50
35
|
const list: Command[] = []
|
|
51
36
|
|
|
@@ -58,17 +43,6 @@ const commands = computed<Command[]>(() => {
|
|
|
58
43
|
keywords: 'pipeline agents chain',
|
|
59
44
|
run: () => ui.openBuilder(),
|
|
60
45
|
})
|
|
61
|
-
for (const type of Object.keys(BLOCK_TYPE_META) as BlockType[]) {
|
|
62
|
-
const meta = BLOCK_TYPE_META[type]
|
|
63
|
-
list.push({
|
|
64
|
-
id: `add-block-${type}`,
|
|
65
|
-
label: `Add ${meta.label} block`,
|
|
66
|
-
group: 'Create',
|
|
67
|
-
icon: meta.icon,
|
|
68
|
-
keywords: 'block frame service create new',
|
|
69
|
-
run: () => addBlock(type),
|
|
70
|
-
})
|
|
71
|
-
}
|
|
72
46
|
|
|
73
47
|
// ---- Repositories -------------------------------------------------------
|
|
74
48
|
if (github.available) {
|
|
@@ -173,7 +147,7 @@ const commands = computed<Command[]>(() => {
|
|
|
173
147
|
group: 'Workspace',
|
|
174
148
|
icon: 'i-lucide-git-merge',
|
|
175
149
|
keywords: 'merge policy preset auto-merge ci',
|
|
176
|
-
run: () => ui.
|
|
150
|
+
run: () => ui.openWorkspaceSettings('merge'),
|
|
177
151
|
})
|
|
178
152
|
list.push({
|
|
179
153
|
id: 'workspace-settings',
|
|
@@ -197,7 +171,7 @@ const commands = computed<Command[]>(() => {
|
|
|
197
171
|
group: 'Workspace',
|
|
198
172
|
icon: 'i-lucide-book-open-check',
|
|
199
173
|
keywords: 'fragment best practice guideline service default code-aware',
|
|
200
|
-
run: () => ui.
|
|
174
|
+
run: () => ui.openWorkspaceSettings('fragments'),
|
|
201
175
|
})
|
|
202
176
|
list.push({
|
|
203
177
|
id: 'local-models',
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The Integrations hub: a single modal that lists every external system the
|
|
3
|
+
// workspace can enable or link in — replacing the per-integration buttons that
|
|
4
|
+
// used to clutter the left navbar. Each row reuses the existing per-integration
|
|
5
|
+
// panel handlers on the `ui` store (so the integrations themselves are unchanged);
|
|
6
|
+
// opening one closes the hub and reveals that integration's own panel/modal.
|
|
7
|
+
//
|
|
8
|
+
// Sections gate on the same `available` probes the navbar used, so a system that
|
|
9
|
+
// the backend has turned off simply doesn't appear here.
|
|
10
|
+
const ui = useUiStore()
|
|
11
|
+
const github = useGitHubStore()
|
|
12
|
+
const slack = useSlackStore()
|
|
13
|
+
const documents = useDocumentsStore()
|
|
14
|
+
const tasks = useTasksStore()
|
|
15
|
+
const releaseHealth = useReleaseHealthStore()
|
|
16
|
+
|
|
17
|
+
// The observability connection status drives the hub's connected badge. Load it
|
|
18
|
+
// lazily when the hub opens (the secret-less connection view is cheap).
|
|
19
|
+
watch(
|
|
20
|
+
() => ui.integrationsOpen,
|
|
21
|
+
(isOpen) => {
|
|
22
|
+
if (isOpen) void releaseHealth.ensureLoaded().catch(() => {})
|
|
23
|
+
},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const open = computed({
|
|
27
|
+
get: () => ui.integrationsOpen,
|
|
28
|
+
set: (v: boolean) => (v ? ui.openIntegrations() : ui.closeIntegrations()),
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// One integration row. `status` is the connected-state line shown under the label
|
|
32
|
+
// (an account/team name, "Connected", or a hint); `connected` drives the badge.
|
|
33
|
+
interface IntegrationItem {
|
|
34
|
+
key: string
|
|
35
|
+
icon: string
|
|
36
|
+
label: string
|
|
37
|
+
description: string
|
|
38
|
+
status?: string
|
|
39
|
+
connected?: boolean
|
|
40
|
+
onClick: () => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface IntegrationGroup {
|
|
44
|
+
title: string
|
|
45
|
+
items: IntegrationItem[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Run an integration's open handler, then dismiss the hub so its panel takes over.
|
|
49
|
+
function go(fn: () => void) {
|
|
50
|
+
fn()
|
|
51
|
+
ui.closeIntegrations()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const groups = computed<IntegrationGroup[]>(() => {
|
|
55
|
+
const out: IntegrationGroup[] = []
|
|
56
|
+
|
|
57
|
+
// --- Source control --------------------------------------------------------
|
|
58
|
+
const code: IntegrationItem[] = []
|
|
59
|
+
if (github.available) {
|
|
60
|
+
code.push({
|
|
61
|
+
key: 'github',
|
|
62
|
+
icon: 'i-lucide-github',
|
|
63
|
+
label: 'GitHub',
|
|
64
|
+
description: 'Connect the workspace’s GitHub App, browse repos, PRs and issues.',
|
|
65
|
+
status: github.connected ? github.connection?.accountLogin : undefined,
|
|
66
|
+
connected: github.connected,
|
|
67
|
+
onClick: () => go(ui.openGitHub),
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
if (code.length) out.push({ title: 'Source control', items: code })
|
|
71
|
+
|
|
72
|
+
// --- Communication ---------------------------------------------------------
|
|
73
|
+
const comms: IntegrationItem[] = []
|
|
74
|
+
if (slack.available) {
|
|
75
|
+
comms.push({
|
|
76
|
+
key: 'slack',
|
|
77
|
+
icon: 'i-lucide-slack',
|
|
78
|
+
label: 'Slack',
|
|
79
|
+
description: 'Route notifications to your team’s Slack workspace.',
|
|
80
|
+
status: slack.connected ? slack.connection?.teamName : undefined,
|
|
81
|
+
connected: slack.connected,
|
|
82
|
+
onClick: () => go(ui.openSlack),
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
if (comms.length) out.push({ title: 'Communication', items: comms })
|
|
86
|
+
|
|
87
|
+
// --- Documents (dynamic sources: Confluence / Notion / GitHub) -------------
|
|
88
|
+
if (documents.available && documents.sources.length) {
|
|
89
|
+
const docs: IntegrationItem[] = documents.sources.map((src) => ({
|
|
90
|
+
key: `doc:${src.source}`,
|
|
91
|
+
icon: src.icon,
|
|
92
|
+
label: src.label,
|
|
93
|
+
description: `Link ${src.label} as a document source for requirements context.`,
|
|
94
|
+
status: documents.isConnected(src.source) ? 'Connected' : undefined,
|
|
95
|
+
connected: documents.isConnected(src.source),
|
|
96
|
+
onClick: () => go(() => ui.openDocumentConnect(src.source)),
|
|
97
|
+
}))
|
|
98
|
+
if (documents.anyConnected) {
|
|
99
|
+
docs.push({
|
|
100
|
+
key: 'doc:import',
|
|
101
|
+
icon: 'i-lucide-file-down',
|
|
102
|
+
label: 'Import & spawn',
|
|
103
|
+
description: 'Pull documents from a connected source and spawn structure.',
|
|
104
|
+
onClick: () => go(() => ui.openDocumentImport(null)),
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
out.push({ title: 'Documents', items: docs })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- Task trackers (dynamic sources: Jira / GitHub) ------------------------
|
|
111
|
+
if (tasks.available && tasks.sources.length) {
|
|
112
|
+
const trackers: IntegrationItem[] = tasks.sources.map((src) => ({
|
|
113
|
+
key: `task:${src.source}`,
|
|
114
|
+
icon: src.icon,
|
|
115
|
+
label: src.label,
|
|
116
|
+
description: `Link ${src.label} to import and reference tracker issues.`,
|
|
117
|
+
status: tasks.isConnected(src.source) ? 'Connected' : undefined,
|
|
118
|
+
connected: tasks.isConnected(src.source),
|
|
119
|
+
onClick: () => go(() => ui.openTaskConnect(src.source)),
|
|
120
|
+
}))
|
|
121
|
+
if (tasks.anyConnected) {
|
|
122
|
+
trackers.push({
|
|
123
|
+
key: 'task:import',
|
|
124
|
+
icon: 'i-lucide-file-down',
|
|
125
|
+
label: 'Import issues',
|
|
126
|
+
description: 'Pull issues from a connected tracker onto the board.',
|
|
127
|
+
onClick: () => go(() => ui.openTaskImport(null)),
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
trackers.push({
|
|
131
|
+
key: 'task:writeback',
|
|
132
|
+
icon: 'i-lucide-message-square-reply',
|
|
133
|
+
label: 'Issue tracker writeback',
|
|
134
|
+
description: 'Comment on the PR and close the linked issue on merge.',
|
|
135
|
+
onClick: () => go(() => ui.openWorkspaceSettings('writeback')),
|
|
136
|
+
})
|
|
137
|
+
out.push({ title: 'Task trackers', items: trackers })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- Observability ---------------------------------------------------------
|
|
141
|
+
// Gated like every other backend-toggleable system: hidden until a probe confirms
|
|
142
|
+
// the observability module is enabled (`available === true`), so a disabled backend
|
|
143
|
+
// doesn't show a dead "Connect" row that only 503s.
|
|
144
|
+
if (releaseHealth.available) {
|
|
145
|
+
out.push({
|
|
146
|
+
title: 'Observability',
|
|
147
|
+
items: [
|
|
148
|
+
{
|
|
149
|
+
key: 'observability',
|
|
150
|
+
icon: 'i-lucide-activity',
|
|
151
|
+
label: 'Post-release health',
|
|
152
|
+
description: 'Watch monitors and SLOs after a release ships (Datadog).',
|
|
153
|
+
status: releaseHealth.connection.connected ? 'Connected' : undefined,
|
|
154
|
+
connected: releaseHealth.connection.connected,
|
|
155
|
+
onClick: () => go(ui.openObservabilityConnection),
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// --- Models & providers ----------------------------------------------------
|
|
162
|
+
out.push({
|
|
163
|
+
title: 'Models & providers',
|
|
164
|
+
items: [
|
|
165
|
+
{
|
|
166
|
+
key: 'vendors',
|
|
167
|
+
icon: 'i-lucide-key-round',
|
|
168
|
+
label: 'Vendors & keys',
|
|
169
|
+
description: 'LLM vendor subscriptions and provider API keys.',
|
|
170
|
+
onClick: () => go(ui.openVendorCredentials),
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
key: 'local-runners',
|
|
174
|
+
icon: 'i-lucide-server',
|
|
175
|
+
label: 'My local runners',
|
|
176
|
+
description: 'Your own-machine model runners (Ollama, LM Studio, vLLM…).',
|
|
177
|
+
onClick: () => go(ui.openLocalModels),
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
key: 'openrouter',
|
|
181
|
+
icon: 'i-lucide-waypoints',
|
|
182
|
+
label: 'OpenRouter models',
|
|
183
|
+
description: 'Browse and enable models from the OpenRouter gateway.',
|
|
184
|
+
onClick: () => go(ui.openOpenRouter),
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
return out
|
|
190
|
+
})
|
|
191
|
+
</script>
|
|
192
|
+
|
|
193
|
+
<template>
|
|
194
|
+
<UModal v-model:open="open" title="Integrations" :ui="{ content: 'max-w-xl' }">
|
|
195
|
+
<template #body>
|
|
196
|
+
<div class="space-y-5">
|
|
197
|
+
<p class="text-xs text-slate-400">
|
|
198
|
+
Connect and manage the external systems this workspace can link in.
|
|
199
|
+
</p>
|
|
200
|
+
|
|
201
|
+
<section v-for="group in groups" :key="group.title">
|
|
202
|
+
<h3 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
203
|
+
{{ group.title }}
|
|
204
|
+
</h3>
|
|
205
|
+
<div class="space-y-1.5">
|
|
206
|
+
<button
|
|
207
|
+
v-for="item in group.items"
|
|
208
|
+
:key="item.key"
|
|
209
|
+
type="button"
|
|
210
|
+
class="flex w-full items-center gap-3 rounded-lg border border-slate-700 bg-slate-800/40 px-3 py-2.5 text-left transition hover:border-slate-500 hover:bg-slate-800"
|
|
211
|
+
@click="item.onClick()"
|
|
212
|
+
>
|
|
213
|
+
<UIcon :name="item.icon" class="h-5 w-5 shrink-0 text-slate-300" />
|
|
214
|
+
<div class="min-w-0 flex-1">
|
|
215
|
+
<div class="flex items-center gap-2">
|
|
216
|
+
<span class="truncate text-sm font-medium text-slate-100">{{ item.label }}</span>
|
|
217
|
+
<UBadge v-if="item.connected" color="success" variant="subtle" size="sm">
|
|
218
|
+
{{ item.status || 'Connected' }}
|
|
219
|
+
</UBadge>
|
|
220
|
+
</div>
|
|
221
|
+
<p class="truncate text-xs text-slate-400">{{ item.description }}</p>
|
|
222
|
+
</div>
|
|
223
|
+
<UIcon name="i-lucide-chevron-right" class="h-4 w-4 shrink-0 text-slate-500" />
|
|
224
|
+
</button>
|
|
225
|
+
</div>
|
|
226
|
+
</section>
|
|
227
|
+
</div>
|
|
228
|
+
</template>
|
|
229
|
+
</UModal>
|
|
230
|
+
</template>
|