@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,305 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref } from 'vue'
3
+ import type {
4
+ CreateBranchInput,
5
+ CreateRepoRequest,
6
+ GitHubAvailableRepo,
7
+ GitHubBranch,
8
+ GitHubConnection,
9
+ GitHubInstallationOption,
10
+ GitHubIssue,
11
+ GitHubPullRequest,
12
+ GitHubRepo,
13
+ MergePullRequestInput,
14
+ OpenPullRequestInput,
15
+ ResyncRequest,
16
+ } from '~/types/domain'
17
+ import { useWorkspaceStore } from '~/stores/workspace'
18
+
19
+ /**
20
+ * GitHub integration state: the workspace's App installation, the projected
21
+ * repos/branches/pull-requests/issues the backend caches in D1, and the actions
22
+ * that connect/resync and write against the repo. `available` mirrors the
23
+ * backend's opt-in gate — a 503 from the connection probe means the integration
24
+ * is off, and the UI hides its entry points (exactly as the documents store
25
+ * gates on its source probe, and `auth.required` gates the login UI). Per
26
+ * workspace, like the board itself; nothing is persisted client-side.
27
+ */
28
+ export const useGitHubStore = defineStore('github', () => {
29
+ const api = useApi()
30
+ const workspace = useWorkspaceStore()
31
+
32
+ /** null = unknown (not probed yet), true/false = integration on/off. */
33
+ const available = ref<boolean | null>(null)
34
+ /** The workspace's App installation, or null when not yet connected. */
35
+ const connection = ref<GitHubConnection | null>(null)
36
+ /** Discovered App installations for the connect picker; loaded on demand. */
37
+ const installations = ref<GitHubInstallationOption[]>([])
38
+ const loadingInstallations = ref(false)
39
+ const repos = ref<GitHubRepo[]>([])
40
+ /** Repos the installation can access, for the per-workspace link picker. */
41
+ const availableRepos = ref<GitHubAvailableRepo[]>([])
42
+ const loadingAvailable = ref(false)
43
+ const savingRepos = ref(false)
44
+ const pulls = ref<GitHubPullRequest[]>([])
45
+ const issues = ref<GitHubIssue[]>([])
46
+ /** Branches loaded lazily per repo (by GitHub numeric id). */
47
+ const branches = ref<Record<number, GitHubBranch[]>>({})
48
+ const loading = ref(false)
49
+ const syncing = ref(false)
50
+
51
+ const connected = computed(() => connection.value !== null)
52
+ /** Whether cat-factory can create repos under the connected account itself. */
53
+ const canCreateRepos = computed(() => connection.value?.canCreateRepos === true)
54
+ /**
55
+ * True when connected but the install is MISSING `workflows: write` — agent
56
+ * pushes that touch `.github/workflows/*` will be rejected until it's granted.
57
+ */
58
+ const missingWorkflowsPermission = computed(
59
+ () => connection.value !== null && connection.value.canManageWorkflows !== true,
60
+ )
61
+
62
+ function repoFor(repoGithubId: number): GitHubRepo | undefined {
63
+ return repos.value.find((r) => r.githubId === repoGithubId)
64
+ }
65
+
66
+ /** The repo linked to a board block (its backing service repo), if any. */
67
+ function repoForBlock(blockId: string): GitHubRepo | undefined {
68
+ return repos.value.find((r) => r.blockId === blockId)
69
+ }
70
+
71
+ function pullsForRepo(repoGithubId: number): GitHubPullRequest[] {
72
+ return pulls.value.filter((p) => p.repoGithubId === repoGithubId)
73
+ }
74
+
75
+ function issuesForRepo(repoGithubId: number): GitHubIssue[] {
76
+ return issues.value.filter((i) => i.repoGithubId === repoGithubId)
77
+ }
78
+
79
+ /** Build the github.com URL for a repo / PR / issue from the projection row. */
80
+ function repoUrl(repoGithubId: number): string | null {
81
+ const r = repoFor(repoGithubId)
82
+ return r ? `https://github.com/${r.owner}/${r.name}` : null
83
+ }
84
+ function pullUrl(pr: GitHubPullRequest): string | null {
85
+ const base = repoUrl(pr.repoGithubId)
86
+ return base ? `${base}/pull/${pr.number}` : null
87
+ }
88
+ function issueUrl(issue: GitHubIssue): string | null {
89
+ const base = repoUrl(issue.repoGithubId)
90
+ return base ? `${base}/issues/${issue.number}` : null
91
+ }
92
+
93
+ /** Probe the integration: resolves `available` and the current connection. */
94
+ async function probe() {
95
+ if (!workspace.workspaceId) return
96
+ try {
97
+ const { connection: conn } = await api.getGitHubConnection(workspace.requireId())
98
+ available.value = true
99
+ connection.value = conn
100
+ } catch {
101
+ // 503 (integration disabled) or any error → hide the UI entry points.
102
+ available.value = false
103
+ connection.value = null
104
+ }
105
+ }
106
+
107
+ /** Load the cached repos, pull requests and issues for the workspace. */
108
+ async function load() {
109
+ if (!connected.value) return
110
+ loading.value = true
111
+ try {
112
+ const [r, p, i] = await Promise.all([
113
+ api.listGitHubRepos(workspace.requireId()),
114
+ api.listGitHubPullRequests(workspace.requireId()),
115
+ api.listGitHubIssues(workspace.requireId()),
116
+ ])
117
+ repos.value = r
118
+ pulls.value = p
119
+ issues.value = i
120
+ } finally {
121
+ loading.value = false
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Ensure the projection (repos/PRs/issues) is loaded at least once — for views
127
+ * that need it without opening the GitHub panel (e.g. the inspector's repo link).
128
+ * Probes the integration first if it hasn't been yet.
129
+ */
130
+ async function ensureLoaded() {
131
+ if (available.value === null) await probe()
132
+ if (connected.value && repos.value.length === 0) await load()
133
+ }
134
+
135
+ /** Load the repos the installation can access, with this workspace's link state. */
136
+ async function loadAvailableRepos() {
137
+ if (!connected.value) return
138
+ loadingAvailable.value = true
139
+ try {
140
+ availableRepos.value = await api.listGitHubAvailableRepos(workspace.requireId())
141
+ } finally {
142
+ loadingAvailable.value = false
143
+ }
144
+ }
145
+
146
+ /** Set the exact set of repos this workspace links, then refresh projections. */
147
+ async function setLinkedRepos(repoGithubIds: number[]) {
148
+ savingRepos.value = true
149
+ try {
150
+ repos.value = await api.setGitHubLinkedRepos(workspace.requireId(), repoGithubIds)
151
+ // Reflect the new link state in the picker and refresh PRs/issues.
152
+ const linked = new Set(repoGithubIds)
153
+ availableRepos.value = availableRepos.value.map((r) => ({
154
+ ...r,
155
+ linked: linked.has(r.githubId),
156
+ }))
157
+ await load()
158
+ } finally {
159
+ savingRepos.value = false
160
+ }
161
+ }
162
+
163
+ /** Lazily load (and cache) the branches for a single repo. */
164
+ async function loadBranches(repoGithubId: number): Promise<GitHubBranch[]> {
165
+ const list = await api.listGitHubBranches(workspace.requireId(), repoGithubId)
166
+ branches.value = { ...branches.value, [repoGithubId]: list }
167
+ return list
168
+ }
169
+
170
+ /** List one level of a (monorepo) repo's tree, for the service-directory picker. */
171
+ function loadRepoTree(repoGithubId: number, path = '') {
172
+ return api.listGitHubRepoTree(workspace.requireId(), repoGithubId, path)
173
+ }
174
+
175
+ /** The URL a workspace owner visits to install the App against this workspace. */
176
+ function getInstallUrl(): Promise<string> {
177
+ return api.getGitHubInstallUrl(workspace.requireId()).then((r) => r.url)
178
+ }
179
+
180
+ /** Discover the App's installations so the user can connect one without typing an id. */
181
+ async function loadInstallations() {
182
+ loadingInstallations.value = true
183
+ try {
184
+ const { installations: list } = await api.listGitHubInstallations(workspace.requireId())
185
+ installations.value = list
186
+ } finally {
187
+ loadingInstallations.value = false
188
+ }
189
+ }
190
+
191
+ /** Programmatic bind by installation id (the browser flow uses the redirect). */
192
+ async function connect(installationId: number) {
193
+ connection.value = await api.connectGitHub(workspace.requireId(), installationId)
194
+ available.value = true
195
+ await load()
196
+ }
197
+
198
+ async function disconnect() {
199
+ await api.disconnectGitHub(workspace.requireId())
200
+ connection.value = null
201
+ repos.value = []
202
+ availableRepos.value = []
203
+ pulls.value = []
204
+ issues.value = []
205
+ branches.value = {}
206
+ }
207
+
208
+ /** Trigger a resync, then refresh projections (no-op for queued/backfill). */
209
+ async function resync(body: ResyncRequest = {}) {
210
+ syncing.value = true
211
+ try {
212
+ const res = await api.resyncGitHub(workspace.requireId(), body)
213
+ await load()
214
+ return res
215
+ } finally {
216
+ syncing.value = false
217
+ }
218
+ }
219
+
220
+ // ---- repo writes ----------------------------------------------------------
221
+
222
+ /**
223
+ * Create a repository under the connected account (privileged App tier). Only
224
+ * meaningful when `canCreateRepos`; the backend 409s otherwise. Returns the
225
+ * created repo so the caller can confirm/link it.
226
+ */
227
+ function createRepo(input: CreateRepoRequest) {
228
+ return api.createGitHubRepo(workspace.requireId(), input)
229
+ }
230
+
231
+ async function createBranch(repoGithubId: number, input: CreateBranchInput) {
232
+ const branch = await api.createGitHubBranch(workspace.requireId(), repoGithubId, input)
233
+ const next = branches.value[repoGithubId] ?? []
234
+ branches.value = { ...branches.value, [repoGithubId]: [branch, ...next] }
235
+ return branch
236
+ }
237
+
238
+ async function openPullRequest(repoGithubId: number, input: OpenPullRequestInput) {
239
+ const pr = await api.openGitHubPullRequest(workspace.requireId(), repoGithubId, input)
240
+ const i = pulls.value.findIndex(
241
+ (p) => p.repoGithubId === pr.repoGithubId && p.number === pr.number,
242
+ )
243
+ if (i >= 0) pulls.value[i] = pr
244
+ else pulls.value.unshift(pr)
245
+ return pr
246
+ }
247
+
248
+ async function mergePullRequest(
249
+ repoGithubId: number,
250
+ number: number,
251
+ input: MergePullRequestInput = {},
252
+ ) {
253
+ await api.mergeGitHubPullRequest(workspace.requireId(), repoGithubId, number, input)
254
+ // Optimistically reflect the merge until the next sync confirms it.
255
+ const i = pulls.value.findIndex((p) => p.repoGithubId === repoGithubId && p.number === number)
256
+ if (i >= 0) pulls.value[i] = { ...pulls.value[i]!, state: 'closed', merged: true }
257
+ }
258
+
259
+ function comment(repoGithubId: number, number: number, body: string) {
260
+ return api.commentGitHubIssue(workspace.requireId(), repoGithubId, number, body)
261
+ }
262
+
263
+ return {
264
+ available,
265
+ connection,
266
+ installations,
267
+ loadingInstallations,
268
+ repos,
269
+ availableRepos,
270
+ loadingAvailable,
271
+ savingRepos,
272
+ pulls,
273
+ issues,
274
+ branches,
275
+ loading,
276
+ syncing,
277
+ connected,
278
+ canCreateRepos,
279
+ missingWorkflowsPermission,
280
+ repoFor,
281
+ repoForBlock,
282
+ pullsForRepo,
283
+ issuesForRepo,
284
+ repoUrl,
285
+ pullUrl,
286
+ issueUrl,
287
+ probe,
288
+ load,
289
+ ensureLoaded,
290
+ loadAvailableRepos,
291
+ setLinkedRepos,
292
+ loadRepoTree,
293
+ loadBranches,
294
+ getInstallUrl,
295
+ loadInstallations,
296
+ connect,
297
+ disconnect,
298
+ resync,
299
+ createRepo,
300
+ createBranch,
301
+ openPullRequest,
302
+ mergePullRequest,
303
+ comment,
304
+ }
305
+ })
@@ -0,0 +1,51 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref } from 'vue'
3
+ import type {
4
+ LocalModelEndpoint,
5
+ LocalModelEndpointTestResult,
6
+ LocalRunner,
7
+ TestLocalModelEndpointInput,
8
+ UpsertLocalModelEndpointInput,
9
+ } from '~/types/localModels'
10
+
11
+ // The signed-in user's local model runner endpoints — Ollama / LM Studio / llama.cpp /
12
+ // vLLM / a custom OpenAI-compatible server running on their OWN machine. A runner lives
13
+ // on a person's box (`localhost:11434` means something different per member), so these are
14
+ // stored PER USER, not pooled on the workspace. The API key is write-only server-side and
15
+ // never returned; this store only carries the metadata (+ the enabled model ids). Loaded
16
+ // INDEPENDENTLY (not from the workspace snapshot) — like personal subscriptions.
17
+ export const useLocalModelsStore = defineStore('localModels', () => {
18
+ const api = useApi()
19
+ const endpoints = ref<LocalModelEndpoint[]>([])
20
+ const loading = ref(false)
21
+
22
+ async function load() {
23
+ loading.value = true
24
+ try {
25
+ const { endpoints: list } = await api.listLocalModelEndpoints()
26
+ endpoints.value = list
27
+ } catch {
28
+ // Auth disabled / not signed in / feature off → no local runners surface.
29
+ endpoints.value = []
30
+ } finally {
31
+ loading.value = false
32
+ }
33
+ }
34
+
35
+ async function upsert(input: UpsertLocalModelEndpointInput) {
36
+ const endpoint = await api.upsertLocalModelEndpoint(input.provider, input)
37
+ endpoints.value = [...endpoints.value.filter((e) => e.provider !== endpoint.provider), endpoint]
38
+ return endpoint
39
+ }
40
+
41
+ async function remove(provider: LocalRunner) {
42
+ await api.deleteLocalModelEndpoint(provider)
43
+ endpoints.value = endpoints.value.filter((e) => e.provider !== provider)
44
+ }
45
+
46
+ async function test(input: TestLocalModelEndpointInput): Promise<LocalModelEndpointTestResult> {
47
+ return await api.testLocalModelEndpoint(input)
48
+ }
49
+
50
+ return { endpoints, loading, load, upsert, remove, test }
51
+ })
@@ -0,0 +1,58 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref } from 'vue'
3
+ import type {
4
+ CreateMergePresetInput,
5
+ MergeThresholdPreset,
6
+ UpdateMergePresetInput,
7
+ } from '~/types/domain'
8
+ import { useWorkspaceStore } from '~/stores/workspace'
9
+
10
+ /**
11
+ * The workspace's merge threshold presets — the library a task picks its
12
+ * auto-merge policy from (the `merger` step compares the PR assessment against the
13
+ * resolved preset). Hydrated from the workspace snapshot; managed via a small
14
+ * settings UI. The backend always keeps at least one default preset.
15
+ */
16
+ export const useMergePresetsStore = defineStore('mergePresets', () => {
17
+ const api = useApi()
18
+
19
+ const presets = ref<MergeThresholdPreset[]>([])
20
+
21
+ function hydrate(list: MergeThresholdPreset[]) {
22
+ presets.value = [...list].sort((a, b) => a.createdAt - b.createdAt)
23
+ }
24
+
25
+ /** The workspace default (fallback for a task that picks none). */
26
+ const defaultPreset = computed(() => presets.value.find((p) => p.isDefault) ?? null)
27
+
28
+ /** Resolve a task's effective preset by id, falling back to the default. */
29
+ function resolve(presetId: string | undefined): MergeThresholdPreset | null {
30
+ if (presetId) {
31
+ const picked = presets.value.find((p) => p.id === presetId)
32
+ if (picked) return picked
33
+ }
34
+ return defaultPreset.value
35
+ }
36
+
37
+ async function create(input: CreateMergePresetInput) {
38
+ const ws = useWorkspaceStore()
39
+ const created = await api.createMergePreset(ws.requireId(), input)
40
+ await ws.refresh()
41
+ return created
42
+ }
43
+
44
+ async function update(presetId: string, patch: UpdateMergePresetInput) {
45
+ const ws = useWorkspaceStore()
46
+ const updated = await api.updateMergePreset(ws.requireId(), presetId, patch)
47
+ await ws.refresh()
48
+ return updated
49
+ }
50
+
51
+ async function remove(presetId: string) {
52
+ const ws = useWorkspaceStore()
53
+ await api.deleteMergePreset(ws.requireId(), presetId)
54
+ await ws.refresh()
55
+ }
56
+
57
+ return { presets, defaultPreset, resolve, hydrate, create, update, remove }
58
+ })
@@ -0,0 +1,76 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref } from 'vue'
3
+ import { useWorkspaceStore } from '~/stores/workspace'
4
+
5
+ /**
6
+ * The workspace's per-agent-kind default model overrides — the map an agent step
7
+ * resolves its model from when the task pins none (a block-pinned model still
8
+ * wins; a kind absent from the map falls back to the deployment's env routing).
9
+ * Hydrated from the workspace snapshot; edited via the Default-models settings
10
+ * panel, which replaces the whole map on save.
11
+ */
12
+ export const useModelDefaultsStore = defineStore('modelDefaults', () => {
13
+ const api = useApi()
14
+
15
+ /** agentKind → model catalog id. */
16
+ const defaults = ref<Record<string, string>>({})
17
+
18
+ /**
19
+ * The deployment's env-routing defaults as `provider:model` refs — what a kind
20
+ * runs on when neither the task nor this workspace pins a model. `default` is the
21
+ * global fallback; `byKind` carries kinds the operator routed specifically. Used
22
+ * only to NAME the fallback in the settings panel; it never overrides a pin.
23
+ */
24
+ const deployment = ref<{ default: string; byKind: Record<string, string> }>({
25
+ default: '',
26
+ byKind: {},
27
+ })
28
+
29
+ function hydrate(map: Record<string, string> | undefined) {
30
+ defaults.value = { ...map }
31
+ }
32
+
33
+ function hydrateDeployment(
34
+ next: { default: string; byKind: Record<string, string> } | undefined,
35
+ ) {
36
+ deployment.value = next
37
+ ? { default: next.default, byKind: { ...next.byKind } }
38
+ : {
39
+ default: '',
40
+ byKind: {},
41
+ }
42
+ }
43
+
44
+ /** The model id chosen for a kind, or undefined when it falls back to routing. */
45
+ function forKind(kind: string): string | undefined {
46
+ return defaults.value[kind]
47
+ }
48
+
49
+ /** The deployment-routing model ref a kind falls back to (`byKind[kind] ?? default`). */
50
+ function deploymentRefForKind(kind: string): string | undefined {
51
+ return deployment.value.byKind[kind] || deployment.value.default || undefined
52
+ }
53
+
54
+ /**
55
+ * Set (or, with `null`, clear) the default model for a single agent kind, then
56
+ * persist the whole map. The backend replaces the stored set on every write.
57
+ */
58
+ async function set(kind: string, modelId: string | null) {
59
+ const next = { ...defaults.value }
60
+ if (modelId) next[kind] = modelId
61
+ else delete next[kind]
62
+ const ws = useWorkspaceStore()
63
+ const saved = await api.setModelDefaults(ws.requireId(), next)
64
+ defaults.value = { ...saved.defaults }
65
+ }
66
+
67
+ return {
68
+ defaults,
69
+ deployment,
70
+ hydrate,
71
+ hydrateDeployment,
72
+ forKind,
73
+ deploymentRefForKind,
74
+ set,
75
+ }
76
+ })
@@ -0,0 +1,134 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref, computed } from 'vue'
3
+ import type { ModelCost, ModelOption, SubscriptionVendor } from '~/types/domain'
4
+
5
+ /** The flavour of a model to actually display/run, given configured subscriptions. */
6
+ export interface DisplayFlavor {
7
+ providerLabel: string
8
+ provider: string
9
+ model: string
10
+ contextTokens?: number
11
+ cost?: ModelCost
12
+ /** True ⇒ flat-rate quota; its cost is a quota burn rate, not budget spend. */
13
+ quotaBased: boolean
14
+ vendor?: SubscriptionVendor
15
+ }
16
+
17
+ /**
18
+ * The flavour a model resolves to in the picker given the workspace's configured
19
+ * subscription vendors. A dual-mode model (GLM/Kimi) collapses to its subscription
20
+ * flavour once that vendor is connected ("subscriptions always win"); otherwise the
21
+ * base (cloudflare/direct, or the subscription itself for subscription-only models).
22
+ */
23
+ export function displayFlavor(m: ModelOption, configured: Set<SubscriptionVendor>): DisplayFlavor {
24
+ if (m.subscription && configured.has(m.subscription.vendor)) {
25
+ return {
26
+ providerLabel: m.subscription.providerLabel,
27
+ provider: m.subscription.provider,
28
+ model: m.subscription.model,
29
+ contextTokens: m.subscription.contextTokens,
30
+ cost: m.subscription.cost,
31
+ quotaBased: true,
32
+ vendor: m.subscription.vendor,
33
+ }
34
+ }
35
+ return {
36
+ providerLabel: m.providerLabel,
37
+ provider: m.provider,
38
+ model: m.model,
39
+ contextTokens: m.contextTokens,
40
+ cost: m.cost,
41
+ quotaBased: m.quotaBased ?? false,
42
+ vendor: m.vendor,
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Whether a model is selectable. On the per-workspace catalog the backend already
48
+ * computes `available` from the configured API keys / subscriptions / Cloudflare opt-in,
49
+ * so honour it directly. On the deployment catalog (`available` absent) fall back to the
50
+ * subscription-token heuristic so the picker still gates subscription-only models.
51
+ */
52
+ export function isSelectable(m: ModelOption, configured: Set<SubscriptionVendor>): boolean {
53
+ if (m.available !== undefined) return m.available
54
+ if (m.flavor === 'subscription' && m.vendor) return configured.has(m.vendor)
55
+ return true
56
+ }
57
+
58
+ /** Compact context-window label, e.g. `200K`. */
59
+ export function contextLabel(tokens: number | undefined): string | undefined {
60
+ if (!tokens) return undefined
61
+ return tokens >= 1000 ? `${Math.round(tokens / 1000)}K` : `${tokens}`
62
+ }
63
+
64
+ /** One-line cost/quota suffix for the picker. */
65
+ export function costLabel(flavor: DisplayFlavor): string | undefined {
66
+ if (!flavor.cost) return undefined
67
+ const { inputPerMillion, outputPerMillion, currency } = flavor.cost
68
+ const body = `${inputPerMillion}/${outputPerMillion} ${currency} per Mtok`
69
+ return flavor.quotaBased ? `quota burn ~${body}` : body
70
+ }
71
+
72
+ /**
73
+ * The model picker catalog. Served by `GET /models`, where each model is already
74
+ * resolved to the flavour in use for this deployment (direct when the provider's
75
+ * key is configured, else the Cloudflare fallback). Fetched once and cached for
76
+ * the per-block picker, and used to label which model produced a step's output.
77
+ */
78
+ export const useModelsStore = defineStore('models', () => {
79
+ const api = useApi()
80
+ const models = ref<ModelOption[]>([])
81
+ const loaded = ref(false)
82
+ const loadedWorkspaceId = ref<string | null>(null)
83
+
84
+ /**
85
+ * Fetch the catalog. Pass a `workspaceId` for the per-workspace catalog (selectability
86
+ * reflects that workspace's configured keys/subscriptions); re-fetches when the
87
+ * workspace changes. Without one, the deployment-level catalog is loaded once.
88
+ */
89
+ async function ensureLoaded(workspaceId?: string) {
90
+ if (workspaceId) {
91
+ if (loaded.value && loadedWorkspaceId.value === workspaceId) return
92
+ models.value = await api.getWorkspaceModels(workspaceId)
93
+ loadedWorkspaceId.value = workspaceId
94
+ loaded.value = true
95
+ return
96
+ }
97
+ if (loaded.value) return
98
+ models.value = await api.getModels()
99
+ loaded.value = true
100
+ }
101
+
102
+ /** Force a re-fetch of the per-workspace catalog (e.g. after adding an API key). */
103
+ async function refresh(workspaceId: string) {
104
+ models.value = await api.getWorkspaceModels(workspaceId)
105
+ loadedWorkspaceId.value = workspaceId
106
+ loaded.value = true
107
+ }
108
+
109
+ const byId = computed(() => {
110
+ const map = new Map<string, ModelOption>()
111
+ for (const m of models.value) map.set(m.id, m)
112
+ return map
113
+ })
114
+
115
+ function getModel(id: string | undefined) {
116
+ return id ? byId.value.get(id) : undefined
117
+ }
118
+
119
+ /**
120
+ * Friendly label for a recorded `provider:model` identifier (as carried on a
121
+ * pipeline step). Matches it against the catalog's effective refs; falls back
122
+ * to the bare model id for anything not in the catalog (e.g. a pinned override).
123
+ */
124
+ function labelForRef(ref: string | undefined): string | undefined {
125
+ if (!ref) return undefined
126
+ const idx = ref.indexOf(':')
127
+ const provider = idx === -1 ? ref : ref.slice(0, idx)
128
+ const model = idx === -1 ? '' : ref.slice(idx + 1)
129
+ const hit = models.value.find((m) => m.provider === provider && m.model === model)
130
+ return hit ? `${hit.label} · ${hit.providerLabel}` : model || ref
131
+ }
132
+
133
+ return { models, loaded, ensureLoaded, refresh, byId, getModel, labelForRef }
134
+ })
@@ -0,0 +1,70 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref } from 'vue'
3
+ import type { Notification } from '~/types/domain'
4
+ import { useWorkspaceStore } from '~/stores/workspace'
5
+
6
+ /**
7
+ * Open, human-actionable notifications surfaced on the board (a PR awaiting a
8
+ * merge decision, a completed pipeline awaiting confirmation, CI that gave up).
9
+ * Hydrated from the workspace snapshot and patched live by the `notification`
10
+ * WorkspaceEvent (see `useWorkspaceStream`). The board renders an inbox + a
11
+ * per-block badge from `open` / `byBlock`.
12
+ */
13
+ export const useNotificationsStore = defineStore('notifications', () => {
14
+ const api = useApi()
15
+
16
+ /** All open notifications, newest-first. */
17
+ const open = ref<Notification[]>([])
18
+
19
+ /** Replace the cache from a server snapshot. */
20
+ function hydrate(notifications: Notification[]) {
21
+ open.value = [...notifications]
22
+ .filter((n) => n.status === 'open')
23
+ .sort((a, b) => b.createdAt - a.createdAt)
24
+ }
25
+
26
+ /**
27
+ * Patch one notification from a real-time event: an `open` one is inserted /
28
+ * replaced in place; a resolved one (acted/dismissed) is removed from the inbox.
29
+ */
30
+ function upsert(notification: Notification) {
31
+ const i = open.value.findIndex((n) => n.id === notification.id)
32
+ if (notification.status !== 'open') {
33
+ if (i >= 0) open.value.splice(i, 1)
34
+ return
35
+ }
36
+ if (i >= 0) open.value[i] = notification
37
+ else open.value.unshift(notification)
38
+ }
39
+
40
+ /** Open notifications for a given block (for the board card badge). */
41
+ const byBlock = computed<Record<string, Notification[]>>(() => {
42
+ const map: Record<string, Notification[]> = {}
43
+ for (const n of open.value) {
44
+ if (!n.blockId) continue
45
+ ;(map[n.blockId] ??= []).push(n)
46
+ }
47
+ return map
48
+ })
49
+
50
+ /** Total open count, for the toolbar badge. */
51
+ const count = computed(() => open.value.length)
52
+
53
+ /** Act on a notification (merge / confirm / retry); the board patches via the event. */
54
+ async function act(id: string) {
55
+ const ws = useWorkspaceStore()
56
+ const resolved = await api.actNotification(ws.requireId(), id)
57
+ upsert(resolved)
58
+ // The action (merge/confirm/retry) changed block/run state — reconcile fully.
59
+ await ws.refresh()
60
+ }
61
+
62
+ /** Dismiss a notification without acting. */
63
+ async function dismiss(id: string) {
64
+ const ws = useWorkspaceStore()
65
+ const resolved = await api.dismissNotification(ws.requireId(), id)
66
+ upsert(resolved)
67
+ }
68
+
69
+ return { open, hydrate, upsert, byBlock, count, act, dismiss }
70
+ })