@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,444 @@
1
+ <script setup lang="ts">
2
+ // Create a new task on the board. The user names the task and writes its
3
+ // description themselves — there are no auto-generated placeholder titles. The
4
+ // task lands in `planned` state; it is never launched here. The user starts a
5
+ // pipeline on it explicitly (and can keep editing it until they do).
6
+ //
7
+ // When the document/task integrations are available, the user can also attach
8
+ // external context up front via <ContextPicker>: search a connected source
9
+ // (Confluence / Notion / GitHub repo docs / Jira / GitHub issues) by title or
10
+ // content, paste a page/issue URL, or pick something already imported. Linking
11
+ // needs the block id, so we create the task first, then import-and-link the
12
+ // chosen items to it before closing — the same context the agents see for every
13
+ // step of the run (see the backend's linkedContextSection).
14
+ import type { CreateTaskType, TaskTypeFields } from '~/types/domain'
15
+
16
+ const ui = useUiStore()
17
+ const board = useBoardStore()
18
+ const documents = useDocumentsStore()
19
+ const tasks = useTasksStore()
20
+ const mergePresets = useMergePresetsStore()
21
+ const pipelines = usePipelinesStore()
22
+ const agentConfig = useAgentConfigStore()
23
+ const toast = useToast()
24
+
25
+ const { linkPending } = useContextLinking()
26
+
27
+ const open = computed({
28
+ get: () => ui.addTaskContainerId !== null,
29
+ set: (v: boolean) => {
30
+ if (!v) ui.closeAddTask()
31
+ },
32
+ })
33
+
34
+ const container = computed(() =>
35
+ ui.addTaskContainerId ? board.getBlock(ui.addTaskContainerId) : undefined,
36
+ )
37
+
38
+ const title = ref('')
39
+ const description = ref('')
40
+ const saving = ref(false)
41
+
42
+ // The kind of task being created. `recurring` is special: it is created through the
43
+ // recurring-pipeline schedule flow (a schedule on the service frame), so picking it
44
+ // delegates to <RecurringPipelineModal> instead of creating a one-off task here.
45
+ type TaskTypeChoice = CreateTaskType | 'recurring'
46
+ const taskType = ref<TaskTypeChoice>('feature')
47
+ const TASK_TYPES: { value: TaskTypeChoice; label: string; icon: string }[] = [
48
+ { value: 'feature', label: 'Feature', icon: 'i-lucide-sparkles' },
49
+ { value: 'bug', label: 'Bug', icon: 'i-lucide-bug' },
50
+ { value: 'document', label: 'Document', icon: 'i-lucide-file-text' },
51
+ { value: 'spike', label: 'Spike', icon: 'i-lucide-flask-conical' },
52
+ { value: 'recurring', label: 'Recurring', icon: 'i-lucide-repeat' },
53
+ ]
54
+ const isRecurring = computed(() => taskType.value === 'recurring')
55
+
56
+ // Per-type fields (only the ones relevant to the chosen type are shown / sent).
57
+ const severity = ref<'low' | 'medium' | 'high' | 'critical' | ''>('')
58
+ const stepsToReproduce = ref('')
59
+ const timeboxHours = ref<number | undefined>(undefined)
60
+ const docKind = ref<'prd' | 'rfc' | 'runbook' | 'reference' | 'other' | ''>('')
61
+ const SEVERITIES = ['low', 'medium', 'high', 'critical'] as const
62
+ const DOC_KINDS = ['prd', 'rfc', 'runbook', 'reference', 'other'] as const
63
+
64
+ function buildTypeFields(): TaskTypeFields | undefined {
65
+ if (taskType.value === 'bug') {
66
+ const f: TaskTypeFields = {}
67
+ if (severity.value) f.severity = severity.value
68
+ if (stepsToReproduce.value.trim()) f.stepsToReproduce = stepsToReproduce.value.trim()
69
+ return Object.keys(f).length ? f : undefined
70
+ }
71
+ if (taskType.value === 'spike') {
72
+ // `v-model.number` on a cleared number input yields '' (not undefined), which would
73
+ // serialise as a non-number and 400 the create — so require a finite number here.
74
+ return typeof timeboxHours.value === 'number' &&
75
+ Number.isFinite(timeboxHours.value) &&
76
+ timeboxHours.value >= 0
77
+ ? { timeboxHours: timeboxHours.value }
78
+ : undefined
79
+ }
80
+ if (taskType.value === 'document') {
81
+ return docKind.value ? { docKind: docKind.value } : undefined
82
+ }
83
+ return undefined
84
+ }
85
+
86
+ // For a recurring task, the schedule attaches to the service frame: the container itself
87
+ // when it's a frame, else its parent frame (a module's parent).
88
+ const recurringFrameId = computed(() => {
89
+ const c = container.value
90
+ if (!c) return null
91
+ return c.level === 'frame' ? c.id : c.parentId
92
+ })
93
+
94
+ // Run configuration picked up front. Empty string = use the default (workspace
95
+ // default merge preset / no pinned pipeline).
96
+ const mergePresetId = ref('')
97
+ const pipelineId = ref('')
98
+
99
+ const presetMenu = computed(() => [
100
+ [
101
+ {
102
+ label: mergePresets.defaultPreset
103
+ ? `Default (${mergePresets.defaultPreset.name})`
104
+ : 'Workspace default',
105
+ icon: 'i-lucide-rotate-ccw',
106
+ onSelect: () => (mergePresetId.value = ''),
107
+ },
108
+ ...mergePresets.presets.map((p) => ({
109
+ label: p.name,
110
+ icon: 'i-lucide-git-merge',
111
+ onSelect: () => (mergePresetId.value = p.id),
112
+ })),
113
+ ],
114
+ ])
115
+ const selectedPresetLabel = computed(() => {
116
+ if (!mergePresetId.value) {
117
+ return mergePresets.defaultPreset
118
+ ? `Default (${mergePresets.defaultPreset.name})`
119
+ : 'Workspace default'
120
+ }
121
+ return mergePresets.presets.find((p) => p.id === mergePresetId.value)?.name ?? 'Workspace default'
122
+ })
123
+
124
+ const pipelineMenu = computed(() => [
125
+ [
126
+ {
127
+ label: 'Choose at run time',
128
+ icon: 'i-lucide-rotate-ccw',
129
+ onSelect: () => (pipelineId.value = ''),
130
+ },
131
+ ...pipelines.pipelines.map((p) => ({
132
+ label: p.name,
133
+ icon: 'i-lucide-workflow',
134
+ onSelect: () => (pipelineId.value = p.id),
135
+ })),
136
+ ],
137
+ ])
138
+ const selectedPipelineLabel = computed(
139
+ () => pipelines.getPipeline(pipelineId.value)?.name ?? 'Choose at run time',
140
+ )
141
+
142
+ // Task-level agent config contributed by the selected pipeline's agents (e.g. the
143
+ // Tester's environment). Editable up front; persisted on the task and frozen once
144
+ // the contributing agent runs. Defaults to each descriptor's default until changed.
145
+ const agentConfigValues = ref<Record<string, string>>({})
146
+ const configDescriptors = computed(() => agentConfig.forPipeline(pipelineId.value))
147
+ function configValue(id: string, fallback: string): string {
148
+ return agentConfigValues.value[id] ?? fallback
149
+ }
150
+ function setConfig(id: string, value: string) {
151
+ agentConfigValues.value = { ...agentConfigValues.value, [id]: value }
152
+ }
153
+
154
+ // Context the user chose to attach to the new task (search hits, pasted URLs,
155
+ // already-imported items), collected by <ContextPicker> and committed on add.
156
+ const pendingContext = ref<PendingContext[]>([])
157
+
158
+ // The picker is offered whenever either integration is configured (even with
159
+ // nothing imported yet — you can search/paste a URL to attach the first item).
160
+ const showContext = computed(() => documents.available || tasks.available)
161
+
162
+ // Reset the form whenever the modal opens for a (new) container, and refresh the
163
+ // imported docs/issues so the quick-pick list is current.
164
+ watch(open, (isOpen) => {
165
+ if (!isOpen) return
166
+ title.value = ''
167
+ description.value = ''
168
+ saving.value = false
169
+ taskType.value = 'feature'
170
+ severity.value = ''
171
+ stepsToReproduce.value = ''
172
+ timeboxHours.value = undefined
173
+ docKind.value = ''
174
+ mergePresetId.value = ''
175
+ pipelineId.value = ''
176
+ agentConfigValues.value = {}
177
+ pendingContext.value = []
178
+ documents.loadDocuments().catch(() => {})
179
+ tasks.loadTasks().catch(() => {})
180
+ })
181
+
182
+ // A recurring task only needs a target frame (its details are filled in the schedule
183
+ // modal); every other type needs a title.
184
+ const canAdd = computed(() =>
185
+ isRecurring.value ? recurringFrameId.value !== null : title.value.trim().length > 0,
186
+ )
187
+
188
+ async function add() {
189
+ const containerId = ui.addTaskContainerId
190
+ if (!containerId || !canAdd.value) return
191
+ // Recurring tasks are created via a schedule on the service frame — hand off to the
192
+ // existing recurring-pipeline modal (which carries the cadence + prompt).
193
+ if (isRecurring.value) {
194
+ const frameId = recurringFrameId.value
195
+ if (!frameId) return
196
+ ui.closeAddTask()
197
+ ui.openAddRecurring(frameId)
198
+ return
199
+ }
200
+ saving.value = true
201
+ try {
202
+ const typeFields = buildTypeFields()
203
+ const block = await board.addTask(
204
+ containerId,
205
+ title.value.trim(),
206
+ description.value.trim() || undefined,
207
+ {
208
+ taskType: taskType.value as CreateTaskType,
209
+ ...(typeFields ? { taskTypeFields: typeFields } : {}),
210
+ ...(mergePresetId.value ? { mergePresetId: mergePresetId.value } : {}),
211
+ ...(pipelineId.value ? { pipelineId: pipelineId.value } : {}),
212
+ ...(Object.keys(agentConfigValues.value).length
213
+ ? { agentConfig: agentConfigValues.value }
214
+ : {}),
215
+ },
216
+ )
217
+ if (block) {
218
+ const failed = await linkPending(block.id, pendingContext.value)
219
+ if (failed > 0) {
220
+ toast.add({
221
+ title: `Task added, but ${failed} attachment${failed === 1 ? '' : 's'} could not be linked`,
222
+ icon: 'i-lucide-triangle-alert',
223
+ color: 'warning',
224
+ })
225
+ }
226
+ }
227
+ ui.closeAddTask()
228
+ } catch (e) {
229
+ toast.add({
230
+ title: 'Could not add task',
231
+ description: e instanceof Error ? e.message : String(e),
232
+ icon: 'i-lucide-triangle-alert',
233
+ color: 'error',
234
+ })
235
+ } finally {
236
+ saving.value = false
237
+ }
238
+ }
239
+ </script>
240
+
241
+ <template>
242
+ <UModal v-model:open="open" title="Add a task">
243
+ <template #body>
244
+ <div class="space-y-4">
245
+ <p v-if="container" class="text-xs text-slate-400">
246
+ New task in <span class="font-medium text-slate-200">{{ container.title }}</span>
247
+ </p>
248
+
249
+ <UFormField label="Type">
250
+ <div class="flex flex-wrap gap-1">
251
+ <UButton
252
+ v-for="t in TASK_TYPES"
253
+ :key="t.value"
254
+ :color="taskType === t.value ? 'primary' : 'neutral'"
255
+ :variant="taskType === t.value ? 'soft' : 'ghost'"
256
+ :icon="t.icon"
257
+ size="xs"
258
+ @click="taskType = t.value"
259
+ >
260
+ {{ t.label }}
261
+ </UButton>
262
+ </div>
263
+ </UFormField>
264
+
265
+ <!-- Recurring tasks are configured as a schedule on the service frame. -->
266
+ <div
267
+ v-if="isRecurring"
268
+ class="rounded-lg border border-slate-800 p-3 text-[11px] text-slate-400"
269
+ >
270
+ <template v-if="recurringFrameId">
271
+ A recurring task runs a pipeline on a cadence. Continue to set the schedule + prompt.
272
+ </template>
273
+ <template v-else>
274
+ A recurring task must live on a service. Add it from a service frame (or a module inside
275
+ one).
276
+ </template>
277
+ </div>
278
+
279
+ <template v-if="!isRecurring">
280
+ <UFormField label="Title" required>
281
+ <UInput
282
+ v-model="title"
283
+ placeholder="What needs to be done?"
284
+ autofocus
285
+ class="w-full"
286
+ @keydown.enter="add"
287
+ />
288
+ </UFormField>
289
+
290
+ <UFormField label="Description">
291
+ <UTextarea
292
+ v-model="description"
293
+ :rows="4"
294
+ autoresize
295
+ placeholder="Describe the work — context, acceptance criteria, anything the agent should know…"
296
+ class="w-full"
297
+ />
298
+ </UFormField>
299
+
300
+ <!-- Per-type fields. -->
301
+ <div v-if="taskType === 'bug'" class="grid grid-cols-2 gap-3">
302
+ <UFormField label="Severity">
303
+ <div class="flex flex-wrap gap-1">
304
+ <UButton
305
+ v-for="s in SEVERITIES"
306
+ :key="s"
307
+ :color="severity === s ? 'primary' : 'neutral'"
308
+ :variant="severity === s ? 'soft' : 'ghost'"
309
+ size="xs"
310
+ class="capitalize"
311
+ @click="severity = severity === s ? '' : s"
312
+ >
313
+ {{ s }}
314
+ </UButton>
315
+ </div>
316
+ </UFormField>
317
+ <UFormField label="Steps to reproduce" class="col-span-2">
318
+ <UTextarea
319
+ v-model="stepsToReproduce"
320
+ :rows="2"
321
+ autoresize
322
+ placeholder="Observed vs expected, and how to reproduce…"
323
+ class="w-full"
324
+ />
325
+ </UFormField>
326
+ </div>
327
+
328
+ <UFormField v-else-if="taskType === 'spike'" label="Time-box (hours)">
329
+ <UInput
330
+ v-model.number="timeboxHours"
331
+ type="number"
332
+ min="0"
333
+ placeholder="e.g. 8"
334
+ class="w-full"
335
+ />
336
+ </UFormField>
337
+
338
+ <UFormField v-else-if="taskType === 'document'" label="Document kind">
339
+ <div class="flex flex-wrap gap-1">
340
+ <UButton
341
+ v-for="k in DOC_KINDS"
342
+ :key="k"
343
+ :color="docKind === k ? 'primary' : 'neutral'"
344
+ :variant="docKind === k ? 'soft' : 'ghost'"
345
+ size="xs"
346
+ class="uppercase"
347
+ @click="docKind = docKind === k ? '' : k"
348
+ >
349
+ {{ k }}
350
+ </UButton>
351
+ </div>
352
+ </UFormField>
353
+
354
+ <div class="grid grid-cols-2 gap-3">
355
+ <UFormField label="Pipeline">
356
+ <UDropdownMenu :items="pipelineMenu" class="w-full">
357
+ <UButton
358
+ color="neutral"
359
+ variant="subtle"
360
+ size="sm"
361
+ icon="i-lucide-workflow"
362
+ trailing-icon="i-lucide-chevron-down"
363
+ class="w-full justify-between"
364
+ >
365
+ {{ selectedPipelineLabel }}
366
+ </UButton>
367
+ </UDropdownMenu>
368
+ </UFormField>
369
+
370
+ <UFormField label="Merge policy">
371
+ <UDropdownMenu :items="presetMenu" class="w-full">
372
+ <UButton
373
+ color="neutral"
374
+ variant="subtle"
375
+ size="sm"
376
+ icon="i-lucide-git-merge"
377
+ trailing-icon="i-lucide-chevron-down"
378
+ class="w-full justify-between"
379
+ >
380
+ {{ selectedPresetLabel }}
381
+ </UButton>
382
+ </UDropdownMenu>
383
+ </UFormField>
384
+ </div>
385
+
386
+ <div v-if="configDescriptors.length" class="space-y-3">
387
+ <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
388
+ Agent configuration
389
+ </span>
390
+ <div v-for="d in configDescriptors" :key="d.id" class="space-y-1">
391
+ <div class="text-[11px] text-slate-400">{{ d.label }}</div>
392
+ <div class="flex flex-wrap gap-1">
393
+ <UButton
394
+ v-for="opt in d.options"
395
+ :key="opt.value"
396
+ :color="configValue(d.id, d.default) === opt.value ? 'primary' : 'neutral'"
397
+ :variant="configValue(d.id, d.default) === opt.value ? 'soft' : 'ghost'"
398
+ size="xs"
399
+ @click="setConfig(d.id, opt.value)"
400
+ >
401
+ {{ opt.label }}
402
+ </UButton>
403
+ </div>
404
+ <p class="text-[11px] leading-snug text-slate-500">{{ d.description }}</p>
405
+ </div>
406
+ </div>
407
+
408
+ <div v-if="showContext" class="space-y-2">
409
+ <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
410
+ Extra context (optional)
411
+ </span>
412
+
413
+ <ContextPicker v-model="pendingContext" />
414
+
415
+ <p class="text-[11px] text-slate-500">
416
+ Search a connected source, paste a page/issue URL, or pick something already imported
417
+ — it's fed to every agent step as context.
418
+ </p>
419
+ </div>
420
+
421
+ <p class="text-[11px] text-slate-500">
422
+ The task is added in a planned state. It won't run until you start a pipeline on it —
423
+ you can keep editing it until then.
424
+ </p>
425
+ </template>
426
+ </div>
427
+ </template>
428
+
429
+ <template #footer>
430
+ <div class="flex w-full justify-end gap-2">
431
+ <UButton color="neutral" variant="ghost" @click="ui.closeAddTask()">Cancel</UButton>
432
+ <UButton
433
+ color="primary"
434
+ :icon="isRecurring ? 'i-lucide-arrow-right' : 'i-lucide-plus'"
435
+ :loading="saving"
436
+ :disabled="!canAdd"
437
+ @click="add"
438
+ >
439
+ {{ isRecurring ? 'Continue' : 'Add task' }}
440
+ </UButton>
441
+ </div>
442
+ </template>
443
+ </UModal>
444
+ </template>
@@ -0,0 +1,97 @@
1
+ <script setup lang="ts">
2
+ // Shared failure banner + retry for any failed "agent run" (a bootstrap or a
3
+ // task execution). Self-contained: it owns the in-flight retry guard and calls
4
+ // the unified retry through the agentRuns store, so every surface (board card,
5
+ // inspector, task panel) gets identical behaviour from one place. Replaces the
6
+ // three hand-rolled bootstrap banners that used to duplicate this logic.
7
+ import type { AgentRunSummary } from '~/stores/agentRuns'
8
+
9
+ const props = withDefaults(
10
+ defineProps<{ run: AgentRunSummary; variant?: 'compact' | 'expanded' }>(),
11
+ { variant: 'expanded' },
12
+ )
13
+
14
+ const agentRuns = useAgentRunsStore()
15
+ const toast = useToast()
16
+
17
+ const compact = computed(() => props.variant === 'compact')
18
+ const failure = computed(() => props.run.failure)
19
+ const title = computed(() => (props.run.kind === 'bootstrap' ? 'Bootstrap failed' : 'Run failed'))
20
+ const retryLabel = computed(() =>
21
+ props.run.kind === 'bootstrap' ? 'Retry bootstrap' : 'Retry run',
22
+ )
23
+
24
+ const retrying = ref(false)
25
+ async function retry() {
26
+ if (retrying.value) return
27
+ retrying.value = true
28
+ try {
29
+ await agentRuns.retry(props.run.runId)
30
+ } catch (e) {
31
+ toast.add({
32
+ title: 'Retry failed',
33
+ description: e instanceof Error ? e.message : String(e),
34
+ color: 'error',
35
+ })
36
+ } finally {
37
+ retrying.value = false
38
+ }
39
+ }
40
+ </script>
41
+
42
+ <template>
43
+ <div
44
+ class="nodrag rounded-lg border border-rose-900/60 bg-rose-950/40"
45
+ :class="compact ? 'px-3 py-2' : 'px-3 py-2.5'"
46
+ >
47
+ <div class="flex items-center gap-1.5" :class="compact ? 'text-[11px]' : 'text-xs'">
48
+ <UIcon
49
+ name="i-lucide-alert-triangle"
50
+ class="shrink-0 text-rose-400"
51
+ :class="compact ? 'h-3.5 w-3.5' : 'h-4 w-4'"
52
+ />
53
+ <span class="text-rose-300">{{ title }}</span>
54
+ </div>
55
+
56
+ <p
57
+ v-if="failure?.message"
58
+ class="mt-1 leading-snug text-rose-300/90"
59
+ :class="compact ? 'line-clamp-2 text-[10px]' : 'text-[11px]'"
60
+ :title="failure.message"
61
+ >
62
+ {{ failure.message }}
63
+ </p>
64
+
65
+ <p
66
+ v-if="failure?.hint"
67
+ class="mt-1 leading-snug text-rose-400/70"
68
+ :class="compact ? 'text-[10px]' : 'text-[11px]'"
69
+ >
70
+ {{ failure.hint }}
71
+ </p>
72
+
73
+ <details v-if="!compact && failure?.detail && failure.detail !== failure.message" class="mt-1">
74
+ <summary class="cursor-pointer text-[10px] text-rose-400/60 hover:text-rose-300">
75
+ Show detail
76
+ </summary>
77
+ <pre
78
+ class="mt-1 max-h-32 overflow-auto whitespace-pre-wrap rounded bg-rose-950/60 p-1.5 text-[10px] text-rose-200/80"
79
+ >{{ failure.detail }}</pre
80
+ >
81
+ </details>
82
+
83
+ <button
84
+ type="button"
85
+ class="nodrag mt-2 flex items-center gap-1 rounded-md bg-rose-900/40 text-rose-200 hover:bg-rose-900/70 disabled:opacity-60"
86
+ :class="compact ? 'px-2 py-0.5 text-[10px]' : 'px-2 py-1 text-[11px]'"
87
+ :disabled="retrying"
88
+ @click.stop="retry"
89
+ >
90
+ <UIcon
91
+ :name="retrying ? 'i-lucide-loader-circle' : 'i-lucide-rotate-ccw'"
92
+ :class="[compact ? 'h-3 w-3' : 'h-3.5 w-3.5', { 'animate-spin': retrying }]"
93
+ />
94
+ {{ retrying ? 'Retrying…' : compact ? 'Retry' : retryLabel }}
95
+ </button>
96
+ </div>
97
+ </template>
@@ -0,0 +1,61 @@
1
+ <script setup lang="ts">
2
+ // Self-contained "Stop" control for a RUNNING agent run (bootstrap or execution).
3
+ // Calls the unified stop through the agentRuns store — which kills the per-run
4
+ // container and tears down the durable driver server-side — then toasts the
5
+ // outcome so the user is told it actually happened. Mirrors AgentFailureCard's
6
+ // self-contained pattern so every surface (board card, inspector, task panel)
7
+ // behaves identically from one place.
8
+ import type { AgentRunKind } from '~/types/domain'
9
+
10
+ const props = withDefaults(
11
+ defineProps<{
12
+ runId: string
13
+ /** Hint for the button label only; the backend resolves the real kind. */
14
+ kind?: AgentRunKind
15
+ size?: 'xs' | 'sm' | 'md'
16
+ variant?: 'solid' | 'soft' | 'ghost' | 'subtle' | 'outline'
17
+ label?: string
18
+ }>(),
19
+ { size: 'xs', variant: 'soft', label: 'Stop' },
20
+ )
21
+
22
+ const agentRuns = useAgentRunsStore()
23
+ const toast = useToast()
24
+ const stopping = ref(false)
25
+
26
+ async function stop() {
27
+ if (stopping.value) return
28
+ stopping.value = true
29
+ try {
30
+ const kind = await agentRuns.stop(props.runId)
31
+ toast.add({
32
+ title: kind === 'bootstrap' ? 'Bootstrap stopped' : 'Run stopped',
33
+ description: 'The container was killed and the run was cancelled.',
34
+ icon: 'i-lucide-circle-stop',
35
+ color: 'warning',
36
+ })
37
+ } catch (e) {
38
+ toast.add({
39
+ title: 'Stop failed',
40
+ description: e instanceof Error ? e.message : String(e),
41
+ color: 'error',
42
+ })
43
+ } finally {
44
+ stopping.value = false
45
+ }
46
+ }
47
+ </script>
48
+
49
+ <template>
50
+ <UButton
51
+ class="nodrag"
52
+ color="warning"
53
+ :variant="variant"
54
+ :size="size"
55
+ icon="i-lucide-circle-stop"
56
+ :loading="stopping"
57
+ @click.stop="stop"
58
+ >
59
+ {{ label }}
60
+ </UButton>
61
+ </template>