@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.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/app/app.config.ts +8 -0
- package/app/app.vue +11 -0
- package/app/assets/css/main.css +100 -0
- package/app/components/auth/AuthGate.vue +24 -0
- package/app/components/auth/LoginScreen.vue +143 -0
- package/app/components/auth/UserMenu.vue +39 -0
- package/app/components/board/AddTaskModal.vue +444 -0
- package/app/components/board/AgentFailureCard.vue +97 -0
- package/app/components/board/AgentStopButton.vue +61 -0
- package/app/components/board/BoardCanvas.vue +183 -0
- package/app/components/board/ContextPicker.vue +367 -0
- package/app/components/board/RecurringPipelineModal.vue +219 -0
- package/app/components/board/TaskDependencyEdges.vue +132 -0
- package/app/components/board/nodes/AgentChip.vue +59 -0
- package/app/components/board/nodes/BlockNode.vue +433 -0
- package/app/components/board/nodes/DecisionBadge.vue +27 -0
- package/app/components/board/nodes/DraggableTask.vue +48 -0
- package/app/components/board/nodes/ModuleFrame.vue +97 -0
- package/app/components/board/nodes/TaskCard.vue +359 -0
- package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
- package/app/components/bootstrap/BootstrapModal.vue +665 -0
- package/app/components/clarity/ClarityReviewWindow.vue +611 -0
- package/app/components/consensus/ConsensusSessionWindow.vue +210 -0
- package/app/components/documents/DocumentImportModal.vue +161 -0
- package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
- package/app/components/documents/SpawnPreviewModal.vue +161 -0
- package/app/components/documents/TaskContextDocs.vue +83 -0
- package/app/components/focus/BlockFocusView.vue +171 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
- package/app/components/gates/GateResultView.vue +282 -0
- package/app/components/github/AddServiceFromRepoModal.vue +354 -0
- package/app/components/github/GitHubConnect.vue +183 -0
- package/app/components/github/GitHubOnboarding.vue +45 -0
- package/app/components/github/GitHubPanel.vue +584 -0
- package/app/components/github/RepoTreeBrowser.vue +171 -0
- package/app/components/layout/AccountTeamSettings.vue +237 -0
- package/app/components/layout/BoardSwitcher.vue +280 -0
- package/app/components/layout/BoardToolbar.vue +156 -0
- package/app/components/layout/CommandBar.vue +336 -0
- package/app/components/layout/GitHubPatBanner.vue +73 -0
- package/app/components/layout/NotificationsInbox.vue +175 -0
- package/app/components/layout/SideBar.vue +314 -0
- package/app/components/layout/SpendWarningBanner.vue +107 -0
- package/app/components/observability/StepMetricsBar.vue +102 -0
- package/app/components/palettes/AgentPalette.vue +86 -0
- package/app/components/panels/AgentStepDetail.vue +737 -0
- package/app/components/panels/DecisionModal.vue +71 -0
- package/app/components/panels/InspectorPanel.vue +465 -0
- package/app/components/panels/ObservabilityPanel.vue +351 -0
- package/app/components/panels/StepMetadataCard.vue +253 -0
- package/app/components/panels/StepRestartControl.vue +90 -0
- package/app/components/panels/StepResultViewHost.vue +40 -0
- package/app/components/panels/StepTestReport.vue +84 -0
- package/app/components/panels/inspector/ContainerSummary.vue +74 -0
- package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
- package/app/components/panels/inspector/ServiceFragments.vue +82 -0
- package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
- package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
- package/app/components/panels/inspector/TaskDependencies.vue +70 -0
- package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
- package/app/components/panels/inspector/TaskExecution.vue +364 -0
- package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
- package/app/components/panels/inspector/TaskStructure.vue +96 -0
- package/app/components/pipeline/AgentKindIcon.vue +30 -0
- package/app/components/pipeline/IterationCapPrompt.vue +70 -0
- package/app/components/pipeline/PipelineBuilder.vue +817 -0
- package/app/components/pipeline/PipelineProgress.vue +484 -0
- package/app/components/providers/ApiKeysSection.vue +273 -0
- package/app/components/providers/PersonalCredentialModal.vue +128 -0
- package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
- package/app/components/providers/VendorCredentialsModal.vue +197 -0
- package/app/components/recurring/RecurrenceEditor.vue +124 -0
- package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
- package/app/components/settings/DatadogPanel.vue +213 -0
- package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
- package/app/components/settings/MergeThresholdsPanel.vue +378 -0
- package/app/components/settings/ModelDefaultsPanel.vue +250 -0
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
- package/app/components/slack/SlackPanel.vue +299 -0
- package/app/components/tasks/TaskContextIssues.vue +88 -0
- package/app/components/tasks/TaskImportModal.vue +207 -0
- package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
- package/app/components/testing/TestReportWindow.vue +404 -0
- package/app/composables/api/accounts.ts +81 -0
- package/app/composables/api/auth.ts +45 -0
- package/app/composables/api/board.ts +101 -0
- package/app/composables/api/bootstrap.ts +62 -0
- package/app/composables/api/context.ts +25 -0
- package/app/composables/api/documents.ts +74 -0
- package/app/composables/api/execution.ts +127 -0
- package/app/composables/api/fragments.ts +71 -0
- package/app/composables/api/github.ts +131 -0
- package/app/composables/api/models.ts +127 -0
- package/app/composables/api/notifications.ts +23 -0
- package/app/composables/api/presets.ts +29 -0
- package/app/composables/api/recurring.ts +68 -0
- package/app/composables/api/releaseHealth.ts +43 -0
- package/app/composables/api/reviews.ts +146 -0
- package/app/composables/api/slack.ts +54 -0
- package/app/composables/api/tasks.ts +72 -0
- package/app/composables/api/workspaces.ts +36 -0
- package/app/composables/useApi.ts +89 -0
- package/app/composables/useBlockDrag.ts +90 -0
- package/app/composables/useBlockQueries.ts +154 -0
- package/app/composables/useBoardFlow.ts +11 -0
- package/app/composables/useContextLinking.ts +65 -0
- package/app/composables/useDepLabels.ts +26 -0
- package/app/composables/useFrameResize.ts +54 -0
- package/app/composables/useResultView.ts +48 -0
- package/app/composables/useReviewStage.ts +40 -0
- package/app/composables/useSemanticZoom.ts +31 -0
- package/app/composables/useStepApproval.ts +233 -0
- package/app/composables/useStepProse.ts +78 -0
- package/app/composables/useStepTimer.ts +63 -0
- package/app/composables/useTaskExpansion.ts +92 -0
- package/app/composables/useWorkspaceStream.ts +155 -0
- package/app/docs/architecture.md +31 -0
- package/app/pages/index.vue +141 -0
- package/app/stores/accounts.ts +152 -0
- package/app/stores/agentConfig.ts +35 -0
- package/app/stores/agentRuns.ts +122 -0
- package/app/stores/agents.ts +40 -0
- package/app/stores/apiKeys.ts +108 -0
- package/app/stores/auth.ts +166 -0
- package/app/stores/board.spec.ts +205 -0
- package/app/stores/board.ts +286 -0
- package/app/stores/bootstrap.ts +97 -0
- package/app/stores/clarity.ts +196 -0
- package/app/stores/consensus.ts +60 -0
- package/app/stores/documents.ts +176 -0
- package/app/stores/execution.ts +273 -0
- package/app/stores/fragmentLibrary.ts +147 -0
- package/app/stores/fragments.ts +40 -0
- package/app/stores/github.ts +305 -0
- package/app/stores/localModels.ts +51 -0
- package/app/stores/mergePresets.ts +58 -0
- package/app/stores/modelDefaults.ts +76 -0
- package/app/stores/models.ts +134 -0
- package/app/stores/notifications.ts +70 -0
- package/app/stores/observability.ts +144 -0
- package/app/stores/personalSubscriptions.ts +215 -0
- package/app/stores/pipelines.ts +327 -0
- package/app/stores/recurringPipelines.ts +112 -0
- package/app/stores/releaseHealth.ts +75 -0
- package/app/stores/requirements.spec.ts +94 -0
- package/app/stores/requirements.ts +208 -0
- package/app/stores/serviceFragmentDefaults.ts +29 -0
- package/app/stores/services.ts +87 -0
- package/app/stores/slack.ts +142 -0
- package/app/stores/taskExpansion.ts +36 -0
- package/app/stores/tasks.spec.ts +71 -0
- package/app/stores/tasks.ts +176 -0
- package/app/stores/tracker.ts +27 -0
- package/app/stores/ui.ts +434 -0
- package/app/stores/vendorCredentials.ts +54 -0
- package/app/stores/workspace.ts +215 -0
- package/app/stores/workspaceSettings.ts +36 -0
- package/app/types/accounts.ts +77 -0
- package/app/types/bootstrap.ts +83 -0
- package/app/types/clarity.ts +59 -0
- package/app/types/consensus.ts +91 -0
- package/app/types/documents.ts +104 -0
- package/app/types/domain.ts +495 -0
- package/app/types/execution.ts +383 -0
- package/app/types/fragments.ts +72 -0
- package/app/types/github.ts +173 -0
- package/app/types/localModels.ts +73 -0
- package/app/types/merge.ts +71 -0
- package/app/types/models.ts +157 -0
- package/app/types/notifications.ts +74 -0
- package/app/types/recurring.ts +69 -0
- package/app/types/releaseHealth.ts +31 -0
- package/app/types/requirements.ts +61 -0
- package/app/types/services.ts +27 -0
- package/app/types/slack.ts +57 -0
- package/app/types/tasks.ts +82 -0
- package/app/types/tracker.ts +18 -0
- package/app/utils/agentOutput.spec.ts +128 -0
- package/app/utils/agentOutput.ts +173 -0
- package/app/utils/catalog.spec.ts +112 -0
- package/app/utils/catalog.ts +455 -0
- package/app/utils/dnd.ts +29 -0
- package/app/utils/observability.ts +52 -0
- package/app/utils/pipelineRender.ts +151 -0
- package/nuxt.config.ts +55 -0
- 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>
|