@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,273 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref, computed } from 'vue'
3
+ import type {
4
+ Decision,
5
+ ExecutionInstance,
6
+ Pipeline,
7
+ PipelineStep,
8
+ StepApproval,
9
+ } from '~/types/domain'
10
+ import type { IterationCapChoice, ReviewComment } from '~/types/execution'
11
+ import { useWorkspaceStore } from '~/stores/workspace'
12
+
13
+ /**
14
+ * Running pipeline instances. The simulation engine lives on the backend: this
15
+ * store mirrors the server's executions and drives them via the API. Commands
16
+ * call the worker and then refresh the workspace snapshot, since advancing an
17
+ * execution also rolls status/progress up onto its block server-side.
18
+ */
19
+ export const useExecutionStore = defineStore('execution', () => {
20
+ const api = useApi()
21
+ const instances = ref<ExecutionInstance[]>([])
22
+
23
+ /** Replace the cached executions with a server snapshot. */
24
+ function hydrate(next: ExecutionInstance[]) {
25
+ instances.value = next
26
+ }
27
+
28
+ /** Insert or replace a single execution instance pushed by the event stream. */
29
+ function upsert(instance: ExecutionInstance) {
30
+ const i = instances.value.findIndex((e) => e.id === instance.id)
31
+ if (i >= 0) instances.value[i] = instance
32
+ else instances.value.push(instance)
33
+ }
34
+
35
+ const byId = computed(() => {
36
+ const map = new Map<string, ExecutionInstance>()
37
+ for (const e of instances.value) map.set(e.id, e)
38
+ return map
39
+ })
40
+
41
+ function getInstance(id: string | null | undefined) {
42
+ return id ? byId.value.get(id) : undefined
43
+ }
44
+
45
+ function getByBlock(blockId: string) {
46
+ return instances.value.find((e) => e.blockId === blockId)
47
+ }
48
+
49
+ /** How many decisions anywhere are awaiting a human. */
50
+ const pendingDecisionCount = computed(() =>
51
+ instances.value.reduce(
52
+ (n, e) => n + e.steps.filter((s) => s.decision && !s.decision.chosen).length,
53
+ 0,
54
+ ),
55
+ )
56
+
57
+ /** All currently-unresolved decisions across all runs (for the toolbar/queue). */
58
+ const openDecisions = computed(() => {
59
+ const out: {
60
+ instanceId: string
61
+ blockId: string
62
+ decision: Decision
63
+ agentKind: PipelineStep['agentKind']
64
+ }[] = []
65
+ for (const e of instances.value) {
66
+ for (const s of e.steps) {
67
+ if (s.decision && !s.decision.chosen) {
68
+ out.push({
69
+ instanceId: e.id,
70
+ blockId: e.blockId,
71
+ decision: s.decision,
72
+ agentKind: s.agentKind,
73
+ })
74
+ }
75
+ }
76
+ }
77
+ return out
78
+ })
79
+
80
+ /** All currently-pending approval gates across all runs (board badges/queue). */
81
+ const openApprovals = computed(() => {
82
+ const out: {
83
+ instanceId: string
84
+ blockId: string
85
+ approval: StepApproval
86
+ agentKind: PipelineStep['agentKind']
87
+ }[] = []
88
+ for (const e of instances.value) {
89
+ for (const s of e.steps) {
90
+ if (s.approval?.status === 'pending') {
91
+ out.push({
92
+ instanceId: e.id,
93
+ blockId: e.blockId,
94
+ approval: s.approval,
95
+ agentKind: s.agentKind,
96
+ })
97
+ }
98
+ }
99
+ }
100
+ return out
101
+ })
102
+
103
+ /**
104
+ * Start `pipeline` against a block; the server marks the block in-progress. A block
105
+ * pinned to an individual-usage model (Claude) needs the initiator's personal
106
+ * password — supplied transparently from the local cache, and prompted via the
107
+ * credential modal (then retried) when the server replies 428.
108
+ */
109
+ async function start(blockId: string, pipeline: Pipeline): Promise<boolean> {
110
+ const ws = useWorkspaceStore()
111
+ const personal = usePersonalSubscriptionsStore()
112
+ // Returns false when the user cancels the personal-password prompt (the run never
113
+ // started), so an optimistic caller can revert its "Starting…" state.
114
+ return personal.withCredential(async (password) => {
115
+ await api.startExecution(ws.requireId(), blockId, { pipelineId: pipeline.id }, password)
116
+ await ws.refresh()
117
+ })
118
+ }
119
+
120
+ // Interacting with a running individual-usage run (resolve/approve/request-changes) rides
121
+ // the CACHED personal password along transparently so the server can re-mint the run's
122
+ // short-TTL activation before advancing — no prompt here (the user is only re-prompted on
123
+ // start/retry, once the cache lapses). For a non-individual run the server ignores it.
124
+ async function resolveDecision(instanceId: string, decisionId: string, choice: string) {
125
+ const ws = useWorkspaceStore()
126
+ const personal = usePersonalSubscriptionsStore()
127
+ await api.resolveDecision(
128
+ ws.requireId(),
129
+ instanceId,
130
+ decisionId,
131
+ { choice },
132
+ personal.getCachedPassword(),
133
+ )
134
+ await ws.refresh()
135
+ }
136
+
137
+ /** Approve a step's gated proposal (optionally edited); the run advances. */
138
+ async function approveStep(instanceId: string, approvalId: string, proposal?: string) {
139
+ const ws = useWorkspaceStore()
140
+ const personal = usePersonalSubscriptionsStore()
141
+ await api.approveStep(
142
+ ws.requireId(),
143
+ instanceId,
144
+ approvalId,
145
+ { proposal },
146
+ personal.getCachedPassword(),
147
+ )
148
+ await ws.refresh()
149
+ }
150
+
151
+ /** Request changes on a gated proposal; the step re-runs with the review. */
152
+ async function requestStepChanges(
153
+ instanceId: string,
154
+ approvalId: string,
155
+ review: { feedback?: string; comments?: ReviewComment[] },
156
+ ) {
157
+ const ws = useWorkspaceStore()
158
+ const personal = usePersonalSubscriptionsStore()
159
+ await api.requestStepChanges(
160
+ ws.requireId(),
161
+ instanceId,
162
+ approvalId,
163
+ review,
164
+ personal.getCachedPassword(),
165
+ )
166
+ await ws.refresh()
167
+ }
168
+
169
+ /** Reject a gated proposal; the run stops entirely (a retryable failure). */
170
+ async function rejectStep(instanceId: string, approvalId: string, reason?: string) {
171
+ const ws = useWorkspaceStore()
172
+ await api.rejectStep(ws.requireId(), instanceId, approvalId, { reason })
173
+ await ws.refresh()
174
+ }
175
+
176
+ /**
177
+ * Resolve a companion step parked at its rework cap: extra-round (one more pass) /
178
+ * proceed (advance with the current output) / stop-reset (cancel + reset the task).
179
+ * Rides the cached personal password so the server can re-mint the run's activation
180
+ * before re-dispatching on extra-round/proceed.
181
+ */
182
+ async function resolveCompanionExceeded(
183
+ instanceId: string,
184
+ approvalId: string,
185
+ choice: IterationCapChoice,
186
+ ) {
187
+ const ws = useWorkspaceStore()
188
+ const personal = usePersonalSubscriptionsStore()
189
+ await api.resolveCompanionExceeded(
190
+ ws.requireId(),
191
+ instanceId,
192
+ approvalId,
193
+ { choice },
194
+ personal.getCachedPassword(),
195
+ )
196
+ await ws.refresh()
197
+ }
198
+
199
+ /** How many approval gates anywhere are awaiting a human. */
200
+ const pendingApprovalCount = computed(() =>
201
+ instances.value.reduce(
202
+ (n, e) => n + e.steps.filter((s) => s.approval?.status === 'pending').length,
203
+ 0,
204
+ ),
205
+ )
206
+
207
+ /** Merge an open PR (a task in `pr_ready`) — the server completes the task. */
208
+ async function mergePr(blockId: string) {
209
+ const ws = useWorkspaceStore()
210
+ await api.mergeBlock(ws.requireId(), blockId)
211
+ await ws.refresh()
212
+ }
213
+
214
+ /**
215
+ * Restart a run from a chosen step: the server re-runs from `stepIndex` onward
216
+ * (resetting that step + later steps' iteration counters) while preserving the
217
+ * earlier steps' outputs as handoff context, and re-drives a fresh run. Like
218
+ * start/retry it may dispatch an individual-usage (Claude) step, so it rides the
219
+ * initiator's personal password — prompted (then retried) on a 428. Returns false
220
+ * when the user cancels that prompt (nothing was restarted).
221
+ */
222
+ async function restartFromStep(instanceId: string, stepIndex: number): Promise<boolean> {
223
+ const ws = useWorkspaceStore()
224
+ const personal = usePersonalSubscriptionsStore()
225
+ return personal.withCredential(async (password) => {
226
+ await api.restartFromStep(ws.requireId(), instanceId, stepIndex, password)
227
+ await ws.refresh()
228
+ })
229
+ }
230
+
231
+ /** Cancel the execution running against a block and reset it to planned. */
232
+ async function cancel(blockId: string) {
233
+ const ws = useWorkspaceStore()
234
+ await api.cancelExecution(ws.requireId(), blockId)
235
+ instances.value = instances.value.filter((e) => e.blockId !== blockId)
236
+ await ws.refresh()
237
+ }
238
+
239
+ /**
240
+ * Stop a running execution WITHOUT deleting it: halts the container + durable driver
241
+ * and records the run as `cancelled` (a retryable failure), leaving the block
242
+ * `blocked`. Unlike {@link cancel} the run is kept — its steps/output stay readable on
243
+ * the board and it can be retried from where it stopped. `runId` is the execution id.
244
+ */
245
+ async function stop(runId: string) {
246
+ const ws = useWorkspaceStore()
247
+ await api.stopAgentRun(ws.requireId(), runId)
248
+ await ws.refresh()
249
+ }
250
+
251
+ return {
252
+ instances,
253
+ hydrate,
254
+ upsert,
255
+ byId,
256
+ getInstance,
257
+ getByBlock,
258
+ pendingDecisionCount,
259
+ openDecisions,
260
+ openApprovals,
261
+ pendingApprovalCount,
262
+ start,
263
+ resolveDecision,
264
+ approveStep,
265
+ requestStepChanges,
266
+ rejectStep,
267
+ resolveCompanionExceeded,
268
+ restartFromStep,
269
+ mergePr,
270
+ cancel,
271
+ stop,
272
+ }
273
+ })
@@ -0,0 +1,147 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref } from 'vue'
3
+ import type {
4
+ CreatePromptFragmentInput,
5
+ FragmentSource,
6
+ LinkFragmentSourceInput,
7
+ PromptFragment,
8
+ ResolvedFragment,
9
+ UpdatePromptFragmentInput,
10
+ } from '~/types/domain'
11
+ import { useWorkspaceStore } from '~/stores/workspace'
12
+
13
+ /**
14
+ * Prompt-fragment library state (ADR 0006), scoped to the active board. Holds the
15
+ * board's own (workspace-tier) fragments, its linked guideline repos, and the
16
+ * merged catalog an agent actually sees (built-in ∪ account ∪ workspace). The
17
+ * management surface targets the workspace tier; the resolved read is what every
18
+ * agent run is selected from. `available` mirrors the backend's opt-in gate: a
19
+ * 503 from the resolve probe means the feature is off and the UI hides its entry.
20
+ */
21
+ export const useFragmentLibraryStore = defineStore('fragmentLibrary', () => {
22
+ const api = useApi()
23
+ const workspace = useWorkspaceStore()
24
+
25
+ /** null = not probed yet; true/false = library on/off. */
26
+ const available = ref<boolean | null>(null)
27
+ /** This board's hand-authored + sourced fragments (workspace tier, raw). */
28
+ const fragments = ref<PromptFragment[]>([])
29
+ /** The merged catalog an agent sees (with each entry's winning tier). */
30
+ const resolved = ref<ResolvedFragment[]>([])
31
+ /** Linked guideline repos for this board. */
32
+ const sources = ref<FragmentSource[]>([])
33
+ /** Per-source "changes available" counts from the last status check. */
34
+ const sourceChanges = ref<Record<string, number>>({})
35
+ const loading = ref(false)
36
+
37
+ const builtinCount = computed(() => resolved.value.filter((f) => f.tier === 'builtin').length)
38
+
39
+ /** Probe the feature + load this board's tier, sources and resolved catalog. */
40
+ async function probe() {
41
+ if (!workspace.workspaceId) return
42
+ const id = workspace.requireId()
43
+ try {
44
+ const [tier, srcs, merged] = await Promise.all([
45
+ api.listFragments('workspace', id),
46
+ api.listFragmentSources('workspace', id).catch(() => [] as FragmentSource[]),
47
+ api.getResolvedFragments(id),
48
+ ])
49
+ fragments.value = tier
50
+ sources.value = srcs
51
+ resolved.value = merged
52
+ available.value = true
53
+ } catch {
54
+ available.value = false
55
+ fragments.value = []
56
+ sources.value = []
57
+ resolved.value = []
58
+ }
59
+ }
60
+
61
+ async function refreshResolved() {
62
+ resolved.value = await api.getResolvedFragments(workspace.requireId())
63
+ }
64
+
65
+ async function create(input: CreatePromptFragmentInput) {
66
+ loading.value = true
67
+ try {
68
+ await api.createFragment('workspace', workspace.requireId(), input)
69
+ await Promise.all([reloadTier(), refreshResolved()])
70
+ } finally {
71
+ loading.value = false
72
+ }
73
+ }
74
+
75
+ async function update(fragmentId: string, patch: UpdatePromptFragmentInput) {
76
+ await api.updateFragment('workspace', workspace.requireId(), fragmentId, patch)
77
+ await Promise.all([reloadTier(), refreshResolved()])
78
+ }
79
+
80
+ /** Tombstone a fragment at the workspace tier (suppresses an inherited one). */
81
+ async function remove(fragmentId: string) {
82
+ await api.deleteFragment('workspace', workspace.requireId(), fragmentId)
83
+ await Promise.all([reloadTier(), refreshResolved()])
84
+ }
85
+
86
+ async function reloadTier() {
87
+ fragments.value = await api.listFragments('workspace', workspace.requireId())
88
+ }
89
+
90
+ async function linkSource(input: LinkFragmentSourceInput) {
91
+ const source = await api.linkFragmentSource('workspace', workspace.requireId(), input)
92
+ sources.value = [source, ...sources.value]
93
+ return source
94
+ }
95
+
96
+ async function unlinkSource(sourceId: string) {
97
+ await api.unlinkFragmentSource('workspace', workspace.requireId(), sourceId)
98
+ sources.value = sources.value.filter((s) => s.id !== sourceId)
99
+ await refreshResolved()
100
+ }
101
+
102
+ /** Resync a source's Markdown into the catalog, then refresh views. */
103
+ async function syncSource(sourceId: string) {
104
+ loading.value = true
105
+ try {
106
+ const result = await api.syncFragmentSource('workspace', workspace.requireId(), sourceId)
107
+ delete sourceChanges.value[sourceId]
108
+ await Promise.all([reloadSources(), refreshResolved()])
109
+ return result
110
+ } finally {
111
+ loading.value = false
112
+ }
113
+ }
114
+
115
+ /** Cheap "check for changes" for a source; caches the changed count. */
116
+ async function checkSource(sourceId: string) {
117
+ const status = await api.fragmentSourceStatus('workspace', workspace.requireId(), sourceId)
118
+ sourceChanges.value = {
119
+ ...sourceChanges.value,
120
+ [sourceId]: status.changed ? status.changedCount : 0,
121
+ }
122
+ return status
123
+ }
124
+
125
+ async function reloadSources() {
126
+ sources.value = await api.listFragmentSources('workspace', workspace.requireId())
127
+ }
128
+
129
+ return {
130
+ available,
131
+ fragments,
132
+ resolved,
133
+ sources,
134
+ sourceChanges,
135
+ loading,
136
+ builtinCount,
137
+ probe,
138
+ refreshResolved,
139
+ create,
140
+ update,
141
+ remove,
142
+ linkSource,
143
+ unlinkSource,
144
+ syncSource,
145
+ checkSource,
146
+ }
147
+ })
@@ -0,0 +1,40 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref, computed } from 'vue'
3
+ import type { BlockType, PromptFragment } from '~/types/domain'
4
+
5
+ /**
6
+ * The best-practice prompt fragment catalog. It is build-static reference data on
7
+ * the backend (`GET /prompt-fragments`), workspace-independent, so this store
8
+ * fetches it once and caches it for the per-block picker in the inspector.
9
+ */
10
+ export const useFragmentsStore = defineStore('fragments', () => {
11
+ const api = useApi()
12
+ const fragments = ref<PromptFragment[]>([])
13
+ const loaded = ref(false)
14
+
15
+ /** Fetch the catalog once; subsequent calls are no-ops. */
16
+ async function ensureLoaded() {
17
+ if (loaded.value) return
18
+ fragments.value = await api.getPromptFragments()
19
+ loaded.value = true
20
+ }
21
+
22
+ const byId = computed(() => {
23
+ const map = new Map<string, PromptFragment>()
24
+ for (const f of fragments.value) map.set(f.id, f)
25
+ return map
26
+ })
27
+
28
+ function getFragment(id: string) {
29
+ return byId.value.get(id)
30
+ }
31
+
32
+ /** Fragments suitable for a block type (those with no `blockTypes` apply to all). */
33
+ function forBlockType(type: BlockType) {
34
+ return fragments.value.filter(
35
+ (f) => !f.appliesTo?.blockTypes || f.appliesTo.blockTypes.includes(type),
36
+ )
37
+ }
38
+
39
+ return { fragments, loaded, ensureLoaded, byId, getFragment, forBlockType }
40
+ })