@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,434 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref, computed } from 'vue'
3
+ import type { DocumentSourceKind, TaskSourceKind, LodLevel } from '~/types/domain'
4
+ import { zoomToLod, lodAtLeast } from '~/composables/useSemanticZoom'
5
+ import { useExecutionStore } from '~/stores/execution'
6
+ import { agentKindMeta } from '~/utils/catalog'
7
+
8
+ /** Transient UI state: selection, panels, zoom level. */
9
+ export const useUiStore = defineStore('ui', () => {
10
+ const selectedBlockId = ref<string | null>(null)
11
+ const focusBlockId = ref<string | null>(null)
12
+ const builderOpen = ref(false)
13
+ const decisionContext = ref<{ instanceId: string; decisionId: string } | null>(null)
14
+
15
+ // Document-source integration modals, keyed by source. `documentImport` and
16
+ // `spawnPreview` carry an optional target frame, so structure spawned from a
17
+ // frame's inspector lands inside that frame rather than creating new top-level
18
+ // frames. `documentConnect` carries the source whose connect form to show;
19
+ // `documentImport`'s source may be null to let the modal pick a connected one.
20
+ const documentConnect = ref<{ source: DocumentSourceKind } | null>(null)
21
+ const documentImport = ref<{
22
+ source: DocumentSourceKind | null
23
+ targetFrameId: string | null
24
+ } | null>(null)
25
+ const spawnPreview = ref<{
26
+ source: DocumentSourceKind
27
+ externalId: string
28
+ targetFrameId: string | null
29
+ } | null>(null)
30
+
31
+ // Task-source integration modals, keyed by source. `taskConnect` carries the
32
+ // source whose connect form to show; `taskImport`'s source may be null to let
33
+ // the modal pick a connected one (there is no spawn target — issues are linked
34
+ // to a block for context, not expanded into structure).
35
+ const taskConnect = ref<{ source: TaskSourceKind } | null>(null)
36
+ const taskImport = ref<{ source: TaskSourceKind | null } | null>(null)
37
+
38
+ // Add-task modal: the container (service frame or module) a new task is being
39
+ // added to, or null when closed. The user types the title + description; nothing
40
+ // is launched until they explicitly start the created task.
41
+ const addTaskContainerId = ref<string | null>(null)
42
+
43
+ // Add-recurring-pipeline modal: the service frame a new recurring pipeline is
44
+ // being added to, or null when closed (mirrors the add-task flow — a button on
45
+ // the frame opens it, scoped to that frame).
46
+ const addRecurringFrameId = ref<string | null>(null)
47
+
48
+ // Repo-bootstrap modal (manage reference architectures + launch a bootstrap).
49
+ const bootstrapOpen = ref(false)
50
+
51
+ // "Add a service from an existing GitHub repo" modal (no bootstrap run).
52
+ const addServiceOpen = ref(false)
53
+
54
+ // GitHub integration panel (connection management + repo/PR/issue browsing).
55
+ const githubOpen = ref(false)
56
+
57
+ // Slack integration panel (connect the account's Slack + per-workspace routing).
58
+ const slackOpen = ref(false)
59
+
60
+ // Prompt-fragment library panel (manage the board's best-practice catalog +
61
+ // linked guideline repos; ADR 0006).
62
+ const fragmentLibraryOpen = ref(false)
63
+
64
+ // Command bar (⌘K) — searchable launcher for every navbar action.
65
+ const commandBarOpen = ref(false)
66
+
67
+ // Workspace-settings panels: merge-threshold preset library + per-agent-kind
68
+ // default model overrides.
69
+ const mergeThresholdsOpen = ref(false)
70
+ // Workspace-settings panel: the run-timing escalation threshold + per-service task limit.
71
+ const workspaceSettingsOpen = ref(false)
72
+ const datadogOpen = ref(false)
73
+ const modelDefaultsOpen = ref(false)
74
+ // Workspace-settings panel: the default service-fragment selection new services inherit.
75
+ const serviceFragmentDefaultsOpen = ref(false)
76
+ // LLM-vendor subscription credentials (the token pool powering the Claude Code
77
+ // / Codex harnesses).
78
+ const vendorCredentialsOpen = ref(false)
79
+ // Per-user settings panel: the signed-in user's own-machine local model runners.
80
+ const localModelsOpen = ref(false)
81
+
82
+ // Dedicated result-view overlay: a step whose agent kind declares a bespoke
83
+ // visualization (via the archetype's `resultView`) opens here instead of the generic
84
+ // prose step-detail panel. `view` is the registry id (e.g. 'requirements-review');
85
+ // `blockId` is always set; `instanceId`/`stepIndex` are present on the pipeline path and
86
+ // null for an off-path open (e.g. the inspector's pre-start requirements review).
87
+ const resultView = ref<{
88
+ view: string
89
+ blockId: string
90
+ instanceId: string | null
91
+ stepIndex: number | null
92
+ } | null>(null)
93
+
94
+ // Agent step-detail overlay: which pipeline step (a run instance + step index)
95
+ // a human is inspecting, or null when closed. The overlay resolves the step
96
+ // from the execution store so it stays live; it shows the step's metadata
97
+ // (model, state, progress, subtasks, …) and — when the agent produced prose —
98
+ // a reader for it (ToC + collapsible sections).
99
+ const stepDetail = ref<{ instanceId: string; stepIndex: number } | null>(null)
100
+
101
+ // LLM observability panel: which run (execution instance) a human is inspecting
102
+ // the per-call model activity for, or null when closed. The panel loads the full
103
+ // per-call detail from the observability store on open.
104
+ const observabilityInstanceId = ref<string | null>(null)
105
+
106
+ /** Current canvas zoom (driven by Vue Flow viewport). */
107
+ const zoom = ref(1)
108
+
109
+ const lod = computed<LodLevel>(() => zoomToLod(zoom.value))
110
+
111
+ /** Frames the user has manually expanded to reveal their tasks. */
112
+ const expandedFrames = ref<Set<string>>(new Set())
113
+
114
+ function toggleFrame(id: string) {
115
+ const next = new Set(expandedFrames.value)
116
+ if (next.has(id)) next.delete(id)
117
+ else next.add(id)
118
+ expandedFrames.value = next
119
+ }
120
+
121
+ function expandFrame(id: string) {
122
+ if (expandedFrames.value.has(id)) return
123
+ expandedFrames.value = new Set(expandedFrames.value).add(id)
124
+ }
125
+
126
+ /** A frame shows its tasks when manually expanded OR once zoomed in to `close`
127
+ * or any deeper band (`steps`/`subtasks` drill further into those tasks). */
128
+ function isFrameExpanded(id: string) {
129
+ return expandedFrames.value.has(id) || lodAtLeast(lod.value, 'close')
130
+ }
131
+
132
+ function select(id: string | null) {
133
+ selectedBlockId.value = id
134
+ }
135
+
136
+ function focus(id: string | null) {
137
+ focusBlockId.value = id
138
+ }
139
+
140
+ function openBuilder() {
141
+ builderOpen.value = true
142
+ }
143
+
144
+ function openDecision(instanceId: string, decisionId: string) {
145
+ decisionContext.value = { instanceId, decisionId }
146
+ }
147
+
148
+ function closeDecision() {
149
+ decisionContext.value = null
150
+ }
151
+
152
+ /**
153
+ * Open a pending approval gate in the conclusions reader (approval mode). Resolves
154
+ * the step index from the gate id so every board/inspector entry point can keep
155
+ * passing the approval id it already has.
156
+ */
157
+ function openApprovalDetail(instanceId: string, approvalId: string) {
158
+ const execution = useExecutionStore()
159
+ const instance = execution.getInstance(instanceId)
160
+ const idx = instance?.steps.findIndex((s) => s.approval?.id === approvalId) ?? -1
161
+ if (idx >= 0) dispatchStepView(instanceId, idx)
162
+ }
163
+
164
+ /**
165
+ * Open a pipeline step: route it to its agent kind's DEDICATED result window when the
166
+ * archetype declares one (the universal `resultView` seam), else the generic prose
167
+ * step-detail panel. This is the single dispatch every board/inspector entry point uses,
168
+ * so adding a bespoke window for a new agent is just declaring `resultView` + registering
169
+ * a component — no caller changes.
170
+ */
171
+ function dispatchStepView(instanceId: string, stepIndex: number) {
172
+ const execution = useExecutionStore()
173
+ const instance = execution.getInstance(instanceId)
174
+ const step = instance?.steps[stepIndex]
175
+ // A step that actually ran the consensus mechanism opens the dedicated Consensus
176
+ // Session window, regardless of its kind's normal result view — consensus is an
177
+ // execution MODE on a kind, not a kind, so it can't be a static archetype `resultView`.
178
+ const view = step?.consensus?.enabled
179
+ ? 'consensus-session'
180
+ : step
181
+ ? agentKindMeta(step.agentKind).resultView
182
+ : undefined
183
+ if (view && instance) {
184
+ resultView.value = { view, blockId: instance.blockId, instanceId, stepIndex }
185
+ return
186
+ }
187
+ stepDetail.value = { instanceId, stepIndex }
188
+ }
189
+
190
+ function openDocumentConnect(source: DocumentSourceKind) {
191
+ documentConnect.value = { source }
192
+ }
193
+ function closeDocumentConnect() {
194
+ documentConnect.value = null
195
+ }
196
+ function openDocumentImport(
197
+ targetFrameId: string | null = null,
198
+ source: DocumentSourceKind | null = null,
199
+ ) {
200
+ documentImport.value = { source, targetFrameId }
201
+ }
202
+ function closeDocumentImport() {
203
+ documentImport.value = null
204
+ }
205
+ function openSpawnPreview(
206
+ source: DocumentSourceKind,
207
+ externalId: string,
208
+ targetFrameId: string | null = null,
209
+ ) {
210
+ spawnPreview.value = { source, externalId, targetFrameId }
211
+ }
212
+ function closeSpawnPreview() {
213
+ spawnPreview.value = null
214
+ }
215
+ function openTaskConnect(source: TaskSourceKind) {
216
+ taskConnect.value = { source }
217
+ }
218
+ function closeTaskConnect() {
219
+ taskConnect.value = null
220
+ }
221
+ function openTaskImport(source: TaskSourceKind | null = null) {
222
+ taskImport.value = { source }
223
+ }
224
+ function closeTaskImport() {
225
+ taskImport.value = null
226
+ }
227
+ function openAddTask(containerId: string) {
228
+ addTaskContainerId.value = containerId
229
+ }
230
+ function closeAddTask() {
231
+ addTaskContainerId.value = null
232
+ }
233
+ function openAddRecurring(frameId: string) {
234
+ addRecurringFrameId.value = frameId
235
+ }
236
+ function closeAddRecurring() {
237
+ addRecurringFrameId.value = null
238
+ }
239
+ function openBootstrap() {
240
+ bootstrapOpen.value = true
241
+ }
242
+ function closeBootstrap() {
243
+ bootstrapOpen.value = false
244
+ }
245
+ function openAddService() {
246
+ addServiceOpen.value = true
247
+ }
248
+ function closeAddService() {
249
+ addServiceOpen.value = false
250
+ }
251
+ function openGitHub() {
252
+ githubOpen.value = true
253
+ }
254
+ function closeGitHub() {
255
+ githubOpen.value = false
256
+ }
257
+ function openSlack() {
258
+ slackOpen.value = true
259
+ }
260
+ function closeSlack() {
261
+ slackOpen.value = false
262
+ }
263
+ function openFragmentLibrary() {
264
+ fragmentLibraryOpen.value = true
265
+ }
266
+ function closeFragmentLibrary() {
267
+ fragmentLibraryOpen.value = false
268
+ }
269
+ function openCommandBar() {
270
+ commandBarOpen.value = true
271
+ }
272
+ function closeCommandBar() {
273
+ commandBarOpen.value = false
274
+ }
275
+ function toggleCommandBar() {
276
+ commandBarOpen.value = !commandBarOpen.value
277
+ }
278
+ function openMergeThresholds() {
279
+ mergeThresholdsOpen.value = true
280
+ }
281
+ function closeMergeThresholds() {
282
+ mergeThresholdsOpen.value = false
283
+ }
284
+ function openWorkspaceSettings() {
285
+ workspaceSettingsOpen.value = true
286
+ }
287
+ function closeWorkspaceSettings() {
288
+ workspaceSettingsOpen.value = false
289
+ }
290
+ function openDatadog() {
291
+ datadogOpen.value = true
292
+ }
293
+ function closeDatadog() {
294
+ datadogOpen.value = false
295
+ }
296
+ function openModelDefaults() {
297
+ modelDefaultsOpen.value = true
298
+ }
299
+ function closeModelDefaults() {
300
+ modelDefaultsOpen.value = false
301
+ }
302
+ function openServiceFragmentDefaults() {
303
+ serviceFragmentDefaultsOpen.value = true
304
+ }
305
+ function closeServiceFragmentDefaults() {
306
+ serviceFragmentDefaultsOpen.value = false
307
+ }
308
+ function openVendorCredentials() {
309
+ vendorCredentialsOpen.value = true
310
+ }
311
+ function closeVendorCredentials() {
312
+ vendorCredentialsOpen.value = false
313
+ }
314
+ function openLocalModels() {
315
+ localModelsOpen.value = true
316
+ }
317
+ function closeLocalModels() {
318
+ localModelsOpen.value = false
319
+ }
320
+ function openRequirementReview(blockId: string) {
321
+ resultView.value = { view: 'requirements-review', blockId, instanceId: null, stepIndex: null }
322
+ }
323
+ function openClarityReview(blockId: string) {
324
+ resultView.value = { view: 'clarity-review', blockId, instanceId: null, stepIndex: null }
325
+ }
326
+ function closeResultView() {
327
+ resultView.value = null
328
+ }
329
+ // Kept name for the requirements window's close handler.
330
+ const closeRequirementReview = closeResultView
331
+ function openStepDetail(instanceId: string, stepIndex: number) {
332
+ dispatchStepView(instanceId, stepIndex)
333
+ }
334
+ function closeStepDetail() {
335
+ stepDetail.value = null
336
+ }
337
+ function openObservability(instanceId: string) {
338
+ observabilityInstanceId.value = instanceId
339
+ }
340
+ function closeObservability() {
341
+ observabilityInstanceId.value = null
342
+ }
343
+
344
+ return {
345
+ selectedBlockId,
346
+ focusBlockId,
347
+ builderOpen,
348
+ decisionContext,
349
+ documentConnect,
350
+ documentImport,
351
+ spawnPreview,
352
+ taskConnect,
353
+ taskImport,
354
+ addTaskContainerId,
355
+ addRecurringFrameId,
356
+ bootstrapOpen,
357
+ addServiceOpen,
358
+ githubOpen,
359
+ slackOpen,
360
+ fragmentLibraryOpen,
361
+ commandBarOpen,
362
+ mergeThresholdsOpen,
363
+ workspaceSettingsOpen,
364
+ datadogOpen,
365
+ modelDefaultsOpen,
366
+ serviceFragmentDefaultsOpen,
367
+ vendorCredentialsOpen,
368
+ localModelsOpen,
369
+ resultView,
370
+ closeResultView,
371
+ stepDetail,
372
+ observabilityInstanceId,
373
+ zoom,
374
+ lod,
375
+ expandedFrames,
376
+ toggleFrame,
377
+ expandFrame,
378
+ isFrameExpanded,
379
+ select,
380
+ focus,
381
+ openBuilder,
382
+ openDecision,
383
+ closeDecision,
384
+ openApprovalDetail,
385
+ openDocumentConnect,
386
+ closeDocumentConnect,
387
+ openDocumentImport,
388
+ closeDocumentImport,
389
+ openSpawnPreview,
390
+ closeSpawnPreview,
391
+ openTaskConnect,
392
+ closeTaskConnect,
393
+ openTaskImport,
394
+ closeTaskImport,
395
+ openAddTask,
396
+ closeAddTask,
397
+ openAddRecurring,
398
+ closeAddRecurring,
399
+ openBootstrap,
400
+ closeBootstrap,
401
+ openAddService,
402
+ closeAddService,
403
+ openGitHub,
404
+ closeGitHub,
405
+ openSlack,
406
+ closeSlack,
407
+ openFragmentLibrary,
408
+ closeFragmentLibrary,
409
+ openCommandBar,
410
+ closeCommandBar,
411
+ toggleCommandBar,
412
+ openMergeThresholds,
413
+ closeMergeThresholds,
414
+ openWorkspaceSettings,
415
+ closeWorkspaceSettings,
416
+ openDatadog,
417
+ closeDatadog,
418
+ openModelDefaults,
419
+ closeModelDefaults,
420
+ openServiceFragmentDefaults,
421
+ closeServiceFragmentDefaults,
422
+ openVendorCredentials,
423
+ closeVendorCredentials,
424
+ openLocalModels,
425
+ closeLocalModels,
426
+ openRequirementReview,
427
+ openClarityReview,
428
+ closeRequirementReview,
429
+ openStepDetail,
430
+ closeStepDetail,
431
+ openObservability,
432
+ closeObservability,
433
+ }
434
+ })
@@ -0,0 +1,54 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref } from 'vue'
3
+ import type { SubscriptionVendor, VendorCredential } from '~/types/domain'
4
+
5
+ /**
6
+ * The workspace's connected LLM-vendor subscription credentials (the token pool
7
+ * powering the Claude Code / Codex harnesses). Loaded from
8
+ * `GET /workspaces/:ws/vendor-credentials`; tokens are write-only so only
9
+ * metadata + rolling-window usage is ever returned. `configuredVendors` drives
10
+ * the model picker: a dual-mode model (GLM/Kimi) collapses to its subscription
11
+ * flavour, and a subscription-only model is enabled, once its vendor is here.
12
+ */
13
+ export const useVendorCredentialsStore = defineStore('vendorCredentials', () => {
14
+ const api = useApi()
15
+ const credentials = ref<VendorCredential[]>([])
16
+ const workspaceId = ref<string | null>(null)
17
+ const loading = ref(false)
18
+
19
+ async function load(ws: string) {
20
+ workspaceId.value = ws
21
+ loading.value = true
22
+ try {
23
+ const { credentials: list } = await api.listVendorCredentials(ws)
24
+ credentials.value = list
25
+ } finally {
26
+ loading.value = false
27
+ }
28
+ }
29
+
30
+ async function add(input: { vendor: SubscriptionVendor; label: string; token: string }) {
31
+ if (!workspaceId.value) return
32
+ const created = await api.addVendorCredential(workspaceId.value, input)
33
+ credentials.value = [...credentials.value, created]
34
+ }
35
+
36
+ async function remove(id: string) {
37
+ if (!workspaceId.value) return
38
+ await api.removeVendorCredential(workspaceId.value, id)
39
+ credentials.value = credentials.value.filter((c) => c.id !== id)
40
+ }
41
+
42
+ /** The set of vendors with at least one connected token. */
43
+ const configuredVendors = computed(() => new Set(credentials.value.map((c) => c.vendor)))
44
+
45
+ function hasVendor(vendor: SubscriptionVendor | undefined): boolean {
46
+ return vendor ? configuredVendors.value.has(vendor) : false
47
+ }
48
+
49
+ function forVendor(vendor: SubscriptionVendor) {
50
+ return credentials.value.filter((c) => c.vendor === vendor)
51
+ }
52
+
53
+ return { credentials, loading, load, add, remove, configuredVendors, hasVendor, forVendor }
54
+ })
@@ -0,0 +1,215 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref } from 'vue'
3
+ import type { SpendStatus, Workspace, WorkspaceSnapshot } from '~/types/domain'
4
+ import { useAccountsStore } from '~/stores/accounts'
5
+ import { useBoardStore } from '~/stores/board'
6
+ import { usePipelinesStore } from '~/stores/pipelines'
7
+ import { useExecutionStore } from '~/stores/execution'
8
+ import { useAgentRunsStore } from '~/stores/agentRuns'
9
+ import { useNotificationsStore } from '~/stores/notifications'
10
+ import { useMergePresetsStore } from '~/stores/mergePresets'
11
+ import { useWorkspaceSettingsStore } from '~/stores/workspaceSettings'
12
+ import { useAgentConfigStore } from '~/stores/agentConfig'
13
+ import { useModelDefaultsStore } from '~/stores/modelDefaults'
14
+ import { useServiceFragmentDefaultsStore } from '~/stores/serviceFragmentDefaults'
15
+ import { useRecurringPipelinesStore } from '~/stores/recurringPipelines'
16
+ import { useServicesStore } from '~/stores/services'
17
+ import { useTrackerStore } from '~/stores/tracker'
18
+
19
+ /**
20
+ * Owns the active workspace and bootstraps the app against the backend. On load
21
+ * it resolves the user's accounts, lists the boards in the active account, opens
22
+ * the persisted one (or the first, or a fresh seeded board), and hydrates the
23
+ * board / pipelines / execution stores from its snapshot.
24
+ *
25
+ * Boards are scoped to an account: switching account re-scopes the board list,
26
+ * and new boards are stamped with the active account so a team can keep org
27
+ * boards separate from personal ones. Only the active workspace id is persisted —
28
+ * all board data lives on the server.
29
+ */
30
+ export const useWorkspaceStore = defineStore(
31
+ 'workspace',
32
+ () => {
33
+ const api = useApi()
34
+
35
+ /** Active workspace id (persisted so a reload reopens the same board). */
36
+ const workspaceId = ref<string | null>(null)
37
+ /** Every board visible to the user, across the accounts they belong to. */
38
+ const workspaces = ref<Workspace[]>([])
39
+ /** True once the initial snapshot has been loaded and stores hydrated. */
40
+ const ready = ref(false)
41
+ /** Set when bootstrap fails so the UI can show a retry. */
42
+ const error = ref<string | null>(null)
43
+ /** Latest spend-safeguard status from the server (null until first load). */
44
+ const spend = ref<SpendStatus | null>(null)
45
+
46
+ /** The boards belonging to the active account (all boards when auth is off). */
47
+ const accountWorkspaces = computed(() => {
48
+ const accounts = useAccountsStore()
49
+ if (!accounts.enabled || !accounts.activeAccountId) return workspaces.value
50
+ return workspaces.value.filter((w) => w.accountId === accounts.activeAccountId)
51
+ })
52
+
53
+ /** The active board's row (for the switcher label). */
54
+ const activeWorkspace = computed(
55
+ () => workspaces.value.find((w) => w.id === workspaceId.value) ?? null,
56
+ )
57
+
58
+ /** Push a snapshot into the data stores. */
59
+ function hydrate(snapshot: WorkspaceSnapshot) {
60
+ workspaceId.value = snapshot.workspace.id
61
+ spend.value = snapshot.spend ?? null
62
+ // Keep the board list in step (e.g. a freshly created board, or a rename).
63
+ const i = workspaces.value.findIndex((w) => w.id === snapshot.workspace.id)
64
+ if (i >= 0) workspaces.value[i] = snapshot.workspace
65
+ else workspaces.value.unshift(snapshot.workspace)
66
+ useBoardStore().hydrate(snapshot.blocks)
67
+ usePipelinesStore().hydrate(snapshot.pipelines)
68
+ useExecutionStore().hydrate(snapshot.executions)
69
+ useAgentRunsStore().hydrate(snapshot.bootstrapJobs ?? [])
70
+ useNotificationsStore().hydrate(snapshot.notifications ?? [])
71
+ useMergePresetsStore().hydrate(snapshot.mergePresets ?? [])
72
+ useWorkspaceSettingsStore().hydrate(snapshot.settings)
73
+ useAgentConfigStore().hydrate(snapshot.agentConfigCatalog ?? [])
74
+ useModelDefaultsStore().hydrate(snapshot.modelDefaults?.defaults)
75
+ useModelDefaultsStore().hydrateDeployment(snapshot.deploymentModelDefaults)
76
+ useServiceFragmentDefaultsStore().hydrate(snapshot.serviceFragmentDefaults?.fragmentIds)
77
+ useRecurringPipelinesStore().hydrate(snapshot.recurringPipelines ?? [])
78
+ useTrackerStore().hydrate(snapshot.trackerSettings)
79
+ useServicesStore().hydrate(snapshot.mounts ?? [], snapshot.serviceCatalog ?? [])
80
+ }
81
+
82
+ /** Resolve accounts + boards, then open the right board for the active account. */
83
+ async function init() {
84
+ ready.value = false
85
+ error.value = null
86
+ try {
87
+ // Accounts are an auth concept — empty in dev, which leaves boards unscoped.
88
+ await useAccountsStore()
89
+ .load()
90
+ .catch(() => {})
91
+ workspaces.value = await api.listWorkspaces()
92
+ await resolveActiveBoard()
93
+ ready.value = true
94
+ } catch (e) {
95
+ error.value = e instanceof Error ? e.message : 'Failed to reach the backend.'
96
+ }
97
+ }
98
+
99
+ /** Open the persisted board (aligning the active account to it), else pick/create one. */
100
+ async function resolveActiveBoard() {
101
+ const accounts = useAccountsStore()
102
+ if (workspaceId.value) {
103
+ const existing = workspaces.value.find((w) => w.id === workspaceId.value)
104
+ if (existing) {
105
+ if (accounts.enabled && existing.accountId) accounts.activeAccountId = existing.accountId
106
+ hydrate(await api.getWorkspace(existing.id))
107
+ return
108
+ }
109
+ // Persisted board is gone (deleted, or now another tenant's) — fall through.
110
+ workspaceId.value = null
111
+ }
112
+ const first = accountWorkspaces.value[0]
113
+ if (first) {
114
+ hydrate(await api.getWorkspace(first.id))
115
+ } else {
116
+ hydrate(
117
+ await api.createWorkspace({
118
+ seed: false,
119
+ accountId: accounts.activeAccountId ?? undefined,
120
+ }),
121
+ )
122
+ }
123
+ }
124
+
125
+ /** Switch to another board (within reach of the active account). */
126
+ async function switchTo(id: string) {
127
+ if (id === workspaceId.value) return
128
+ hydrate(await api.getWorkspace(id))
129
+ }
130
+
131
+ /** Switch the active account, then open one of its boards (creating one if needed). */
132
+ async function selectAccount(id: string) {
133
+ const accounts = useAccountsStore()
134
+ if (id === accounts.activeAccountId) return
135
+ accounts.switchTo(id)
136
+ workspaceId.value = null
137
+ await resolveActiveBoard()
138
+ }
139
+
140
+ /** Create a new board in the active account and open it. */
141
+ async function create(name?: string, description?: string) {
142
+ const accounts = useAccountsStore()
143
+ const snapshot = await api.createWorkspace({
144
+ seed: false,
145
+ name,
146
+ description,
147
+ accountId: accounts.activeAccountId ?? undefined,
148
+ })
149
+ hydrate(snapshot)
150
+ return snapshot.workspace
151
+ }
152
+
153
+ /** Rename a board and/or update its description. */
154
+ async function update(id: string, patch: { name?: string; description?: string | null }) {
155
+ const updated = await api.updateWorkspace(id, patch)
156
+ const i = workspaces.value.findIndex((w) => w.id === id)
157
+ if (i >= 0) workspaces.value[i] = updated
158
+ return updated
159
+ }
160
+
161
+ /** Rename a board (kept for the existing rename callers). */
162
+ async function rename(id: string, name: string) {
163
+ return update(id, { name })
164
+ }
165
+
166
+ /** Delete a board; if it was active, fall back to another in the account. */
167
+ async function remove(id: string) {
168
+ await api.deleteWorkspace(id)
169
+ workspaces.value = workspaces.value.filter((w) => w.id !== id)
170
+ if (workspaceId.value === id) {
171
+ workspaceId.value = null
172
+ await resolveActiveBoard()
173
+ }
174
+ }
175
+
176
+ /** Re-fetch the snapshot and re-hydrate (after mutations and on stream (re)connect). */
177
+ async function refresh() {
178
+ if (!workspaceId.value) return
179
+ hydrate(await api.getWorkspace(workspaceId.value))
180
+ }
181
+
182
+ /** The active workspace id, or throw if the app isn't bootstrapped yet. */
183
+ function requireId(): string {
184
+ if (!workspaceId.value) throw new Error('No active workspace')
185
+ return workspaceId.value
186
+ }
187
+
188
+ /** Resume runs paused by the spend safeguard, then refresh the snapshot. */
189
+ async function resumeSpend() {
190
+ await api.resumeSpend(requireId())
191
+ await refresh()
192
+ }
193
+
194
+ return {
195
+ workspaceId,
196
+ workspaces,
197
+ accountWorkspaces,
198
+ activeWorkspace,
199
+ ready,
200
+ error,
201
+ spend,
202
+ init,
203
+ switchTo,
204
+ selectAccount,
205
+ create,
206
+ update,
207
+ rename,
208
+ remove,
209
+ refresh,
210
+ requireId,
211
+ resumeSpend,
212
+ }
213
+ },
214
+ { persist: { pick: ['workspaceId'] } },
215
+ )