@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.
Files changed (189) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/app/app.config.ts +8 -0
  4. package/app/app.vue +11 -0
  5. package/app/assets/css/main.css +100 -0
  6. package/app/components/auth/AuthGate.vue +24 -0
  7. package/app/components/auth/LoginScreen.vue +143 -0
  8. package/app/components/auth/UserMenu.vue +39 -0
  9. package/app/components/board/AddTaskModal.vue +444 -0
  10. package/app/components/board/AgentFailureCard.vue +97 -0
  11. package/app/components/board/AgentStopButton.vue +61 -0
  12. package/app/components/board/BoardCanvas.vue +183 -0
  13. package/app/components/board/ContextPicker.vue +367 -0
  14. package/app/components/board/RecurringPipelineModal.vue +219 -0
  15. package/app/components/board/TaskDependencyEdges.vue +132 -0
  16. package/app/components/board/nodes/AgentChip.vue +59 -0
  17. package/app/components/board/nodes/BlockNode.vue +433 -0
  18. package/app/components/board/nodes/DecisionBadge.vue +27 -0
  19. package/app/components/board/nodes/DraggableTask.vue +48 -0
  20. package/app/components/board/nodes/ModuleFrame.vue +97 -0
  21. package/app/components/board/nodes/TaskCard.vue +359 -0
  22. package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
  23. package/app/components/bootstrap/BootstrapModal.vue +665 -0
  24. package/app/components/clarity/ClarityReviewWindow.vue +611 -0
  25. package/app/components/consensus/ConsensusSessionWindow.vue +210 -0
  26. package/app/components/documents/DocumentImportModal.vue +161 -0
  27. package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
  28. package/app/components/documents/SpawnPreviewModal.vue +161 -0
  29. package/app/components/documents/TaskContextDocs.vue +83 -0
  30. package/app/components/focus/BlockFocusView.vue +171 -0
  31. package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
  32. package/app/components/gates/GateResultView.vue +282 -0
  33. package/app/components/github/AddServiceFromRepoModal.vue +354 -0
  34. package/app/components/github/GitHubConnect.vue +183 -0
  35. package/app/components/github/GitHubOnboarding.vue +45 -0
  36. package/app/components/github/GitHubPanel.vue +584 -0
  37. package/app/components/github/RepoTreeBrowser.vue +171 -0
  38. package/app/components/layout/AccountTeamSettings.vue +237 -0
  39. package/app/components/layout/BoardSwitcher.vue +280 -0
  40. package/app/components/layout/BoardToolbar.vue +156 -0
  41. package/app/components/layout/CommandBar.vue +336 -0
  42. package/app/components/layout/GitHubPatBanner.vue +73 -0
  43. package/app/components/layout/NotificationsInbox.vue +175 -0
  44. package/app/components/layout/SideBar.vue +314 -0
  45. package/app/components/layout/SpendWarningBanner.vue +107 -0
  46. package/app/components/observability/StepMetricsBar.vue +102 -0
  47. package/app/components/palettes/AgentPalette.vue +86 -0
  48. package/app/components/panels/AgentStepDetail.vue +737 -0
  49. package/app/components/panels/DecisionModal.vue +71 -0
  50. package/app/components/panels/InspectorPanel.vue +465 -0
  51. package/app/components/panels/ObservabilityPanel.vue +351 -0
  52. package/app/components/panels/StepMetadataCard.vue +253 -0
  53. package/app/components/panels/StepRestartControl.vue +90 -0
  54. package/app/components/panels/StepResultViewHost.vue +40 -0
  55. package/app/components/panels/StepTestReport.vue +84 -0
  56. package/app/components/panels/inspector/ContainerSummary.vue +74 -0
  57. package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
  58. package/app/components/panels/inspector/ServiceFragments.vue +82 -0
  59. package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
  60. package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
  61. package/app/components/panels/inspector/TaskDependencies.vue +70 -0
  62. package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
  63. package/app/components/panels/inspector/TaskExecution.vue +364 -0
  64. package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
  65. package/app/components/panels/inspector/TaskStructure.vue +96 -0
  66. package/app/components/pipeline/AgentKindIcon.vue +30 -0
  67. package/app/components/pipeline/IterationCapPrompt.vue +70 -0
  68. package/app/components/pipeline/PipelineBuilder.vue +817 -0
  69. package/app/components/pipeline/PipelineProgress.vue +484 -0
  70. package/app/components/providers/ApiKeysSection.vue +273 -0
  71. package/app/components/providers/PersonalCredentialModal.vue +128 -0
  72. package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
  73. package/app/components/providers/VendorCredentialsModal.vue +197 -0
  74. package/app/components/recurring/RecurrenceEditor.vue +124 -0
  75. package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
  76. package/app/components/settings/DatadogPanel.vue +213 -0
  77. package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
  78. package/app/components/settings/MergeThresholdsPanel.vue +378 -0
  79. package/app/components/settings/ModelDefaultsPanel.vue +250 -0
  80. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
  81. package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
  82. package/app/components/slack/SlackPanel.vue +299 -0
  83. package/app/components/tasks/TaskContextIssues.vue +88 -0
  84. package/app/components/tasks/TaskImportModal.vue +207 -0
  85. package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
  86. package/app/components/testing/TestReportWindow.vue +404 -0
  87. package/app/composables/api/accounts.ts +81 -0
  88. package/app/composables/api/auth.ts +45 -0
  89. package/app/composables/api/board.ts +101 -0
  90. package/app/composables/api/bootstrap.ts +62 -0
  91. package/app/composables/api/context.ts +25 -0
  92. package/app/composables/api/documents.ts +74 -0
  93. package/app/composables/api/execution.ts +127 -0
  94. package/app/composables/api/fragments.ts +71 -0
  95. package/app/composables/api/github.ts +131 -0
  96. package/app/composables/api/models.ts +127 -0
  97. package/app/composables/api/notifications.ts +23 -0
  98. package/app/composables/api/presets.ts +29 -0
  99. package/app/composables/api/recurring.ts +68 -0
  100. package/app/composables/api/releaseHealth.ts +43 -0
  101. package/app/composables/api/reviews.ts +146 -0
  102. package/app/composables/api/slack.ts +54 -0
  103. package/app/composables/api/tasks.ts +72 -0
  104. package/app/composables/api/workspaces.ts +36 -0
  105. package/app/composables/useApi.ts +89 -0
  106. package/app/composables/useBlockDrag.ts +90 -0
  107. package/app/composables/useBlockQueries.ts +154 -0
  108. package/app/composables/useBoardFlow.ts +11 -0
  109. package/app/composables/useContextLinking.ts +65 -0
  110. package/app/composables/useDepLabels.ts +26 -0
  111. package/app/composables/useFrameResize.ts +54 -0
  112. package/app/composables/useResultView.ts +48 -0
  113. package/app/composables/useReviewStage.ts +40 -0
  114. package/app/composables/useSemanticZoom.ts +31 -0
  115. package/app/composables/useStepApproval.ts +233 -0
  116. package/app/composables/useStepProse.ts +78 -0
  117. package/app/composables/useStepTimer.ts +63 -0
  118. package/app/composables/useTaskExpansion.ts +92 -0
  119. package/app/composables/useWorkspaceStream.ts +155 -0
  120. package/app/docs/architecture.md +31 -0
  121. package/app/pages/index.vue +141 -0
  122. package/app/stores/accounts.ts +152 -0
  123. package/app/stores/agentConfig.ts +35 -0
  124. package/app/stores/agentRuns.ts +122 -0
  125. package/app/stores/agents.ts +40 -0
  126. package/app/stores/apiKeys.ts +108 -0
  127. package/app/stores/auth.ts +166 -0
  128. package/app/stores/board.spec.ts +205 -0
  129. package/app/stores/board.ts +286 -0
  130. package/app/stores/bootstrap.ts +97 -0
  131. package/app/stores/clarity.ts +196 -0
  132. package/app/stores/consensus.ts +60 -0
  133. package/app/stores/documents.ts +176 -0
  134. package/app/stores/execution.ts +273 -0
  135. package/app/stores/fragmentLibrary.ts +147 -0
  136. package/app/stores/fragments.ts +40 -0
  137. package/app/stores/github.ts +305 -0
  138. package/app/stores/localModels.ts +51 -0
  139. package/app/stores/mergePresets.ts +58 -0
  140. package/app/stores/modelDefaults.ts +76 -0
  141. package/app/stores/models.ts +134 -0
  142. package/app/stores/notifications.ts +70 -0
  143. package/app/stores/observability.ts +144 -0
  144. package/app/stores/personalSubscriptions.ts +215 -0
  145. package/app/stores/pipelines.ts +327 -0
  146. package/app/stores/recurringPipelines.ts +112 -0
  147. package/app/stores/releaseHealth.ts +75 -0
  148. package/app/stores/requirements.spec.ts +94 -0
  149. package/app/stores/requirements.ts +208 -0
  150. package/app/stores/serviceFragmentDefaults.ts +29 -0
  151. package/app/stores/services.ts +87 -0
  152. package/app/stores/slack.ts +142 -0
  153. package/app/stores/taskExpansion.ts +36 -0
  154. package/app/stores/tasks.spec.ts +71 -0
  155. package/app/stores/tasks.ts +176 -0
  156. package/app/stores/tracker.ts +27 -0
  157. package/app/stores/ui.ts +434 -0
  158. package/app/stores/vendorCredentials.ts +54 -0
  159. package/app/stores/workspace.ts +215 -0
  160. package/app/stores/workspaceSettings.ts +36 -0
  161. package/app/types/accounts.ts +77 -0
  162. package/app/types/bootstrap.ts +83 -0
  163. package/app/types/clarity.ts +59 -0
  164. package/app/types/consensus.ts +91 -0
  165. package/app/types/documents.ts +104 -0
  166. package/app/types/domain.ts +495 -0
  167. package/app/types/execution.ts +383 -0
  168. package/app/types/fragments.ts +72 -0
  169. package/app/types/github.ts +173 -0
  170. package/app/types/localModels.ts +73 -0
  171. package/app/types/merge.ts +71 -0
  172. package/app/types/models.ts +157 -0
  173. package/app/types/notifications.ts +74 -0
  174. package/app/types/recurring.ts +69 -0
  175. package/app/types/releaseHealth.ts +31 -0
  176. package/app/types/requirements.ts +61 -0
  177. package/app/types/services.ts +27 -0
  178. package/app/types/slack.ts +57 -0
  179. package/app/types/tasks.ts +82 -0
  180. package/app/types/tracker.ts +18 -0
  181. package/app/utils/agentOutput.spec.ts +128 -0
  182. package/app/utils/agentOutput.ts +173 -0
  183. package/app/utils/catalog.spec.ts +112 -0
  184. package/app/utils/catalog.ts +455 -0
  185. package/app/utils/dnd.ts +29 -0
  186. package/app/utils/observability.ts +52 -0
  187. package/app/utils/pipelineRender.ts +151 -0
  188. package/nuxt.config.ts +55 -0
  189. 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>