@cat-factory/app 0.6.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/LICENSE +21 -0
- package/README.md +88 -0
- package/app/app.config.ts +8 -0
- package/app/app.vue +11 -0
- package/app/assets/css/main.css +100 -0
- package/app/components/auth/AuthGate.vue +24 -0
- package/app/components/auth/LoginScreen.vue +143 -0
- package/app/components/auth/UserMenu.vue +39 -0
- package/app/components/board/AddTaskModal.vue +444 -0
- package/app/components/board/AgentFailureCard.vue +97 -0
- package/app/components/board/AgentStopButton.vue +61 -0
- package/app/components/board/BoardCanvas.vue +183 -0
- package/app/components/board/ContextPicker.vue +367 -0
- package/app/components/board/RecurringPipelineModal.vue +219 -0
- package/app/components/board/TaskDependencyEdges.vue +132 -0
- package/app/components/board/nodes/AgentChip.vue +59 -0
- package/app/components/board/nodes/BlockNode.vue +433 -0
- package/app/components/board/nodes/DecisionBadge.vue +27 -0
- package/app/components/board/nodes/DraggableTask.vue +48 -0
- package/app/components/board/nodes/ModuleFrame.vue +97 -0
- package/app/components/board/nodes/TaskCard.vue +359 -0
- package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
- package/app/components/bootstrap/BootstrapModal.vue +665 -0
- package/app/components/clarity/ClarityReviewWindow.vue +611 -0
- package/app/components/consensus/ConsensusSessionWindow.vue +210 -0
- package/app/components/documents/DocumentImportModal.vue +161 -0
- package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
- package/app/components/documents/SpawnPreviewModal.vue +161 -0
- package/app/components/documents/TaskContextDocs.vue +83 -0
- package/app/components/focus/BlockFocusView.vue +171 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
- package/app/components/gates/GateResultView.vue +282 -0
- package/app/components/github/AddServiceFromRepoModal.vue +354 -0
- package/app/components/github/GitHubConnect.vue +183 -0
- package/app/components/github/GitHubOnboarding.vue +45 -0
- package/app/components/github/GitHubPanel.vue +584 -0
- package/app/components/github/RepoTreeBrowser.vue +171 -0
- package/app/components/layout/AccountTeamSettings.vue +237 -0
- package/app/components/layout/BoardSwitcher.vue +280 -0
- package/app/components/layout/BoardToolbar.vue +156 -0
- package/app/components/layout/CommandBar.vue +336 -0
- package/app/components/layout/GitHubPatBanner.vue +73 -0
- package/app/components/layout/NotificationsInbox.vue +175 -0
- package/app/components/layout/SideBar.vue +314 -0
- package/app/components/layout/SpendWarningBanner.vue +107 -0
- package/app/components/observability/StepMetricsBar.vue +102 -0
- package/app/components/palettes/AgentPalette.vue +86 -0
- package/app/components/panels/AgentStepDetail.vue +737 -0
- package/app/components/panels/DecisionModal.vue +71 -0
- package/app/components/panels/InspectorPanel.vue +465 -0
- package/app/components/panels/ObservabilityPanel.vue +351 -0
- package/app/components/panels/StepMetadataCard.vue +253 -0
- package/app/components/panels/StepRestartControl.vue +90 -0
- package/app/components/panels/StepResultViewHost.vue +40 -0
- package/app/components/panels/StepTestReport.vue +84 -0
- package/app/components/panels/inspector/ContainerSummary.vue +74 -0
- package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
- package/app/components/panels/inspector/ServiceFragments.vue +82 -0
- package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
- package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
- package/app/components/panels/inspector/TaskDependencies.vue +70 -0
- package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
- package/app/components/panels/inspector/TaskExecution.vue +364 -0
- package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
- package/app/components/panels/inspector/TaskStructure.vue +96 -0
- package/app/components/pipeline/AgentKindIcon.vue +30 -0
- package/app/components/pipeline/IterationCapPrompt.vue +70 -0
- package/app/components/pipeline/PipelineBuilder.vue +817 -0
- package/app/components/pipeline/PipelineProgress.vue +484 -0
- package/app/components/providers/ApiKeysSection.vue +273 -0
- package/app/components/providers/PersonalCredentialModal.vue +128 -0
- package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
- package/app/components/providers/VendorCredentialsModal.vue +197 -0
- package/app/components/recurring/RecurrenceEditor.vue +124 -0
- package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
- package/app/components/settings/DatadogPanel.vue +213 -0
- package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
- package/app/components/settings/MergeThresholdsPanel.vue +378 -0
- package/app/components/settings/ModelDefaultsPanel.vue +250 -0
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
- package/app/components/slack/SlackPanel.vue +299 -0
- package/app/components/tasks/TaskContextIssues.vue +88 -0
- package/app/components/tasks/TaskImportModal.vue +207 -0
- package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
- package/app/components/testing/TestReportWindow.vue +404 -0
- package/app/composables/api/accounts.ts +81 -0
- package/app/composables/api/auth.ts +45 -0
- package/app/composables/api/board.ts +101 -0
- package/app/composables/api/bootstrap.ts +62 -0
- package/app/composables/api/context.ts +25 -0
- package/app/composables/api/documents.ts +74 -0
- package/app/composables/api/execution.ts +127 -0
- package/app/composables/api/fragments.ts +71 -0
- package/app/composables/api/github.ts +131 -0
- package/app/composables/api/models.ts +127 -0
- package/app/composables/api/notifications.ts +23 -0
- package/app/composables/api/presets.ts +29 -0
- package/app/composables/api/recurring.ts +68 -0
- package/app/composables/api/releaseHealth.ts +43 -0
- package/app/composables/api/reviews.ts +146 -0
- package/app/composables/api/slack.ts +54 -0
- package/app/composables/api/tasks.ts +72 -0
- package/app/composables/api/workspaces.ts +36 -0
- package/app/composables/useApi.ts +89 -0
- package/app/composables/useBlockDrag.ts +90 -0
- package/app/composables/useBlockQueries.ts +154 -0
- package/app/composables/useBoardFlow.ts +11 -0
- package/app/composables/useContextLinking.ts +65 -0
- package/app/composables/useDepLabels.ts +26 -0
- package/app/composables/useFrameResize.ts +54 -0
- package/app/composables/useResultView.ts +48 -0
- package/app/composables/useReviewStage.ts +40 -0
- package/app/composables/useSemanticZoom.ts +31 -0
- package/app/composables/useStepApproval.ts +233 -0
- package/app/composables/useStepProse.ts +78 -0
- package/app/composables/useStepTimer.ts +63 -0
- package/app/composables/useTaskExpansion.ts +92 -0
- package/app/composables/useWorkspaceStream.ts +155 -0
- package/app/docs/architecture.md +31 -0
- package/app/pages/index.vue +141 -0
- package/app/stores/accounts.ts +152 -0
- package/app/stores/agentConfig.ts +35 -0
- package/app/stores/agentRuns.ts +122 -0
- package/app/stores/agents.ts +40 -0
- package/app/stores/apiKeys.ts +108 -0
- package/app/stores/auth.ts +166 -0
- package/app/stores/board.spec.ts +205 -0
- package/app/stores/board.ts +286 -0
- package/app/stores/bootstrap.ts +97 -0
- package/app/stores/clarity.ts +196 -0
- package/app/stores/consensus.ts +60 -0
- package/app/stores/documents.ts +176 -0
- package/app/stores/execution.ts +273 -0
- package/app/stores/fragmentLibrary.ts +147 -0
- package/app/stores/fragments.ts +40 -0
- package/app/stores/github.ts +305 -0
- package/app/stores/localModels.ts +51 -0
- package/app/stores/mergePresets.ts +58 -0
- package/app/stores/modelDefaults.ts +76 -0
- package/app/stores/models.ts +134 -0
- package/app/stores/notifications.ts +70 -0
- package/app/stores/observability.ts +144 -0
- package/app/stores/personalSubscriptions.ts +215 -0
- package/app/stores/pipelines.ts +327 -0
- package/app/stores/recurringPipelines.ts +112 -0
- package/app/stores/releaseHealth.ts +75 -0
- package/app/stores/requirements.spec.ts +94 -0
- package/app/stores/requirements.ts +208 -0
- package/app/stores/serviceFragmentDefaults.ts +29 -0
- package/app/stores/services.ts +87 -0
- package/app/stores/slack.ts +142 -0
- package/app/stores/taskExpansion.ts +36 -0
- package/app/stores/tasks.spec.ts +71 -0
- package/app/stores/tasks.ts +176 -0
- package/app/stores/tracker.ts +27 -0
- package/app/stores/ui.ts +434 -0
- package/app/stores/vendorCredentials.ts +54 -0
- package/app/stores/workspace.ts +215 -0
- package/app/stores/workspaceSettings.ts +36 -0
- package/app/types/accounts.ts +77 -0
- package/app/types/bootstrap.ts +83 -0
- package/app/types/clarity.ts +59 -0
- package/app/types/consensus.ts +91 -0
- package/app/types/documents.ts +104 -0
- package/app/types/domain.ts +495 -0
- package/app/types/execution.ts +383 -0
- package/app/types/fragments.ts +72 -0
- package/app/types/github.ts +173 -0
- package/app/types/localModels.ts +73 -0
- package/app/types/merge.ts +71 -0
- package/app/types/models.ts +157 -0
- package/app/types/notifications.ts +74 -0
- package/app/types/recurring.ts +69 -0
- package/app/types/releaseHealth.ts +31 -0
- package/app/types/requirements.ts +61 -0
- package/app/types/services.ts +27 -0
- package/app/types/slack.ts +57 -0
- package/app/types/tasks.ts +82 -0
- package/app/types/tracker.ts +18 -0
- package/app/utils/agentOutput.spec.ts +128 -0
- package/app/utils/agentOutput.ts +173 -0
- package/app/utils/catalog.spec.ts +112 -0
- package/app/utils/catalog.ts +455 -0
- package/app/utils/dnd.ts +29 -0
- package/app/utils/observability.ts +52 -0
- package/app/utils/pipelineRender.ts +151 -0
- package/nuxt.config.ts +55 -0
- package/package.json +45 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Workspace settings: the default best-practice fragments NEW services inherit. The
|
|
3
|
+
// selection is drawn from the universal fragment pool (built-in + deployment-registered)
|
|
4
|
+
// served by GET /prompt-fragments. Changing it does not retroactively change existing
|
|
5
|
+
// services — each owns its selection from creation. Persisted via the
|
|
6
|
+
// serviceFragmentDefaults store (the backend replaces the whole list on each change).
|
|
7
|
+
import { ref } from 'vue'
|
|
8
|
+
|
|
9
|
+
const ui = useUiStore()
|
|
10
|
+
const fragments = useFragmentsStore()
|
|
11
|
+
const defaults = useServiceFragmentDefaultsStore()
|
|
12
|
+
const toast = useToast()
|
|
13
|
+
|
|
14
|
+
const open = computed({
|
|
15
|
+
get: () => ui.serviceFragmentDefaultsOpen,
|
|
16
|
+
set: (v: boolean) => (v ? ui.openServiceFragmentDefaults() : ui.closeServiceFragmentDefaults()),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const busy = ref(false)
|
|
20
|
+
|
|
21
|
+
watch(open, (isOpen) => {
|
|
22
|
+
if (isOpen) void fragments.ensureLoaded()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const selected = computed(() =>
|
|
26
|
+
defaults.fragmentIds
|
|
27
|
+
.map((id) => fragments.getFragment(id))
|
|
28
|
+
.filter((f): f is NonNullable<typeof f> => !!f),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
// Pool fragments not already in the default set, grouped by category.
|
|
32
|
+
const menu = computed(() => {
|
|
33
|
+
const chosen = new Set(defaults.fragmentIds)
|
|
34
|
+
const groups = new Map<string, { label: string; onSelect: () => void }[]>()
|
|
35
|
+
for (const f of fragments.fragments) {
|
|
36
|
+
if (chosen.has(f.id)) continue
|
|
37
|
+
const items = groups.get(f.category) ?? []
|
|
38
|
+
items.push({ label: f.title, onSelect: () => add(f.id) })
|
|
39
|
+
groups.set(f.category, items)
|
|
40
|
+
}
|
|
41
|
+
return [...groups.values()]
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
async function save(ids: string[]) {
|
|
45
|
+
busy.value = true
|
|
46
|
+
try {
|
|
47
|
+
await defaults.set(ids)
|
|
48
|
+
} catch (e) {
|
|
49
|
+
toast.add({
|
|
50
|
+
title: 'Could not save default fragments',
|
|
51
|
+
description: e instanceof Error ? e.message : String(e),
|
|
52
|
+
icon: 'i-lucide-triangle-alert',
|
|
53
|
+
color: 'error',
|
|
54
|
+
})
|
|
55
|
+
} finally {
|
|
56
|
+
busy.value = false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function add(id: string) {
|
|
61
|
+
if (defaults.fragmentIds.includes(id)) return
|
|
62
|
+
void save([...defaults.fragmentIds, id])
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function remove(id: string) {
|
|
66
|
+
void save(defaults.fragmentIds.filter((x) => x !== id))
|
|
67
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<template>
|
|
71
|
+
<UModal v-model:open="open" title="Default service best practices" :ui="{ content: 'max-w-2xl' }">
|
|
72
|
+
<template #body>
|
|
73
|
+
<div class="space-y-4">
|
|
74
|
+
<p class="text-xs text-slate-400">
|
|
75
|
+
Pick the best-practice fragments every <span class="text-slate-300">new</span> service
|
|
76
|
+
starts with. Their guidance is folded into the prompt of every
|
|
77
|
+
<span class="text-slate-300">code-aware</span> agent (coder, reviewer, architect, fixers)
|
|
78
|
+
on the service's tasks. You can refine the set per service in its inspector; changing this
|
|
79
|
+
default does not affect services that already exist.
|
|
80
|
+
</p>
|
|
81
|
+
|
|
82
|
+
<div class="flex items-center justify-between">
|
|
83
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
84
|
+
Default fragments
|
|
85
|
+
</span>
|
|
86
|
+
<UDropdownMenu
|
|
87
|
+
v-if="menu.length"
|
|
88
|
+
:items="menu"
|
|
89
|
+
:ui="{ content: 'max-h-72 overflow-y-auto' }"
|
|
90
|
+
>
|
|
91
|
+
<UButton
|
|
92
|
+
size="xs"
|
|
93
|
+
variant="ghost"
|
|
94
|
+
color="neutral"
|
|
95
|
+
icon="i-lucide-plus"
|
|
96
|
+
trailing-icon="i-lucide-chevron-down"
|
|
97
|
+
:loading="busy"
|
|
98
|
+
>
|
|
99
|
+
Add fragment
|
|
100
|
+
</UButton>
|
|
101
|
+
</UDropdownMenu>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div v-if="selected.length" class="flex flex-wrap gap-1">
|
|
105
|
+
<UBadge
|
|
106
|
+
v-for="f in selected"
|
|
107
|
+
:key="f.id"
|
|
108
|
+
color="primary"
|
|
109
|
+
variant="subtle"
|
|
110
|
+
size="sm"
|
|
111
|
+
class="cursor-pointer"
|
|
112
|
+
:title="f.summary"
|
|
113
|
+
@click="remove(f.id)"
|
|
114
|
+
>
|
|
115
|
+
{{ f.title }}<UIcon name="i-lucide-x" class="ml-0.5 h-3 w-3" />
|
|
116
|
+
</UBadge>
|
|
117
|
+
</div>
|
|
118
|
+
<p v-else class="text-[11px] text-slate-500">
|
|
119
|
+
None — new services start with no service-level fragments.
|
|
120
|
+
</p>
|
|
121
|
+
</div>
|
|
122
|
+
</template>
|
|
123
|
+
</UModal>
|
|
124
|
+
</template>
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Workspace settings: the run-timing escalation threshold and the per-service
|
|
3
|
+
// running-task limit policy.
|
|
4
|
+
// - waitingEscalationMinutes — runs never time out waiting for a human; after this
|
|
5
|
+
// long their notification escalates yellow → red ("Overdue") in the inbox.
|
|
6
|
+
// - task limit — cap how many tasks may run concurrently under one service, either
|
|
7
|
+
// as a single shared bucket across all types or one bucket per task type.
|
|
8
|
+
import { reactive, ref, watch } from 'vue'
|
|
9
|
+
import type { CreateTaskType, TaskLimitMode } from '~/types/domain'
|
|
10
|
+
|
|
11
|
+
const ui = useUiStore()
|
|
12
|
+
const store = useWorkspaceSettingsStore()
|
|
13
|
+
const toast = useToast()
|
|
14
|
+
|
|
15
|
+
const open = computed({
|
|
16
|
+
get: () => ui.workspaceSettingsOpen,
|
|
17
|
+
set: (v: boolean) => (v ? ui.openWorkspaceSettings() : ui.closeWorkspaceSettings()),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const TASK_TYPES: CreateTaskType[] = ['feature', 'bug', 'document', 'spike']
|
|
21
|
+
const MODES: { value: TaskLimitMode; label: string }[] = [
|
|
22
|
+
{ value: 'off', label: 'No limit' },
|
|
23
|
+
{ value: 'shared', label: 'Shared across all types' },
|
|
24
|
+
{ value: 'per_type', label: 'Per task type' },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
// Local editable copy, kept in sync with the store's settings.
|
|
28
|
+
const draft = reactive({
|
|
29
|
+
waitingEscalationMinutes: 120,
|
|
30
|
+
taskLimitMode: 'off' as TaskLimitMode,
|
|
31
|
+
taskLimitShared: 5 as number,
|
|
32
|
+
perType: {} as Record<CreateTaskType, number>,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
function hydrate() {
|
|
36
|
+
const s = store.settings
|
|
37
|
+
draft.waitingEscalationMinutes = s.waitingEscalationMinutes
|
|
38
|
+
draft.taskLimitMode = s.taskLimitMode
|
|
39
|
+
draft.taskLimitShared = s.taskLimitShared ?? 5
|
|
40
|
+
const pt = s.taskLimitPerType ?? {}
|
|
41
|
+
for (const t of TASK_TYPES) draft.perType[t] = pt[t] ?? 3
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
watch(() => store.settings, hydrate, { immediate: true, deep: true })
|
|
45
|
+
|
|
46
|
+
const saving = ref(false)
|
|
47
|
+
|
|
48
|
+
async function save() {
|
|
49
|
+
saving.value = true
|
|
50
|
+
try {
|
|
51
|
+
await store.update({
|
|
52
|
+
waitingEscalationMinutes: draft.waitingEscalationMinutes,
|
|
53
|
+
taskLimitMode: draft.taskLimitMode,
|
|
54
|
+
taskLimitShared: draft.taskLimitMode === 'shared' ? draft.taskLimitShared : null,
|
|
55
|
+
taskLimitPerType:
|
|
56
|
+
draft.taskLimitMode === 'per_type'
|
|
57
|
+
? TASK_TYPES.reduce(
|
|
58
|
+
(acc, t) => {
|
|
59
|
+
acc[t] = draft.perType[t]
|
|
60
|
+
return acc
|
|
61
|
+
},
|
|
62
|
+
{} as Record<CreateTaskType, number>,
|
|
63
|
+
)
|
|
64
|
+
: null,
|
|
65
|
+
})
|
|
66
|
+
toast.add({ title: 'Settings saved', icon: 'i-lucide-check', color: 'success' })
|
|
67
|
+
} catch (e) {
|
|
68
|
+
toast.add({
|
|
69
|
+
title: 'Could not save settings',
|
|
70
|
+
description: e instanceof Error ? e.message : String(e),
|
|
71
|
+
icon: 'i-lucide-triangle-alert',
|
|
72
|
+
color: 'error',
|
|
73
|
+
})
|
|
74
|
+
} finally {
|
|
75
|
+
saving.value = false
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<template>
|
|
81
|
+
<UModal v-model:open="open" title="Workspace settings" :ui="{ content: 'max-w-xl' }">
|
|
82
|
+
<template #body>
|
|
83
|
+
<div class="space-y-6">
|
|
84
|
+
<!-- Run-timing escalation -->
|
|
85
|
+
<section class="space-y-2">
|
|
86
|
+
<h3 class="text-sm font-semibold text-slate-200">Waiting for a human</h3>
|
|
87
|
+
<p class="text-[11px] text-slate-400">
|
|
88
|
+
A run parked on a human decision (a review, an approval, a merge) waits as long as it
|
|
89
|
+
needs — it is never cancelled. After this many minutes its notification turns red and is
|
|
90
|
+
flagged <span class="text-error-400">Overdue</span> in the inbox.
|
|
91
|
+
</p>
|
|
92
|
+
<label class="block w-48">
|
|
93
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
94
|
+
Escalate after (minutes)
|
|
95
|
+
</span>
|
|
96
|
+
<UInput
|
|
97
|
+
v-model.number="draft.waitingEscalationMinutes"
|
|
98
|
+
type="number"
|
|
99
|
+
:min="1"
|
|
100
|
+
size="sm"
|
|
101
|
+
/>
|
|
102
|
+
</label>
|
|
103
|
+
</section>
|
|
104
|
+
|
|
105
|
+
<!-- Per-service running-task limit -->
|
|
106
|
+
<section class="space-y-2">
|
|
107
|
+
<h3 class="text-sm font-semibold text-slate-200">Running tasks per service</h3>
|
|
108
|
+
<p class="text-[11px] text-slate-400">
|
|
109
|
+
Cap how many tasks may run at once under one service. Starting a task over the limit is
|
|
110
|
+
refused with a clear message until a running task finishes.
|
|
111
|
+
</p>
|
|
112
|
+
<label class="block w-64">
|
|
113
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">Mode</span>
|
|
114
|
+
<USelect v-model="draft.taskLimitMode" :items="MODES" value-key="value" size="sm" />
|
|
115
|
+
</label>
|
|
116
|
+
|
|
117
|
+
<label v-if="draft.taskLimitMode === 'shared'" class="block w-48">
|
|
118
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
119
|
+
Max running tasks
|
|
120
|
+
</span>
|
|
121
|
+
<UInput v-model.number="draft.taskLimitShared" type="number" :min="1" size="sm" />
|
|
122
|
+
</label>
|
|
123
|
+
|
|
124
|
+
<div v-else-if="draft.taskLimitMode === 'per_type'" class="grid grid-cols-2 gap-3">
|
|
125
|
+
<label v-for="t in TASK_TYPES" :key="t" class="block">
|
|
126
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
127
|
+
Max {{ t }} tasks
|
|
128
|
+
</span>
|
|
129
|
+
<UInput v-model.number="draft.perType[t]" type="number" :min="1" size="sm" />
|
|
130
|
+
</label>
|
|
131
|
+
</div>
|
|
132
|
+
</section>
|
|
133
|
+
|
|
134
|
+
<div class="flex justify-end">
|
|
135
|
+
<UButton color="primary" icon="i-lucide-save" size="sm" :loading="saving" @click="save">
|
|
136
|
+
Save
|
|
137
|
+
</UButton>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</template>
|
|
141
|
+
</UModal>
|
|
142
|
+
</template>
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Slack integration panel. Slack is an extra delivery transport for the existing
|
|
3
|
+
// notifications (merge_review / pipeline_complete / ci_failed). Three sections:
|
|
4
|
+
// - Connection (per-account): "Add to Slack" (OAuth) or paste a bot token.
|
|
5
|
+
// - Routing (per-workspace): per-type enable + channel.
|
|
6
|
+
// - Mentions (per-account): toggle + GitHub-user-id → Slack-member-id map.
|
|
7
|
+
import { computed, reactive, ref, watch } from 'vue'
|
|
8
|
+
import type { NotificationType } from '~/types/notifications'
|
|
9
|
+
import type { SlackMemberMappingEntry, SlackMemberRole, SlackRoute } from '~/types/slack'
|
|
10
|
+
|
|
11
|
+
const ui = useUiStore()
|
|
12
|
+
const slack = useSlackStore()
|
|
13
|
+
const toast = useToast()
|
|
14
|
+
|
|
15
|
+
const open = computed({
|
|
16
|
+
get: () => ui.slackOpen,
|
|
17
|
+
set: (v: boolean) => (v ? ui.openSlack() : ui.closeSlack()),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const ROUTABLE: { type: NotificationType; label: string }[] = [
|
|
21
|
+
{ type: 'merge_review', label: 'Merge review' },
|
|
22
|
+
{ type: 'pipeline_complete', label: 'Pipeline complete' },
|
|
23
|
+
{ type: 'ci_failed', label: 'CI failed' },
|
|
24
|
+
{ type: 'test_failed', label: 'Tests failed' },
|
|
25
|
+
{ type: 'requirement_review', label: 'Requirement review' },
|
|
26
|
+
{ type: 'clarity_review', label: 'Clarity review' },
|
|
27
|
+
{ type: 'release_regression', label: 'Release regression' },
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
/** Notification-role options for a mapped member (drives who gets @-mentioned). */
|
|
31
|
+
const ROLE_OPTIONS: SlackMemberRole[] = ['engineering', 'product']
|
|
32
|
+
|
|
33
|
+
// Local editable copies, synced from the store on load.
|
|
34
|
+
const routes = reactive<Record<NotificationType, SlackRoute>>({
|
|
35
|
+
merge_review: { enabled: false, channel: '' },
|
|
36
|
+
pipeline_complete: { enabled: false, channel: '' },
|
|
37
|
+
ci_failed: { enabled: false, channel: '' },
|
|
38
|
+
test_failed: { enabled: false, channel: '' },
|
|
39
|
+
requirement_review: { enabled: false, channel: '' },
|
|
40
|
+
clarity_review: { enabled: false, channel: '' },
|
|
41
|
+
release_regression: { enabled: false, channel: '' },
|
|
42
|
+
// In-app only (not in ROUTABLE), but the map is exhaustive over the type.
|
|
43
|
+
decision_required: { enabled: false, channel: '' },
|
|
44
|
+
})
|
|
45
|
+
const mentionsEnabled = ref(false)
|
|
46
|
+
const mapping = ref<SlackMemberMappingEntry[]>([])
|
|
47
|
+
const tokenInput = ref('')
|
|
48
|
+
const busy = ref(false)
|
|
49
|
+
|
|
50
|
+
function notifyError(title: string, e: unknown) {
|
|
51
|
+
toast.add({
|
|
52
|
+
title,
|
|
53
|
+
description: e instanceof Error ? e.message : String(e),
|
|
54
|
+
icon: 'i-lucide-triangle-alert',
|
|
55
|
+
color: 'error',
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Load everything the panel needs whenever it opens and Slack is connected.
|
|
60
|
+
watch(
|
|
61
|
+
() => open.value,
|
|
62
|
+
async (isOpen) => {
|
|
63
|
+
if (!isOpen || !slack.connected) return
|
|
64
|
+
try {
|
|
65
|
+
await Promise.all([slack.loadSettings(), slack.loadMemberMapping(), slack.loadChannels()])
|
|
66
|
+
for (const { type } of ROUTABLE) {
|
|
67
|
+
routes[type] = slack.settings?.routes[type] ?? { enabled: false, channel: '' }
|
|
68
|
+
}
|
|
69
|
+
mentionsEnabled.value = slack.settings?.mentionsEnabled ?? false
|
|
70
|
+
mapping.value = slack.memberMapping.map((e) => ({ role: 'engineering', ...e }))
|
|
71
|
+
} catch (e) {
|
|
72
|
+
notifyError('Could not load Slack settings', e)
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
async function connectViaOAuth() {
|
|
78
|
+
try {
|
|
79
|
+
window.location.href = await slack.installUrl()
|
|
80
|
+
} catch (e) {
|
|
81
|
+
notifyError('Could not start Slack OAuth', e)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function connectWithToken() {
|
|
86
|
+
if (!tokenInput.value.trim()) return
|
|
87
|
+
try {
|
|
88
|
+
await slack.connectWithToken(tokenInput.value.trim())
|
|
89
|
+
tokenInput.value = ''
|
|
90
|
+
toast.add({ title: 'Slack connected', icon: 'i-lucide-check', color: 'success' })
|
|
91
|
+
} catch (e) {
|
|
92
|
+
notifyError('Could not connect Slack', e)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function disconnect() {
|
|
97
|
+
try {
|
|
98
|
+
await slack.disconnect()
|
|
99
|
+
} catch (e) {
|
|
100
|
+
notifyError('Could not disconnect Slack', e)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function saveRouting() {
|
|
105
|
+
busy.value = true
|
|
106
|
+
try {
|
|
107
|
+
await slack.updateSettings({
|
|
108
|
+
routes: { ...routes },
|
|
109
|
+
mentionsEnabled: mentionsEnabled.value,
|
|
110
|
+
})
|
|
111
|
+
toast.add({ title: 'Routing saved', icon: 'i-lucide-check', color: 'success' })
|
|
112
|
+
} catch (e) {
|
|
113
|
+
notifyError('Could not save routing', e)
|
|
114
|
+
} finally {
|
|
115
|
+
busy.value = false
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function addMapping() {
|
|
120
|
+
mapping.value.push({ userId: '', slackUserId: '', role: 'engineering' })
|
|
121
|
+
}
|
|
122
|
+
function removeMapping(index: number) {
|
|
123
|
+
mapping.value.splice(index, 1)
|
|
124
|
+
}
|
|
125
|
+
async function saveMapping() {
|
|
126
|
+
busy.value = true
|
|
127
|
+
try {
|
|
128
|
+
const entries = mapping.value.filter((e) => e.userId.trim() && e.slackUserId.trim())
|
|
129
|
+
await slack.updateMemberMapping(entries)
|
|
130
|
+
mapping.value = slack.memberMapping.map((e) => ({ ...e }))
|
|
131
|
+
toast.add({ title: 'Member map saved', icon: 'i-lucide-check', color: 'success' })
|
|
132
|
+
} catch (e) {
|
|
133
|
+
notifyError('Could not save member map', e)
|
|
134
|
+
} finally {
|
|
135
|
+
busy.value = false
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
</script>
|
|
139
|
+
|
|
140
|
+
<template>
|
|
141
|
+
<UModal v-model:open="open" title="Slack notifications" :ui="{ content: 'max-w-2xl' }">
|
|
142
|
+
<template #body>
|
|
143
|
+
<div class="space-y-5">
|
|
144
|
+
<p class="text-xs text-slate-400">
|
|
145
|
+
Post board notifications (merge reviews, pipeline completions, CI failures) to Slack. The
|
|
146
|
+
connection is shared across the account; routing is per board.
|
|
147
|
+
</p>
|
|
148
|
+
|
|
149
|
+
<!-- not connected: connect UI -->
|
|
150
|
+
<div v-if="!slack.connected" class="space-y-3 rounded-lg border border-slate-700 p-3">
|
|
151
|
+
<UButton
|
|
152
|
+
v-if="slack.oauthEnabled"
|
|
153
|
+
color="primary"
|
|
154
|
+
icon="i-lucide-slack"
|
|
155
|
+
@click="connectViaOAuth"
|
|
156
|
+
>
|
|
157
|
+
Add to Slack
|
|
158
|
+
</UButton>
|
|
159
|
+
<div class="space-y-1">
|
|
160
|
+
<span class="block text-[10px] uppercase tracking-wide text-slate-500">
|
|
161
|
+
…or paste a bot token (xoxb-…)
|
|
162
|
+
</span>
|
|
163
|
+
<div class="flex gap-2">
|
|
164
|
+
<UInput
|
|
165
|
+
v-model="tokenInput"
|
|
166
|
+
size="sm"
|
|
167
|
+
class="flex-1"
|
|
168
|
+
type="password"
|
|
169
|
+
placeholder="xoxb-…"
|
|
170
|
+
/>
|
|
171
|
+
<UButton
|
|
172
|
+
color="primary"
|
|
173
|
+
variant="soft"
|
|
174
|
+
size="sm"
|
|
175
|
+
:loading="slack.connecting"
|
|
176
|
+
:disabled="!tokenInput.trim()"
|
|
177
|
+
@click="connectWithToken"
|
|
178
|
+
>
|
|
179
|
+
Connect
|
|
180
|
+
</UButton>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<!-- connected -->
|
|
186
|
+
<template v-else>
|
|
187
|
+
<div
|
|
188
|
+
class="flex items-center gap-2 rounded-lg border border-slate-700 bg-slate-800/40 p-3"
|
|
189
|
+
>
|
|
190
|
+
<UIcon name="i-lucide-slack" class="text-emerald-400" />
|
|
191
|
+
<span class="flex-1 text-sm text-slate-200">
|
|
192
|
+
Connected to <span class="font-semibold">{{ slack.connection?.teamName }}</span>
|
|
193
|
+
</span>
|
|
194
|
+
<UButton
|
|
195
|
+
color="error"
|
|
196
|
+
variant="ghost"
|
|
197
|
+
size="xs"
|
|
198
|
+
icon="i-lucide-unplug"
|
|
199
|
+
@click="disconnect"
|
|
200
|
+
>
|
|
201
|
+
Disconnect
|
|
202
|
+
</UButton>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<!-- routing -->
|
|
206
|
+
<div class="space-y-3">
|
|
207
|
+
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">Routing</p>
|
|
208
|
+
<div
|
|
209
|
+
v-for="row in ROUTABLE"
|
|
210
|
+
:key="row.type"
|
|
211
|
+
class="flex items-center gap-3 rounded-lg border border-slate-700 bg-slate-800/40 p-2"
|
|
212
|
+
>
|
|
213
|
+
<USwitch v-model="routes[row.type]!.enabled" size="sm" />
|
|
214
|
+
<span class="w-32 text-sm text-slate-300">{{ row.label }}</span>
|
|
215
|
+
<UInput
|
|
216
|
+
v-model="routes[row.type]!.channel"
|
|
217
|
+
size="sm"
|
|
218
|
+
class="flex-1"
|
|
219
|
+
placeholder="#channel or channel id"
|
|
220
|
+
:disabled="!routes[row.type]!.enabled"
|
|
221
|
+
list="slack-channels"
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
<datalist id="slack-channels">
|
|
225
|
+
<option v-for="ch in slack.channels" :key="ch.id" :value="`#${ch.name}`" />
|
|
226
|
+
</datalist>
|
|
227
|
+
|
|
228
|
+
<label class="flex items-center gap-2">
|
|
229
|
+
<USwitch v-model="mentionsEnabled" size="sm" />
|
|
230
|
+
<span class="text-sm text-slate-300">@-mention mapped account members</span>
|
|
231
|
+
</label>
|
|
232
|
+
|
|
233
|
+
<div class="flex justify-end">
|
|
234
|
+
<UButton
|
|
235
|
+
color="primary"
|
|
236
|
+
variant="soft"
|
|
237
|
+
size="xs"
|
|
238
|
+
icon="i-lucide-save"
|
|
239
|
+
:loading="busy"
|
|
240
|
+
@click="saveRouting"
|
|
241
|
+
>
|
|
242
|
+
Save routing
|
|
243
|
+
</UButton>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<!-- member mapping -->
|
|
248
|
+
<div v-if="mentionsEnabled" class="space-y-2">
|
|
249
|
+
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
250
|
+
Member map (user id → Slack member id)
|
|
251
|
+
</p>
|
|
252
|
+
<p class="text-[11px] leading-snug text-slate-500">
|
|
253
|
+
<span class="font-medium text-slate-400">Product</span> people are mentioned on
|
|
254
|
+
requirement-review findings; everyone else only when they created the task.
|
|
255
|
+
</p>
|
|
256
|
+
<div v-for="(entry, i) in mapping" :key="i" class="flex items-center gap-2">
|
|
257
|
+
<UInput v-model="entry.userId" size="sm" class="w-40" placeholder="User id (usr_…)" />
|
|
258
|
+
<UInput
|
|
259
|
+
v-model="entry.slackUserId"
|
|
260
|
+
size="sm"
|
|
261
|
+
class="flex-1"
|
|
262
|
+
placeholder="Slack member id (U…)"
|
|
263
|
+
/>
|
|
264
|
+
<USelect v-model="entry.role" :items="ROLE_OPTIONS" size="sm" class="w-32" />
|
|
265
|
+
<UButton
|
|
266
|
+
color="error"
|
|
267
|
+
variant="ghost"
|
|
268
|
+
size="xs"
|
|
269
|
+
icon="i-lucide-trash-2"
|
|
270
|
+
@click="removeMapping(i)"
|
|
271
|
+
/>
|
|
272
|
+
</div>
|
|
273
|
+
<div class="flex justify-between">
|
|
274
|
+
<UButton
|
|
275
|
+
color="neutral"
|
|
276
|
+
variant="ghost"
|
|
277
|
+
size="xs"
|
|
278
|
+
icon="i-lucide-plus"
|
|
279
|
+
@click="addMapping"
|
|
280
|
+
>
|
|
281
|
+
Add member
|
|
282
|
+
</UButton>
|
|
283
|
+
<UButton
|
|
284
|
+
color="primary"
|
|
285
|
+
variant="soft"
|
|
286
|
+
size="xs"
|
|
287
|
+
icon="i-lucide-save"
|
|
288
|
+
:loading="busy"
|
|
289
|
+
@click="saveMapping"
|
|
290
|
+
>
|
|
291
|
+
Save map
|
|
292
|
+
</UButton>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
</template>
|
|
296
|
+
</div>
|
|
297
|
+
</template>
|
|
298
|
+
</UModal>
|
|
299
|
+
</template>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Inspector section for a task block: the tracker issues (Jira, …) attached to
|
|
3
|
+
// it as agent context, plus an "Attach" menu to link an already-imported issue
|
|
4
|
+
// or open the import modal. Mirrors TaskContextDocs.vue; shown only when the
|
|
5
|
+
// task-source integration is available. Each linked issue shows its status so
|
|
6
|
+
// the structured nature of an issue is visible at a glance.
|
|
7
|
+
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
8
|
+
import type { Block, TaskSourceKind } from '~/types/domain'
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{ block: Block }>()
|
|
11
|
+
|
|
12
|
+
const tasks = useTasksStore()
|
|
13
|
+
const ui = useUiStore()
|
|
14
|
+
const toast = useToast()
|
|
15
|
+
|
|
16
|
+
onMounted(() => {
|
|
17
|
+
tasks.loadTasks().catch(() => {})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const linked = computed(() => tasks.tasksForBlock(props.block.id))
|
|
21
|
+
|
|
22
|
+
async function attach(source: TaskSourceKind, externalId: string) {
|
|
23
|
+
try {
|
|
24
|
+
await tasks.linkToBlock(props.block.id, source, externalId)
|
|
25
|
+
toast.add({ title: 'Issue attached', icon: 'i-lucide-link' })
|
|
26
|
+
} catch (e) {
|
|
27
|
+
toast.add({
|
|
28
|
+
title: 'Could not attach',
|
|
29
|
+
description: e instanceof Error ? e.message : String(e),
|
|
30
|
+
icon: 'i-lucide-triangle-alert',
|
|
31
|
+
color: 'error',
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const attachMenu = computed<DropdownMenuItem[][]>(() => {
|
|
37
|
+
const linkedKeys = new Set(linked.value.map((t) => `${t.source}:${t.externalId}`))
|
|
38
|
+
const items: DropdownMenuItem[] = tasks.tasks
|
|
39
|
+
.filter((t) => !linkedKeys.has(`${t.source}:${t.externalId}`))
|
|
40
|
+
.map((t) => ({
|
|
41
|
+
label: `${t.externalId} · ${t.title}`,
|
|
42
|
+
icon: tasks.descriptorFor(t.source)?.icon ?? 'i-lucide-square-check',
|
|
43
|
+
onSelect: () => attach(t.source, t.externalId),
|
|
44
|
+
}))
|
|
45
|
+
items.push({
|
|
46
|
+
label: 'Import an issue…',
|
|
47
|
+
icon: 'i-lucide-file-down',
|
|
48
|
+
onSelect: () => ui.openTaskImport(),
|
|
49
|
+
})
|
|
50
|
+
return [items]
|
|
51
|
+
})
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<div v-if="tasks.available" class="space-y-2">
|
|
56
|
+
<div class="flex items-center justify-between">
|
|
57
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
58
|
+
Context issues
|
|
59
|
+
</span>
|
|
60
|
+
<UDropdownMenu :items="attachMenu" :content="{ side: 'bottom', align: 'end' }">
|
|
61
|
+
<UButton color="neutral" variant="soft" size="xs" icon="i-lucide-plus">Attach</UButton>
|
|
62
|
+
</UDropdownMenu>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div v-if="linked.length" class="space-y-1">
|
|
66
|
+
<a
|
|
67
|
+
v-for="task in linked"
|
|
68
|
+
:key="`${task.source}:${task.externalId}`"
|
|
69
|
+
:href="task.url"
|
|
70
|
+
target="_blank"
|
|
71
|
+
rel="noopener"
|
|
72
|
+
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 hover:bg-slate-800/60"
|
|
73
|
+
>
|
|
74
|
+
<UIcon
|
|
75
|
+
:name="tasks.descriptorFor(task.source)?.icon ?? 'i-lucide-square-check'"
|
|
76
|
+
class="h-3.5 w-3.5 shrink-0 text-indigo-400"
|
|
77
|
+
/>
|
|
78
|
+
<span class="truncate">{{ task.externalId }} · {{ task.title }}</span>
|
|
79
|
+
<UBadge color="neutral" variant="soft" size="xs" class="ml-auto shrink-0">
|
|
80
|
+
{{ task.status }}
|
|
81
|
+
</UBadge>
|
|
82
|
+
</a>
|
|
83
|
+
</div>
|
|
84
|
+
<p v-else class="text-[11px] text-slate-500">
|
|
85
|
+
Attach a Jira issue so agents see its description and comments while implementing this task.
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
</template>
|