@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,378 @@
1
+ <script setup lang="ts">
2
+ // Workspace settings: the merge-threshold preset library a task picks its
3
+ // auto-merge policy from (the `merger` step compares a PR's assessment against the
4
+ // resolved preset). Full CRUD over the mergePresets store — the same library the
5
+ // task inspector's "Merge policy" dropdown selects from. Exactly one preset is the
6
+ // default; it cannot be deleted or un-defaulted (the backend enforces this too).
7
+ import { reactive, ref, watch } from 'vue'
8
+ import type { MergeThresholdPreset, RequirementConcernLevel } from '~/types/merge'
9
+
10
+ // Concern-level options for the requirements auto-pass threshold (none < low < medium < high).
11
+ const CONCERN_LEVELS: { value: RequirementConcernLevel; label: string }[] = [
12
+ { value: 'none', label: 'None (always stop)' },
13
+ { value: 'low', label: 'Low' },
14
+ { value: 'medium', label: 'Medium' },
15
+ { value: 'high', label: 'High (never stop)' },
16
+ ]
17
+
18
+ const ui = useUiStore()
19
+ const store = useMergePresetsStore()
20
+ const toast = useToast()
21
+
22
+ const open = computed({
23
+ get: () => ui.mergeThresholdsOpen,
24
+ set: (v: boolean) => (v ? ui.openMergeThresholds() : ui.closeMergeThresholds()),
25
+ })
26
+
27
+ // Local editable copy per preset, kept in sync with the store. Percentages are
28
+ // edited 0..100 and stored 0..1.
29
+ interface Draft {
30
+ name: string
31
+ maxComplexity: number
32
+ maxRisk: number
33
+ maxImpact: number
34
+ ciMaxAttempts: number
35
+ maxRequirementIterations: number
36
+ maxRequirementConcernAllowed: RequirementConcernLevel
37
+ }
38
+ const drafts = reactive<Record<string, Draft>>({})
39
+
40
+ function toDraft(p: MergeThresholdPreset): Draft {
41
+ return {
42
+ name: p.name,
43
+ maxComplexity: Math.round(p.maxComplexity * 100),
44
+ maxRisk: Math.round(p.maxRisk * 100),
45
+ maxImpact: Math.round(p.maxImpact * 100),
46
+ ciMaxAttempts: p.ciMaxAttempts,
47
+ maxRequirementIterations: p.maxRequirementIterations,
48
+ maxRequirementConcernAllowed: p.maxRequirementConcernAllowed,
49
+ }
50
+ }
51
+
52
+ watch(
53
+ () => store.presets,
54
+ (presets) => {
55
+ for (const p of presets) if (!drafts[p.id]) drafts[p.id] = toDraft(p)
56
+ for (const id of Object.keys(drafts)) if (!presets.some((p) => p.id === id)) delete drafts[id]
57
+ },
58
+ { immediate: true, deep: false },
59
+ )
60
+
61
+ const busy = ref<string | null>(null)
62
+
63
+ function notifyError(title: string, e: unknown) {
64
+ toast.add({
65
+ title,
66
+ description: e instanceof Error ? e.message : String(e),
67
+ icon: 'i-lucide-triangle-alert',
68
+ color: 'error',
69
+ })
70
+ }
71
+
72
+ async function save(p: MergeThresholdPreset) {
73
+ const d = drafts[p.id]
74
+ if (!d) return
75
+ busy.value = p.id
76
+ try {
77
+ await store.update(p.id, {
78
+ name: d.name.trim() || p.name,
79
+ maxComplexity: d.maxComplexity / 100,
80
+ maxRisk: d.maxRisk / 100,
81
+ maxImpact: d.maxImpact / 100,
82
+ ciMaxAttempts: d.ciMaxAttempts,
83
+ maxRequirementIterations: d.maxRequirementIterations,
84
+ maxRequirementConcernAllowed: d.maxRequirementConcernAllowed,
85
+ })
86
+ toast.add({ title: 'Preset saved', icon: 'i-lucide-check', color: 'success' })
87
+ } catch (e) {
88
+ notifyError('Could not save preset', e)
89
+ } finally {
90
+ busy.value = null
91
+ }
92
+ }
93
+
94
+ async function makeDefault(p: MergeThresholdPreset) {
95
+ busy.value = p.id
96
+ try {
97
+ await store.update(p.id, { isDefault: true })
98
+ } catch (e) {
99
+ notifyError('Could not set default', e)
100
+ } finally {
101
+ busy.value = null
102
+ }
103
+ }
104
+
105
+ async function remove(p: MergeThresholdPreset) {
106
+ busy.value = p.id
107
+ try {
108
+ await store.remove(p.id)
109
+ } catch (e) {
110
+ notifyError('Could not delete preset', e)
111
+ } finally {
112
+ busy.value = null
113
+ }
114
+ }
115
+
116
+ // ---- create form ----------------------------------------------------------
117
+ const creating = ref(false)
118
+ const draft = reactive<Draft>({
119
+ name: '',
120
+ maxComplexity: 50,
121
+ maxRisk: 40,
122
+ maxImpact: 50,
123
+ ciMaxAttempts: 10,
124
+ maxRequirementIterations: 6,
125
+ maxRequirementConcernAllowed: 'none',
126
+ })
127
+
128
+ async function create() {
129
+ if (!draft.name.trim()) return
130
+ creating.value = true
131
+ try {
132
+ await store.create({
133
+ name: draft.name.trim(),
134
+ maxComplexity: draft.maxComplexity / 100,
135
+ maxRisk: draft.maxRisk / 100,
136
+ maxImpact: draft.maxImpact / 100,
137
+ ciMaxAttempts: draft.ciMaxAttempts,
138
+ maxRequirementIterations: draft.maxRequirementIterations,
139
+ maxRequirementConcernAllowed: draft.maxRequirementConcernAllowed,
140
+ })
141
+ draft.name = ''
142
+ toast.add({ title: 'Preset created', icon: 'i-lucide-check', color: 'success' })
143
+ } catch (e) {
144
+ notifyError('Could not create preset', e)
145
+ } finally {
146
+ creating.value = false
147
+ }
148
+ }
149
+ </script>
150
+
151
+ <template>
152
+ <UModal v-model:open="open" title="Merge thresholds" :ui="{ content: 'max-w-2xl' }">
153
+ <template #body>
154
+ <div class="space-y-4">
155
+ <p class="text-xs text-slate-400">
156
+ Named auto-merge policies a task can choose. After CI passes, the
157
+ <span class="text-slate-300">merger</span> agent scores the PR on complexity, risk and
158
+ impact (0–100%); the PR auto-merges only when every score is at or below the preset's
159
+ ceilings — otherwise a review notification is raised. The default preset governs any task
160
+ that picks none.
161
+ </p>
162
+
163
+ <div
164
+ v-for="p in store.presets"
165
+ :key="p.id"
166
+ class="rounded-lg border border-slate-700 bg-slate-800/40 p-3"
167
+ >
168
+ <div class="mb-3 flex items-center gap-2">
169
+ <UInput
170
+ v-model="drafts[p.id]!.name"
171
+ size="sm"
172
+ class="flex-1"
173
+ placeholder="Preset name"
174
+ />
175
+ <UBadge v-if="p.isDefault" color="primary" variant="subtle" size="sm">Default</UBadge>
176
+ <UButton
177
+ v-else
178
+ color="neutral"
179
+ variant="ghost"
180
+ size="xs"
181
+ icon="i-lucide-star"
182
+ :loading="busy === p.id"
183
+ @click="makeDefault(p)"
184
+ >
185
+ Make default
186
+ </UButton>
187
+ <UButton
188
+ color="error"
189
+ variant="ghost"
190
+ size="xs"
191
+ icon="i-lucide-trash-2"
192
+ :disabled="p.isDefault || busy === p.id"
193
+ :title="p.isDefault ? 'The default preset cannot be deleted' : 'Delete preset'"
194
+ @click="remove(p)"
195
+ />
196
+ </div>
197
+
198
+ <div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
199
+ <label class="block">
200
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
201
+ Max complexity %
202
+ </span>
203
+ <UInput
204
+ v-model.number="drafts[p.id]!.maxComplexity"
205
+ type="number"
206
+ :min="0"
207
+ :max="100"
208
+ size="sm"
209
+ />
210
+ </label>
211
+ <label class="block">
212
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
213
+ Max risk %
214
+ </span>
215
+ <UInput
216
+ v-model.number="drafts[p.id]!.maxRisk"
217
+ type="number"
218
+ :min="0"
219
+ :max="100"
220
+ size="sm"
221
+ />
222
+ </label>
223
+ <label class="block">
224
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
225
+ Max impact %
226
+ </span>
227
+ <UInput
228
+ v-model.number="drafts[p.id]!.maxImpact"
229
+ type="number"
230
+ :min="0"
231
+ :max="100"
232
+ size="sm"
233
+ />
234
+ </label>
235
+ <label class="block">
236
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
237
+ CI-fix attempts
238
+ </span>
239
+ <UInput
240
+ v-model.number="drafts[p.id]!.ciMaxAttempts"
241
+ type="number"
242
+ :min="0"
243
+ :max="50"
244
+ size="sm"
245
+ />
246
+ </label>
247
+ <label class="block">
248
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
249
+ Requirement iterations
250
+ </span>
251
+ <UInput
252
+ v-model.number="drafts[p.id]!.maxRequirementIterations"
253
+ type="number"
254
+ :min="1"
255
+ :max="20"
256
+ size="sm"
257
+ />
258
+ </label>
259
+ <label class="block">
260
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
261
+ Auto-pass concerns ≤
262
+ </span>
263
+ <USelect
264
+ v-model="drafts[p.id]!.maxRequirementConcernAllowed"
265
+ :items="CONCERN_LEVELS"
266
+ value-key="value"
267
+ size="sm"
268
+ />
269
+ </label>
270
+ </div>
271
+
272
+ <div class="mt-3 flex justify-end">
273
+ <UButton
274
+ color="primary"
275
+ variant="soft"
276
+ size="xs"
277
+ icon="i-lucide-save"
278
+ :loading="busy === p.id"
279
+ @click="save(p)"
280
+ >
281
+ Save
282
+ </UButton>
283
+ </div>
284
+ </div>
285
+
286
+ <!-- create -->
287
+ <div class="rounded-lg border border-dashed border-slate-700 p-3">
288
+ <p class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
289
+ New preset
290
+ </p>
291
+ <div class="flex flex-wrap items-end gap-3">
292
+ <label class="block min-w-40 flex-1">
293
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500"
294
+ >Name</span
295
+ >
296
+ <UInput v-model="draft.name" size="sm" placeholder="e.g. Cautious" />
297
+ </label>
298
+ <label class="block w-20">
299
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500"
300
+ >Cmplx%</span
301
+ >
302
+ <UInput
303
+ v-model.number="draft.maxComplexity"
304
+ type="number"
305
+ :min="0"
306
+ :max="100"
307
+ size="sm"
308
+ />
309
+ </label>
310
+ <label class="block w-20">
311
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500"
312
+ >Risk%</span
313
+ >
314
+ <UInput v-model.number="draft.maxRisk" type="number" :min="0" :max="100" size="sm" />
315
+ </label>
316
+ <label class="block w-20">
317
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500"
318
+ >Impact%</span
319
+ >
320
+ <UInput
321
+ v-model.number="draft.maxImpact"
322
+ type="number"
323
+ :min="0"
324
+ :max="100"
325
+ size="sm"
326
+ />
327
+ </label>
328
+ <label class="block w-20">
329
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500"
330
+ >CI-fix</span
331
+ >
332
+ <UInput
333
+ v-model.number="draft.ciMaxAttempts"
334
+ type="number"
335
+ :min="0"
336
+ :max="50"
337
+ size="sm"
338
+ />
339
+ </label>
340
+ <label class="block w-20">
341
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500"
342
+ >Req iter</span
343
+ >
344
+ <UInput
345
+ v-model.number="draft.maxRequirementIterations"
346
+ type="number"
347
+ :min="1"
348
+ :max="20"
349
+ size="sm"
350
+ />
351
+ </label>
352
+ <label class="block w-32">
353
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500"
354
+ >Auto-pass ≤</span
355
+ >
356
+ <USelect
357
+ v-model="draft.maxRequirementConcernAllowed"
358
+ :items="CONCERN_LEVELS"
359
+ value-key="value"
360
+ size="sm"
361
+ />
362
+ </label>
363
+ <UButton
364
+ color="primary"
365
+ size="sm"
366
+ icon="i-lucide-plus"
367
+ :loading="creating"
368
+ :disabled="!draft.name.trim()"
369
+ @click="create"
370
+ >
371
+ Add
372
+ </UButton>
373
+ </div>
374
+ </div>
375
+ </div>
376
+ </template>
377
+ </UModal>
378
+ </template>
@@ -0,0 +1,250 @@
1
+ <script setup lang="ts">
2
+ // Workspace settings: the per-agent-kind default model overrides. For each agent
3
+ // kind you can pin which model its steps run on for this workspace; a kind left on
4
+ // the deployment default falls back to the env-configured routing (named here so
5
+ // you can see which model that actually is). A model pinned on an individual task
6
+ // still wins over these. Persisted via the modelDefaults store (the backend
7
+ // replaces the whole map on each change).
8
+ //
9
+ // Styled as a dark full-screen window like the agent-output review overlay
10
+ // (AgentStepDetail) rather than a light modal, so the text stays readable
11
+ // regardless of the OS colour-mode preference. The filter box narrows the list of
12
+ // AGENT KINDS (the catalog is long); each kind's model picker is a plain dropdown.
13
+ import { computed, ref, watch } from 'vue'
14
+ import { onKeyStroke } from '@vueuse/core'
15
+ import type { AgentKind } from '~/types/domain'
16
+ import { MODEL_CONFIGURABLE_SYSTEM_KINDS } from '~/utils/catalog'
17
+ import { contextLabel, costLabel, displayFlavor, isSelectable } from '~/stores/models'
18
+
19
+ const ui = useUiStore()
20
+ const models = useModelsStore()
21
+ const defaults = useModelDefaultsStore()
22
+ const agents = useAgentsStore()
23
+ const creds = useVendorCredentialsStore()
24
+ const workspace = useWorkspaceStore()
25
+ const toast = useToast()
26
+
27
+ const open = computed({
28
+ get: () => ui.modelDefaultsOpen,
29
+ set: (v: boolean) => (v ? ui.openModelDefaults() : ui.closeModelDefaults()),
30
+ })
31
+
32
+ const busy = ref<string | null>(null)
33
+ // Narrows the agent-kind rows below, for finding a kind fast in a long catalog.
34
+ const filter = ref('')
35
+
36
+ // The palette archetypes PLUS the engine-driven kinds that still run an LLM
37
+ // (spec-writer, merger, the fixers/resolver) — those aren't user-addable steps but their
38
+ // model is still worth pinning per workspace. The pure gates run no model, so they stay out.
39
+ const configurableKinds = computed(() => [...agents.archetypes, ...MODEL_CONFIGURABLE_SYSTEM_KINDS])
40
+
41
+ const filteredArchetypes = computed(() => {
42
+ const q = filter.value.trim().toLowerCase()
43
+ if (!q) return configurableKinds.value
44
+ return configurableKinds.value.filter(
45
+ (a) => a.label.toLowerCase().includes(q) || String(a.kind).toLowerCase().includes(q),
46
+ )
47
+ })
48
+
49
+ watch(open, (isOpen) => {
50
+ if (isOpen) {
51
+ filter.value = ''
52
+ void models.ensureLoaded(workspace.workspaceId ?? undefined)
53
+ if (workspace.workspaceId) void creds.load(workspace.workspaceId)
54
+ }
55
+ })
56
+
57
+ onKeyStroke('Escape', () => {
58
+ if (open.value) open.value = false
59
+ })
60
+
61
+ /** The dropdown items for a kind's picker: the deployment-default reset plus the catalog. */
62
+ function menuFor(kind: AgentKind) {
63
+ const configured = creds.configuredVendors
64
+ return [
65
+ [
66
+ {
67
+ label: 'Deployment default',
68
+ icon: 'i-lucide-rotate-ccw',
69
+ onSelect: () => choose(kind, null),
70
+ },
71
+ ...models.models
72
+ .filter((m) => isSelectable(m, configured))
73
+ .map((m) => {
74
+ const flavor = displayFlavor(m, configured)
75
+ const ctx = contextLabel(flavor.contextTokens)
76
+ // Show the model's list price too (already resolved from spend pricing on
77
+ // the catalog). `costLabel` folds the quota indicator into the cost string
78
+ // for quota-based models; fall back to a bare "quota" tag when a quota model
79
+ // carries no price.
80
+ const price = costLabel(flavor) ?? (flavor.quotaBased ? 'quota' : undefined)
81
+ const suffix = [flavor.providerLabel, ctx, price].filter(Boolean).join(' · ')
82
+ return {
83
+ label: `${m.label} · ${suffix}`,
84
+ icon: flavor.quotaBased ? 'i-lucide-infinity' : 'i-lucide-cpu',
85
+ onSelect: () => choose(kind, m.id),
86
+ }
87
+ }),
88
+ ],
89
+ ]
90
+ }
91
+
92
+ /** The label shown on a kind's button: its pinned model, else the named deployment default. */
93
+ function buttonLabel(kind: AgentKind): string {
94
+ const pinned = defaults.forKind(kind)
95
+ if (pinned) {
96
+ const m = models.getModel(pinned)
97
+ // A pinned-but-uncatalogued id (e.g. a model whose provider key was since
98
+ // removed) shows the raw id rather than masquerading as a default.
99
+ if (!m) return pinned
100
+ return `${m.label} · ${displayFlavor(m, creds.configuredVendors).providerLabel}`
101
+ }
102
+ // No pin → name the env-routing model this kind actually falls back to.
103
+ const ref = defaults.deploymentRefForKind(kind)
104
+ const label = ref ? models.labelForRef(ref) : undefined
105
+ return label ? `${label} (default)` : 'Deployment default'
106
+ }
107
+
108
+ async function choose(kind: AgentKind, modelId: string | null) {
109
+ busy.value = kind
110
+ try {
111
+ await defaults.set(kind, modelId)
112
+ } catch (e) {
113
+ toast.add({
114
+ title: 'Could not save default model',
115
+ description: e instanceof Error ? e.message : String(e),
116
+ icon: 'i-lucide-triangle-alert',
117
+ color: 'error',
118
+ })
119
+ } finally {
120
+ busy.value = null
121
+ }
122
+ }
123
+ </script>
124
+
125
+ <template>
126
+ <Teleport to="body">
127
+ <Transition name="reader-fade">
128
+ <div
129
+ v-if="open"
130
+ class="fixed inset-0 z-50 flex flex-col bg-slate-950/96 backdrop-blur-sm"
131
+ role="dialog"
132
+ aria-modal="true"
133
+ >
134
+ <header class="flex items-center gap-3 border-b border-slate-800 px-6 py-4">
135
+ <div
136
+ class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-indigo-500/15"
137
+ >
138
+ <UIcon name="i-lucide-cpu" class="h-5 w-5 text-indigo-300" />
139
+ </div>
140
+ <div class="min-w-0">
141
+ <h1 class="truncate text-base font-semibold text-white">Default models for agents</h1>
142
+ <p class="truncate text-xs text-slate-500">
143
+ Pin which model each agent kind runs on for this workspace.
144
+ </p>
145
+ </div>
146
+ <UButton
147
+ icon="i-lucide-x"
148
+ color="neutral"
149
+ variant="ghost"
150
+ size="sm"
151
+ class="ml-auto"
152
+ title="Close (Esc)"
153
+ @click="open = false"
154
+ />
155
+ </header>
156
+
157
+ <div class="flex-1 overflow-auto px-6 py-6">
158
+ <div class="mx-auto max-w-3xl space-y-5">
159
+ <p class="text-sm leading-relaxed text-slate-400">
160
+ Pin which model each agent kind runs on for this workspace, e.g. a strong reasoning
161
+ model for the architect, a cheaper one for the documenter. A kind left on its
162
+ <span class="text-slate-300">deployment default</span> uses the server's configured
163
+ routing (named on the button). A model pinned on an individual task still overrides
164
+ these.
165
+ </p>
166
+
167
+ <UInput
168
+ v-model="filter"
169
+ icon="i-lucide-search"
170
+ size="sm"
171
+ placeholder="Filter agents…"
172
+ class="w-full"
173
+ >
174
+ <template v-if="filter" #trailing>
175
+ <UButton
176
+ icon="i-lucide-x"
177
+ color="neutral"
178
+ variant="link"
179
+ size="xs"
180
+ aria-label="Clear filter"
181
+ @click="filter = ''"
182
+ />
183
+ </template>
184
+ </UInput>
185
+
186
+ <p v-if="models.models.length === 0" class="py-4 text-center text-sm text-slate-500">
187
+ Loading model catalog…
188
+ </p>
189
+
190
+ <div
191
+ v-else
192
+ class="divide-y divide-slate-800 rounded-xl border border-slate-800 bg-slate-900/50"
193
+ >
194
+ <div
195
+ v-for="a in filteredArchetypes"
196
+ :key="a.kind"
197
+ class="flex items-center gap-3 px-4 py-3"
198
+ >
199
+ <UIcon
200
+ :name="a.icon"
201
+ class="h-4 w-4 shrink-0"
202
+ :style="{ color: a.color }"
203
+ :title="a.description"
204
+ />
205
+ <div class="min-w-0 flex-1" :title="a.description">
206
+ <p class="truncate text-sm text-slate-200">{{ a.label }}</p>
207
+ </div>
208
+ <!-- The menu content portals to <body>, where it would sit behind
209
+ this z-50 overlay (it carries no z-index of its own) and the
210
+ overlay would swallow the clicks — so lift it above. -->
211
+ <UDropdownMenu
212
+ :items="menuFor(a.kind)"
213
+ :ui="{ content: 'max-h-80 overflow-y-auto z-[60]' }"
214
+ >
215
+ <UButton
216
+ size="xs"
217
+ :color="defaults.forKind(a.kind) ? 'primary' : 'neutral'"
218
+ :variant="defaults.forKind(a.kind) ? 'subtle' : 'soft'"
219
+ trailing-icon="i-lucide-chevron-down"
220
+ :loading="busy === a.kind"
221
+ class="w-64 shrink-0 justify-between"
222
+ >
223
+ <span class="truncate">{{ buttonLabel(a.kind) }}</span>
224
+ </UButton>
225
+ </UDropdownMenu>
226
+ </div>
227
+ <p
228
+ v-if="filteredArchetypes.length === 0"
229
+ class="px-4 py-6 text-center text-sm text-slate-500"
230
+ >
231
+ No agents match "{{ filter }}".
232
+ </p>
233
+ </div>
234
+ </div>
235
+ </div>
236
+ </div>
237
+ </Transition>
238
+ </Teleport>
239
+ </template>
240
+
241
+ <style scoped>
242
+ .reader-fade-enter-active,
243
+ .reader-fade-leave-active {
244
+ transition: opacity 0.18s ease;
245
+ }
246
+ .reader-fade-enter-from,
247
+ .reader-fade-leave-to {
248
+ opacity: 0;
249
+ }
250
+ </style>