@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,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
|
+
})
|