@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,142 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref } from 'vue'
3
+ import type {
4
+ SlackChannel,
5
+ SlackConnection,
6
+ SlackMemberMappingEntry,
7
+ SlackNotificationSettings,
8
+ } from '~/types/domain'
9
+ import { useWorkspaceStore } from '~/stores/workspace'
10
+
11
+ /**
12
+ * Slack integration state: the account's connection (an extra delivery transport
13
+ * for the notification mechanism), the per-workspace notification routing, the
14
+ * channel picker, and the per-account @-mention member map. `available` mirrors
15
+ * the backend's opt-in gate — a 503 from the connection probe means the
16
+ * integration is off and the UI hides its entry points (exactly as the GitHub and
17
+ * documents stores gate on their probes). Nothing is persisted client-side.
18
+ */
19
+ export const useSlackStore = defineStore('slack', () => {
20
+ const api = useApi()
21
+ const workspace = useWorkspaceStore()
22
+
23
+ /** null = unknown (not probed yet), true/false = integration on/off. */
24
+ const available = ref<boolean | null>(null)
25
+ /** The account's Slack connection, or null when not connected. */
26
+ const connection = ref<SlackConnection | null>(null)
27
+ /** Whether the OAuth "Add to Slack" flow is configured (else paste a token). */
28
+ const oauthEnabled = ref(false)
29
+ /** The workspace's notification routing, loaded on demand. */
30
+ const settings = ref<SlackNotificationSettings | null>(null)
31
+ /** Channels for the routing picker, loaded on demand. */
32
+ const channels = ref<SlackChannel[]>([])
33
+ const loadingChannels = ref(false)
34
+ /** The account's GitHub→Slack member map, loaded on demand. */
35
+ const memberMapping = ref<SlackMemberMappingEntry[]>([])
36
+ const connecting = ref(false)
37
+ const saving = ref(false)
38
+
39
+ const connected = computed(() => connection.value !== null)
40
+
41
+ /**
42
+ * Probe the integration: a 503 (or any error) on the connection read means Slack
43
+ * is off — hide the UI. On success, capture the connection + whether OAuth is
44
+ * available. Called on workspace change, like the GitHub probe.
45
+ */
46
+ async function probe() {
47
+ try {
48
+ const { connection: conn, oauthEnabled: oauth } = await api.getSlackConnection(
49
+ workspace.requireId(),
50
+ )
51
+ available.value = true
52
+ connection.value = conn
53
+ oauthEnabled.value = oauth
54
+ } catch {
55
+ available.value = false
56
+ connection.value = null
57
+ }
58
+ }
59
+
60
+ /** Resolve the "Add to Slack" OAuth URL (only when oauthEnabled). */
61
+ function installUrl(): Promise<string> {
62
+ return api.getSlackInstallUrl(workspace.requireId()).then((r) => r.url)
63
+ }
64
+
65
+ /** Connect by pasting a bot token (the always-available fallback). */
66
+ async function connectWithToken(token: string) {
67
+ connecting.value = true
68
+ try {
69
+ connection.value = await api.connectSlack(workspace.requireId(), token)
70
+ } finally {
71
+ connecting.value = false
72
+ }
73
+ }
74
+
75
+ async function disconnect() {
76
+ await api.disconnectSlack(workspace.requireId())
77
+ connection.value = null
78
+ channels.value = []
79
+ }
80
+
81
+ async function loadChannels() {
82
+ loadingChannels.value = true
83
+ try {
84
+ channels.value = (await api.listSlackChannels(workspace.requireId())).channels
85
+ } finally {
86
+ loadingChannels.value = false
87
+ }
88
+ }
89
+
90
+ async function loadSettings() {
91
+ settings.value = await api.getSlackSettings(workspace.requireId())
92
+ }
93
+
94
+ async function updateSettings(body: {
95
+ routes: SlackNotificationSettings['routes']
96
+ mentionsEnabled: boolean
97
+ }) {
98
+ saving.value = true
99
+ try {
100
+ settings.value = await api.updateSlackSettings(workspace.requireId(), body)
101
+ } finally {
102
+ saving.value = false
103
+ }
104
+ }
105
+
106
+ async function loadMemberMapping() {
107
+ memberMapping.value = (await api.getSlackMemberMapping(workspace.requireId())).entries
108
+ }
109
+
110
+ async function updateMemberMapping(entries: SlackMemberMappingEntry[]) {
111
+ saving.value = true
112
+ try {
113
+ memberMapping.value = (
114
+ await api.updateSlackMemberMapping(workspace.requireId(), entries)
115
+ ).entries
116
+ } finally {
117
+ saving.value = false
118
+ }
119
+ }
120
+
121
+ return {
122
+ available,
123
+ connection,
124
+ oauthEnabled,
125
+ settings,
126
+ channels,
127
+ loadingChannels,
128
+ memberMapping,
129
+ connecting,
130
+ saving,
131
+ connected,
132
+ probe,
133
+ installUrl,
134
+ connectWithToken,
135
+ disconnect,
136
+ loadChannels,
137
+ loadSettings,
138
+ updateSettings,
139
+ loadMemberMapping,
140
+ updateMemberMapping,
141
+ }
142
+ })
@@ -0,0 +1,36 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref } from 'vue'
3
+
4
+ /**
5
+ * Which task cards may expand their full build-pipeline list once zoomed in.
6
+ *
7
+ * Deep-zoom (`steps`/`subtasks`) grows a task card downward, and cards are
8
+ * absolutely positioned in their frame, so several expanded cards stacked
9
+ * vertically pile on top of each other. The board driver (`useTaskExpansion`)
10
+ * recomputes a permitted set every frame — only on-screen cards, and only the
11
+ * one closest to the screen centre when two would overlap — and writes it here.
12
+ * `TaskPipelineMini` reads `canExpand` to decide whether to expand or stay compact.
13
+ *
14
+ * `driverActive` lets the gate degrade gracefully: with no board driver mounted
15
+ * (e.g. a card rendered in isolation) `canExpand` falls back to "allowed", so the
16
+ * plain zoom behaviour is unchanged.
17
+ */
18
+ export const useTaskExpansionStore = defineStore('taskExpansion', () => {
19
+ const allowed = ref<Set<string>>(new Set())
20
+ const driverActive = ref(false)
21
+
22
+ function setAllowed(ids: Set<string>) {
23
+ allowed.value = ids
24
+ }
25
+
26
+ function setDriverActive(active: boolean) {
27
+ driverActive.value = active
28
+ if (!active) allowed.value = new Set()
29
+ }
30
+
31
+ function canExpand(id: string) {
32
+ return driverActive.value ? allowed.value.has(id) : true
33
+ }
34
+
35
+ return { allowed, driverActive, setAllowed, setDriverActive, canExpand }
36
+ })
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest'
2
+ import type { SourceTask, TaskConnection, TaskSourceDescriptor } from '~/types/domain'
3
+ import { useTasksStore } from '~/stores/tasks'
4
+
5
+ /** Minimal SourceTask factory — only the fields the read getters care about. */
6
+ function sourceTask(externalId: string, over: Partial<SourceTask> = {}): SourceTask {
7
+ return {
8
+ source: 'jira',
9
+ externalId,
10
+ title: externalId,
11
+ url: `https://acme.atlassian.net/browse/${externalId}`,
12
+ status: 'To Do',
13
+ type: 'Task',
14
+ assignee: null,
15
+ priority: null,
16
+ labels: [],
17
+ description: '',
18
+ comments: [],
19
+ excerpt: '',
20
+ linkedBlockId: null,
21
+ syncedAt: 0,
22
+ ...over,
23
+ }
24
+ }
25
+
26
+ const jiraDescriptor: TaskSourceDescriptor = {
27
+ source: 'jira',
28
+ label: 'Jira',
29
+ icon: 'i-lucide-square-check',
30
+ credentialFields: [],
31
+ refLabel: 'Issue key or URL',
32
+ refPlaceholder: 'PROJ-123',
33
+ }
34
+
35
+ const jiraConnection: TaskConnection = { source: 'jira', label: 'acme', connectedAt: 0 }
36
+
37
+ describe('tasks store read getters', () => {
38
+ let store: ReturnType<typeof useTasksStore>
39
+ beforeEach(() => {
40
+ store = useTasksStore()
41
+ })
42
+
43
+ it('tasksForBlock returns only issues linked to the block', () => {
44
+ store.tasks = [
45
+ sourceTask('PROJ-1', { linkedBlockId: 'b1' }),
46
+ sourceTask('PROJ-2', { linkedBlockId: 'b2' }),
47
+ sourceTask('PROJ-3', { linkedBlockId: null }),
48
+ ]
49
+ expect(store.tasksForBlock('b1').map((t) => t.externalId)).toEqual(['PROJ-1'])
50
+ expect(store.tasksForBlock('bX')).toEqual([])
51
+ })
52
+
53
+ it('isConnected / connectedSources / anyConnected reflect connections', () => {
54
+ store.sources = [jiraDescriptor]
55
+ expect(store.anyConnected).toBe(false)
56
+ expect(store.isConnected('jira')).toBe(false)
57
+ expect(store.connectedSources).toEqual([])
58
+
59
+ store.connections = [jiraConnection]
60
+ expect(store.anyConnected).toBe(true)
61
+ expect(store.isConnected('jira')).toBe(true)
62
+ expect(store.connectedSources.map((s) => s.source)).toEqual(['jira'])
63
+ })
64
+
65
+ it('descriptorFor / connectionFor resolve by source', () => {
66
+ store.sources = [jiraDescriptor]
67
+ store.connections = [jiraConnection]
68
+ expect(store.descriptorFor('jira')?.label).toBe('Jira')
69
+ expect(store.connectionFor('jira')?.label).toBe('acme')
70
+ })
71
+ })
@@ -0,0 +1,176 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref } from 'vue'
3
+ import type {
4
+ SourceTask,
5
+ TaskConnection,
6
+ TaskSearchResult,
7
+ TaskSourceDescriptor,
8
+ TaskSourceKind,
9
+ } from '~/types/domain'
10
+ import { useWorkspaceStore } from '~/stores/workspace'
11
+
12
+ /**
13
+ * Task-source integration state: the trackers the backend offers (and their
14
+ * connect metadata), the workspace's per-source connections, and the issues it
15
+ * has imported — plus the actions that connect/import/link against the backend.
16
+ * `available` mirrors the backend's opt-in gate: a 503 from the source probe
17
+ * means the integration is off, and the UI hides its entry points (just as the
18
+ * documents store does). The abstraction is source-agnostic; every action is
19
+ * keyed by a `TaskSourceKind`. Per-workspace; nothing is persisted client-side.
20
+ *
21
+ * Unlike documents there is no plan/spawn — an issue is linked to a block for
22
+ * agent context, never expanded into board structure.
23
+ */
24
+ export const useTasksStore = defineStore('tasks', () => {
25
+ const api = useApi()
26
+ const workspace = useWorkspaceStore()
27
+
28
+ /** null = unknown (not probed yet), true/false = integration on/off. */
29
+ const available = ref<boolean | null>(null)
30
+ /** The configured sources and their connect/import descriptors. */
31
+ const sources = ref<TaskSourceDescriptor[]>([])
32
+ /** Live connections, one per connected source. */
33
+ const connections = ref<TaskConnection[]>([])
34
+ const tasks = ref<SourceTask[]>([])
35
+ const loading = ref(false)
36
+
37
+ /** Sources the workspace currently has a live connection to. */
38
+ const connectedSources = computed(() =>
39
+ sources.value.filter((s) => connections.value.some((c) => c.source === s.source)),
40
+ )
41
+ const anyConnected = computed(() => connections.value.length > 0)
42
+
43
+ function descriptorFor(source: TaskSourceKind): TaskSourceDescriptor | undefined {
44
+ return sources.value.find((s) => s.source === source)
45
+ }
46
+
47
+ function connectionFor(source: TaskSourceKind): TaskConnection | undefined {
48
+ return connections.value.find((c) => c.source === source)
49
+ }
50
+
51
+ function isConnected(source: TaskSourceKind): boolean {
52
+ return connectionFor(source) !== undefined
53
+ }
54
+
55
+ /** Imported issues currently attached to a given block. */
56
+ function tasksForBlock(blockId: string): SourceTask[] {
57
+ return tasks.value.filter((t) => t.linkedBlockId === blockId)
58
+ }
59
+
60
+ /** Merge an issue returned by the backend into the local cache. */
61
+ function upsertTask(task: SourceTask) {
62
+ const i = tasks.value.findIndex(
63
+ (t) => t.source === task.source && t.externalId === task.externalId,
64
+ )
65
+ if (i >= 0) tasks.value[i] = task
66
+ else tasks.value.unshift(task)
67
+ }
68
+
69
+ function upsertConnection(conn: TaskConnection) {
70
+ const i = connections.value.findIndex((c) => c.source === conn.source)
71
+ if (i >= 0) connections.value[i] = conn
72
+ else connections.value.push(conn)
73
+ }
74
+
75
+ /** Probe the integration: resolves `available`, the sources and connections. */
76
+ async function probe() {
77
+ if (!workspace.workspaceId) return
78
+ try {
79
+ const [{ sources: srcs }, { connections: conns }] = await Promise.all([
80
+ api.listTaskSources(workspace.requireId()),
81
+ api.listTaskConnections(workspace.requireId()),
82
+ ])
83
+ available.value = true
84
+ sources.value = srcs
85
+ connections.value = conns
86
+ } catch {
87
+ // 503 (integration disabled) or any error → hide the UI entry points.
88
+ available.value = false
89
+ sources.value = []
90
+ connections.value = []
91
+ }
92
+ }
93
+
94
+ /** Connect the workspace to a source with its credential bag. */
95
+ async function connect(source: TaskSourceKind, credentials: Record<string, string>) {
96
+ const conn = await api.connectTaskSource(workspace.requireId(), source, credentials)
97
+ upsertConnection(conn)
98
+ available.value = true
99
+ }
100
+
101
+ /** Disconnect the workspace from a source. */
102
+ async function disconnect(source: TaskSourceKind) {
103
+ await api.disconnectTaskSource(workspace.requireId(), source)
104
+ connections.value = connections.value.filter((c) => c.source !== source)
105
+ }
106
+
107
+ /** Load the imported issues for the workspace (across sources). */
108
+ async function loadTasks() {
109
+ tasks.value = await api.listTasks(workspace.requireId())
110
+ }
111
+
112
+ /** Import (fetch + persist) an issue by key or URL from a source. */
113
+ async function importTask(source: TaskSourceKind, ref: string): Promise<SourceTask> {
114
+ loading.value = true
115
+ try {
116
+ const task = await api.importTask(workspace.requireId(), source, { ref })
117
+ upsertTask(task)
118
+ return task
119
+ } finally {
120
+ loading.value = false
121
+ }
122
+ }
123
+
124
+ /** Search a connected tracker's issues by free text (title/content). */
125
+ async function search(source: TaskSourceKind, query: string): Promise<TaskSearchResult[]> {
126
+ const { results } = await api.searchTaskSource(workspace.requireId(), source, query)
127
+ return results
128
+ }
129
+
130
+ /** Attach an imported issue to a block as agent context. */
131
+ async function linkToBlock(blockId: string, source: TaskSourceKind, externalId: string) {
132
+ const task = await api.linkTask(workspace.requireId(), { source, externalId, blockId })
133
+ upsertTask(task)
134
+ return task
135
+ }
136
+
137
+ /**
138
+ * Create a new board task from an imported issue inside a container, linking the
139
+ * issue to it for context. The caller upserts the returned block onto the board.
140
+ */
141
+ async function createTaskFromIssue(
142
+ source: TaskSourceKind,
143
+ externalId: string,
144
+ containerId: string,
145
+ ) {
146
+ const result = await api.createTaskFromIssue(workspace.requireId(), {
147
+ source,
148
+ externalId,
149
+ containerId,
150
+ })
151
+ upsertTask(result.task)
152
+ return result
153
+ }
154
+
155
+ return {
156
+ available,
157
+ sources,
158
+ connections,
159
+ tasks,
160
+ loading,
161
+ connectedSources,
162
+ anyConnected,
163
+ descriptorFor,
164
+ connectionFor,
165
+ isConnected,
166
+ tasksForBlock,
167
+ probe,
168
+ connect,
169
+ disconnect,
170
+ loadTasks,
171
+ importTask,
172
+ search,
173
+ linkToBlock,
174
+ createTaskFromIssue,
175
+ }
176
+ })
@@ -0,0 +1,27 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref } from 'vue'
3
+ import type { PutTrackerSettingsInput, TrackerSettings } from '~/types/tracker'
4
+ import { useWorkspaceStore } from '~/stores/workspace'
5
+
6
+ /**
7
+ * The workspace's issue-tracker selection (GitHub Issues or Jira) — where the
8
+ * tech-debt recurring pipeline files its ticket. Hydrated from the snapshot;
9
+ * edited inline when configuring a tech-debt recurring pipeline.
10
+ */
11
+ export const useTrackerStore = defineStore('tracker', () => {
12
+ const api = useApi()
13
+
14
+ const settings = ref<TrackerSettings>({ tracker: null, jiraProjectKey: null, updatedAt: 0 })
15
+
16
+ function hydrate(value: TrackerSettings | undefined) {
17
+ settings.value = value ?? { tracker: null, jiraProjectKey: null, updatedAt: 0 }
18
+ }
19
+
20
+ async function save(input: PutTrackerSettingsInput) {
21
+ const ws = useWorkspaceStore()
22
+ settings.value = await api.putTrackerSettings(ws.requireId(), input)
23
+ return settings.value
24
+ }
25
+
26
+ return { settings, hydrate, save }
27
+ })