@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,359 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Block } from '~/types/domain'
|
|
3
|
+
import { STATUS_META, MODULE_META } from '~/utils/catalog'
|
|
4
|
+
import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
|
|
5
|
+
import TaskPipelineMini from './TaskPipelineMini.vue'
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{ taskId: string }>()
|
|
8
|
+
|
|
9
|
+
const board = useBoardStore()
|
|
10
|
+
const execution = useExecutionStore()
|
|
11
|
+
const pipelines = usePipelinesStore()
|
|
12
|
+
const ui = useUiStore()
|
|
13
|
+
const agentRuns = useAgentRunsStore()
|
|
14
|
+
const reviews = useReviewStage()
|
|
15
|
+
const toast = useToast()
|
|
16
|
+
|
|
17
|
+
const task = computed<Block | undefined>(() => board.getBlock(props.taskId))
|
|
18
|
+
const statusMeta = computed(() => (task.value ? STATUS_META[task.value.status] : null))
|
|
19
|
+
const selected = computed(() => ui.selectedBlockId === props.taskId)
|
|
20
|
+
|
|
21
|
+
// ---- dependencies (gate execution order; may point across frames) ----------
|
|
22
|
+
const deps = computed(() =>
|
|
23
|
+
(task.value?.dependsOn ?? []).map((id) => board.getBlock(id)).filter((b): b is Block => !!b),
|
|
24
|
+
)
|
|
25
|
+
/** Deps that haven't merged yet — these block this task from running. */
|
|
26
|
+
const unmet = computed(() => board.unmetDeps(props.taskId))
|
|
27
|
+
const runnable = computed(() => board.isRunnable(props.taskId))
|
|
28
|
+
|
|
29
|
+
/** Label a dependency, noting its frame when it lives in another one. */
|
|
30
|
+
const { depLabel: labelDep } = useDepLabels()
|
|
31
|
+
const depLabel = (dep: Block) => labelDep(dep, task.value?.parentId)
|
|
32
|
+
|
|
33
|
+
/** The pipeline a plain "Start" will use: the task's pinned pipeline, else the first. */
|
|
34
|
+
const defaultPipeline = computed(
|
|
35
|
+
() =>
|
|
36
|
+
(task.value?.pipelineId ? pipelines.getPipeline(task.value.pipelineId) : undefined) ??
|
|
37
|
+
pipelines.pipelines[0],
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
/** The PR the implementer agent opened for this task, if any. */
|
|
41
|
+
const pr = computed(() => task.value?.pullRequest)
|
|
42
|
+
const prLabel = computed(() => (pr.value?.number ? `PR #${pr.value.number}` : 'PR'))
|
|
43
|
+
|
|
44
|
+
// This task's current agent run (if any). A failed run must surface the shared
|
|
45
|
+
// failure banner + retry — NOT a stuck progress bar — so the card never looks
|
|
46
|
+
// like it's still working after the run has terminated.
|
|
47
|
+
const agentRun = computed(() => agentRuns.byBlock[props.taskId])
|
|
48
|
+
const runFailed = computed(() => agentRun.value?.status === 'failed')
|
|
49
|
+
|
|
50
|
+
// When this task backs a recurring pipeline, surface a small repeat badge so the
|
|
51
|
+
// service shows its scheduled work at a glance (full controls live in the inspector).
|
|
52
|
+
const recurring = useRecurringPipelinesStore()
|
|
53
|
+
const schedule = computed(() => recurring.byBlock(props.taskId))
|
|
54
|
+
|
|
55
|
+
// Optimistic "Start": flip the button into a spinning "Starting…" state the
|
|
56
|
+
// instant it's clicked, before the server confirms. The button naturally
|
|
57
|
+
// unmounts once the stream pushes the block into `in_progress`; if the start
|
|
58
|
+
// call faults we revert and surface a toast.
|
|
59
|
+
const starting = ref(false)
|
|
60
|
+
|
|
61
|
+
async function run() {
|
|
62
|
+
if (!runnable.value) {
|
|
63
|
+
toast.add({
|
|
64
|
+
title: 'Blocked by dependencies',
|
|
65
|
+
description: `Waiting on: ${unmet.value.map((d) => d.title).join(', ')}`,
|
|
66
|
+
icon: 'i-lucide-lock',
|
|
67
|
+
})
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
const pipeline = defaultPipeline.value
|
|
71
|
+
if (!pipeline) {
|
|
72
|
+
toast.add({ title: 'No pipeline defined', description: 'Create one in the builder first.' })
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
starting.value = true
|
|
76
|
+
try {
|
|
77
|
+
// false ⇒ the user cancelled the personal-password prompt; revert quietly (the run
|
|
78
|
+
// never started). On success the button unmounts once the stream pushes in_progress.
|
|
79
|
+
const started = await execution.start(props.taskId, pipeline)
|
|
80
|
+
if (!started) starting.value = false
|
|
81
|
+
} catch (e) {
|
|
82
|
+
// Real confirmation came back as a failure — revert the optimistic state.
|
|
83
|
+
starting.value = false
|
|
84
|
+
toast.add({
|
|
85
|
+
title: 'Failed to start',
|
|
86
|
+
description: e instanceof Error ? e.message : String(e),
|
|
87
|
+
color: 'error',
|
|
88
|
+
icon: 'i-lucide-alert-triangle',
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function review() {
|
|
94
|
+
ui.select(props.taskId)
|
|
95
|
+
ui.focus(props.taskId)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function merge() {
|
|
99
|
+
execution.mergePr(props.taskId)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// A `blocked` task is waiting on a human for one of two reasons — an agent-raised
|
|
103
|
+
// decision OR an approval gate — and both must surface here (a failed run is shown
|
|
104
|
+
// separately by the AgentFailureCard above). The board previously only handled
|
|
105
|
+
// decisions, so an approval-gated task was a dead end: it read "Decision needed"
|
|
106
|
+
// (the old generic `blocked` label) with no badge and a click that did nothing.
|
|
107
|
+
const pendingDecision = computed(() =>
|
|
108
|
+
execution.openDecisions.find((d) => d.blockId === props.taskId),
|
|
109
|
+
)
|
|
110
|
+
// The async stage an iterative reviewer gate (requirements-review / clarity-review) is
|
|
111
|
+
// mid-cycle in (folding the answers, then re-reviewing), or null. While set, the gate
|
|
112
|
+
// needs NO human action, so its approval is suppressed below and a working indicator
|
|
113
|
+
// shows instead.
|
|
114
|
+
const reviewStage = computed(() => reviews.stageForBlock(props.taskId))
|
|
115
|
+
const reviewStageLabel = computed(() =>
|
|
116
|
+
reviewStage.value === 'incorporating'
|
|
117
|
+
? 'Incorporating answers…'
|
|
118
|
+
: reviewStage.value === 'reviewing'
|
|
119
|
+
? 'Re-reviewing…'
|
|
120
|
+
: null,
|
|
121
|
+
)
|
|
122
|
+
const pendingApproval = computed(() => {
|
|
123
|
+
const a = execution.openApprovals.find((a) => a.blockId === props.taskId)
|
|
124
|
+
// A reviewer gate whose review is incorporating / re-reviewing in the driver is doing
|
|
125
|
+
// background work, not awaiting a human — don't surface it as "Approval needed".
|
|
126
|
+
if (a && reviews.isBackground(a.agentKind, props.taskId)) return undefined
|
|
127
|
+
return a
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
/** What this blocked task actually needs from a human — drives the card's label,
|
|
131
|
+
* pulse and action. Decision takes precedence over approval (a step never holds
|
|
132
|
+
* both at once; this is just a stable order). Null when nothing is pending. */
|
|
133
|
+
const attention = computed<{
|
|
134
|
+
label: string
|
|
135
|
+
icon: string
|
|
136
|
+
action: string
|
|
137
|
+
open: () => void
|
|
138
|
+
} | null>(() => {
|
|
139
|
+
const d = pendingDecision.value
|
|
140
|
+
if (d)
|
|
141
|
+
return {
|
|
142
|
+
label: 'Decision needed',
|
|
143
|
+
icon: 'i-lucide-circle-help',
|
|
144
|
+
action: 'Resolve',
|
|
145
|
+
open: () => ui.openDecision(d.instanceId, d.decision.id),
|
|
146
|
+
}
|
|
147
|
+
const a = pendingApproval.value
|
|
148
|
+
if (a)
|
|
149
|
+
return {
|
|
150
|
+
label: 'Approval needed',
|
|
151
|
+
icon: 'i-lucide-shield-check',
|
|
152
|
+
action: 'Approve',
|
|
153
|
+
open: () => ui.openApprovalDetail(a.instanceId, a.approval.id),
|
|
154
|
+
}
|
|
155
|
+
return null
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
/** Specific header copy: a failed run reads "Failed", a parked task reads its
|
|
159
|
+
* decision/approval reason, otherwise the generic status label. */
|
|
160
|
+
const statusText = computed(() =>
|
|
161
|
+
runFailed.value
|
|
162
|
+
? 'Failed'
|
|
163
|
+
: (reviewStageLabel.value ?? attention.value?.label ?? statusMeta.value?.label ?? ''),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
// Clicking the card body only selects the task (opening the inspector so the human can
|
|
167
|
+
// interact with it). Whatever the task is parked on — a decision, an approval, or the
|
|
168
|
+
// requirements review — is opened explicitly via the action button below, never by a
|
|
169
|
+
// click anywhere on the card. (A card-body click used to pop the review window open,
|
|
170
|
+
// which got in the way of just inspecting/editing the task.)
|
|
171
|
+
function selectTask() {
|
|
172
|
+
ui.select(props.taskId)
|
|
173
|
+
}
|
|
174
|
+
</script>
|
|
175
|
+
|
|
176
|
+
<template>
|
|
177
|
+
<div
|
|
178
|
+
v-if="task && statusMeta"
|
|
179
|
+
:data-block-id="task.id"
|
|
180
|
+
class="nodrag w-full cursor-pointer rounded-lg border bg-slate-950/70 p-2 text-left transition"
|
|
181
|
+
:class="[
|
|
182
|
+
selected ? 'border-white' : 'border-slate-700 hover:border-slate-500',
|
|
183
|
+
task.status === 'pr_ready' ? 'board-pulse-green' : attention ? 'board-pulse' : '',
|
|
184
|
+
]"
|
|
185
|
+
@click.stop="selectTask"
|
|
186
|
+
>
|
|
187
|
+
<!-- header row -->
|
|
188
|
+
<div class="flex items-center gap-1.5">
|
|
189
|
+
<span class="h-2 w-2 shrink-0 rounded-full" :style="{ backgroundColor: statusMeta.color }" />
|
|
190
|
+
<UIcon
|
|
191
|
+
v-if="schedule"
|
|
192
|
+
name="i-lucide-repeat"
|
|
193
|
+
class="h-3 w-3 shrink-0 text-indigo-400"
|
|
194
|
+
:title="schedule.enabled ? 'Recurring pipeline' : 'Recurring pipeline (paused)'"
|
|
195
|
+
/>
|
|
196
|
+
<span class="truncate text-[11px] font-semibold text-slate-100">{{ task.title }}</span>
|
|
197
|
+
<span
|
|
198
|
+
class="ml-auto shrink-0 text-[9px] uppercase tracking-wide"
|
|
199
|
+
:class="
|
|
200
|
+
runFailed
|
|
201
|
+
? 'text-rose-400'
|
|
202
|
+
: reviewStage
|
|
203
|
+
? 'text-indigo-300'
|
|
204
|
+
: attention
|
|
205
|
+
? 'text-amber-400'
|
|
206
|
+
: 'text-slate-500'
|
|
207
|
+
"
|
|
208
|
+
>
|
|
209
|
+
{{ statusText }}
|
|
210
|
+
</span>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<!-- a failed run: the shared failure banner + retry, never a stuck bar -->
|
|
214
|
+
<AgentFailureCard
|
|
215
|
+
v-if="runFailed && agentRun"
|
|
216
|
+
:run="agentRun"
|
|
217
|
+
variant="compact"
|
|
218
|
+
class="mt-1.5"
|
|
219
|
+
/>
|
|
220
|
+
|
|
221
|
+
<!-- progress while a pipeline runs (suppressed once the run has failed) -->
|
|
222
|
+
<UProgress
|
|
223
|
+
v-else-if="task.status === 'in_progress' || task.status === 'blocked'"
|
|
224
|
+
:model-value="Math.round(task.progress * 100)"
|
|
225
|
+
size="xs"
|
|
226
|
+
class="mt-1.5"
|
|
227
|
+
/>
|
|
228
|
+
|
|
229
|
+
<!-- spatial drill-down: build steps (at `steps` zoom) and each step's live
|
|
230
|
+
subtask todos (one band deeper, at `subtasks` zoom) -->
|
|
231
|
+
<TaskPipelineMini :task-id="taskId" />
|
|
232
|
+
|
|
233
|
+
<!-- dependencies (run order) -->
|
|
234
|
+
<div v-if="deps.length" class="mt-1.5 flex flex-wrap items-center gap-1">
|
|
235
|
+
<UIcon
|
|
236
|
+
:name="runnable ? 'i-lucide-link' : 'i-lucide-lock'"
|
|
237
|
+
class="h-3 w-3"
|
|
238
|
+
:class="runnable ? 'text-slate-500' : 'text-amber-400'"
|
|
239
|
+
/>
|
|
240
|
+
<span
|
|
241
|
+
v-for="d in deps"
|
|
242
|
+
:key="d.id"
|
|
243
|
+
class="inline-flex items-center gap-0.5 rounded bg-slate-800/80 px-1 py-0.5 text-[9px]"
|
|
244
|
+
:class="d.status === 'done' ? 'text-slate-400' : 'text-amber-300'"
|
|
245
|
+
:title="depLabel(d)"
|
|
246
|
+
>
|
|
247
|
+
<UIcon
|
|
248
|
+
:name="d.status === 'done' ? 'i-lucide-check' : 'i-lucide-clock'"
|
|
249
|
+
class="h-2.5 w-2.5"
|
|
250
|
+
/>
|
|
251
|
+
<span class="max-w-[110px] truncate">{{ depLabel(d) }}</span>
|
|
252
|
+
</span>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<!-- actions by state -->
|
|
256
|
+
<div class="nodrag mt-2 flex flex-wrap items-center gap-1">
|
|
257
|
+
<!-- a reviewer gate folding/re-reviewing in the background: a working indicator,
|
|
258
|
+
NOT a gate — the human is back on the board and summoned only if input is needed -->
|
|
259
|
+
<span v-if="reviewStage" class="inline-flex items-center gap-1 text-[9px] text-indigo-300">
|
|
260
|
+
<UIcon name="i-lucide-loader-circle" class="h-3 w-3 animate-spin" />
|
|
261
|
+
{{ reviewStageLabel }}
|
|
262
|
+
</span>
|
|
263
|
+
|
|
264
|
+
<!-- parked for a human: a decision to resolve or an approval gate to clear -->
|
|
265
|
+
<UButton
|
|
266
|
+
v-if="attention"
|
|
267
|
+
color="warning"
|
|
268
|
+
variant="soft"
|
|
269
|
+
size="xs"
|
|
270
|
+
:icon="attention.icon"
|
|
271
|
+
@click.stop="attention.open()"
|
|
272
|
+
>
|
|
273
|
+
{{ attention.action }}
|
|
274
|
+
</UButton>
|
|
275
|
+
|
|
276
|
+
<template v-if="task.status === 'planned' || task.status === 'ready'">
|
|
277
|
+
<UButton
|
|
278
|
+
:color="runnable ? 'primary' : 'neutral'"
|
|
279
|
+
variant="soft"
|
|
280
|
+
size="xs"
|
|
281
|
+
:icon="runnable ? 'i-lucide-play' : 'i-lucide-lock'"
|
|
282
|
+
:loading="starting"
|
|
283
|
+
:disabled="!runnable || starting"
|
|
284
|
+
:title="
|
|
285
|
+
runnable
|
|
286
|
+
? `Start ${defaultPipeline?.name ?? 'pipeline'}`
|
|
287
|
+
: `Waiting on: ${unmet.map((d) => d.title).join(', ')}`
|
|
288
|
+
"
|
|
289
|
+
@click.stop="run"
|
|
290
|
+
>
|
|
291
|
+
{{ starting ? 'Starting…' : runnable ? 'Start' : 'Blocked' }}
|
|
292
|
+
</UButton>
|
|
293
|
+
<span
|
|
294
|
+
v-if="runnable && defaultPipeline"
|
|
295
|
+
class="inline-flex items-center gap-0.5 text-[9px] text-slate-500"
|
|
296
|
+
>
|
|
297
|
+
<UIcon name="i-lucide-workflow" class="h-2.5 w-2.5" />{{ defaultPipeline.name }}
|
|
298
|
+
</span>
|
|
299
|
+
</template>
|
|
300
|
+
|
|
301
|
+
<template v-if="task.status === 'pr_ready'">
|
|
302
|
+
<UButton
|
|
303
|
+
v-if="pr"
|
|
304
|
+
:to="pr.url"
|
|
305
|
+
target="_blank"
|
|
306
|
+
rel="noopener"
|
|
307
|
+
external
|
|
308
|
+
color="neutral"
|
|
309
|
+
variant="soft"
|
|
310
|
+
size="xs"
|
|
311
|
+
icon="i-lucide-git-pull-request"
|
|
312
|
+
:title="`Open ${prLabel} on GitHub`"
|
|
313
|
+
@click.stop
|
|
314
|
+
>
|
|
315
|
+
{{ prLabel }}
|
|
316
|
+
</UButton>
|
|
317
|
+
<UButton
|
|
318
|
+
color="neutral"
|
|
319
|
+
variant="soft"
|
|
320
|
+
size="xs"
|
|
321
|
+
icon="i-lucide-scan-eye"
|
|
322
|
+
@click.stop="review"
|
|
323
|
+
>
|
|
324
|
+
Review
|
|
325
|
+
</UButton>
|
|
326
|
+
<UButton
|
|
327
|
+
color="success"
|
|
328
|
+
variant="solid"
|
|
329
|
+
size="xs"
|
|
330
|
+
icon="i-lucide-git-merge"
|
|
331
|
+
@click.stop="merge"
|
|
332
|
+
>
|
|
333
|
+
Merge
|
|
334
|
+
</UButton>
|
|
335
|
+
</template>
|
|
336
|
+
|
|
337
|
+
<span
|
|
338
|
+
v-else-if="task.status === 'done'"
|
|
339
|
+
class="inline-flex items-center gap-1 text-[9px] text-emerald-400"
|
|
340
|
+
>
|
|
341
|
+
<UIcon name="i-lucide-check-check" class="h-3 w-3" /> implemented
|
|
342
|
+
</span>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
<!-- structural metadata: assigned module -->
|
|
346
|
+
<div
|
|
347
|
+
v-if="task.moduleName"
|
|
348
|
+
class="mt-2 flex flex-wrap items-center gap-1 border-t border-slate-800 pt-2"
|
|
349
|
+
>
|
|
350
|
+
<span
|
|
351
|
+
class="inline-flex items-center gap-1 rounded bg-violet-500/15 px-1.5 py-0.5 text-[9px] text-violet-200"
|
|
352
|
+
:title="`Module: ${task.moduleName}`"
|
|
353
|
+
>
|
|
354
|
+
<UIcon :name="MODULE_META.icon" class="h-3 w-3" :style="{ color: MODULE_META.color }" />
|
|
355
|
+
{{ task.moduleName }}
|
|
356
|
+
</span>
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
</template>
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { AgentState } from '~/types/domain'
|
|
3
|
+
import { agentKindMeta } from '~/utils/catalog'
|
|
4
|
+
import { subtaskIconClass, isFailedStep, FAILED_STEP_META } from '~/utils/pipelineRender'
|
|
5
|
+
import { lodAtLeast } from '~/composables/useSemanticZoom'
|
|
6
|
+
|
|
7
|
+
// Spatial drill-down inside a task card: at the `steps` zoom band the task's
|
|
8
|
+
// build-pipeline steps appear, and one band deeper (`subtasks`) each step's live
|
|
9
|
+
// todo breakdown expands — done / in-progress / pending — exactly the way a
|
|
10
|
+
// zoomed-in bootstrap card reads. Renders nothing until the task has a run and
|
|
11
|
+
// the user has zoomed in far enough, so it's safe to mount on every task card.
|
|
12
|
+
const props = defineProps<{ taskId: string }>()
|
|
13
|
+
|
|
14
|
+
const execution = useExecutionStore()
|
|
15
|
+
const ui = useUiStore()
|
|
16
|
+
const expansion = useTaskExpansionStore()
|
|
17
|
+
const reviews = useReviewStage()
|
|
18
|
+
const { lod } = useSemanticZoom()
|
|
19
|
+
|
|
20
|
+
const instance = computed(() => execution.getByBlock(props.taskId))
|
|
21
|
+
const steps = computed(() => instance.value?.steps ?? [])
|
|
22
|
+
|
|
23
|
+
// A failed run is no longer executing: a step left mid-flight (state still
|
|
24
|
+
// `working`) must stop spinning, matching the failure card the task card shows.
|
|
25
|
+
const runFailed = computed(() => instance.value?.status === 'failed')
|
|
26
|
+
|
|
27
|
+
// Expand the pipeline list only when zoomed in far enough AND the board driver
|
|
28
|
+
// permits this card — on-screen, and the centre-most of any cards that would
|
|
29
|
+
// otherwise overlap (see useTaskExpansion) — so deep-zoom expansions don't pile up.
|
|
30
|
+
const showSteps = computed(
|
|
31
|
+
() =>
|
|
32
|
+
lodAtLeast(lod.value, 'steps') && steps.value.length > 0 && expansion.canExpand(props.taskId),
|
|
33
|
+
)
|
|
34
|
+
const showItems = computed(() => lodAtLeast(lod.value, 'subtasks'))
|
|
35
|
+
|
|
36
|
+
// Clicking a step opens the full agent step-detail overlay — execution metadata
|
|
37
|
+
// (state, timing, model, subtasks) plus the agent's prose output — exactly like
|
|
38
|
+
// clicking it from the inspector panel or the focus-view pipeline, rather than
|
|
39
|
+
// expanding the text inline inside the board card.
|
|
40
|
+
function openStep(i: number) {
|
|
41
|
+
if (instance.value) ui.openStepDetail(instance.value.id, i)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Per-state accent, matching the inspector/focus pipeline views. */
|
|
45
|
+
const STATE_META: Record<AgentState, { color: string; icon: string }> = {
|
|
46
|
+
pending: { color: '#64748b', icon: 'i-lucide-circle-dashed' },
|
|
47
|
+
working: { color: '#6366f1', icon: 'i-lucide-loader' },
|
|
48
|
+
waiting_decision: { color: '#f59e0b', icon: 'i-lucide-circle-help' },
|
|
49
|
+
done: { color: '#22c55e', icon: 'i-lucide-circle-check' },
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Same todo-status icons the bootstrap card uses, so a zoomed-in task reads the
|
|
53
|
+
// same way as a zoomed-in bootstrap.
|
|
54
|
+
const ITEM_ICON: Record<string, string> = {
|
|
55
|
+
completed: 'i-lucide-check-circle-2',
|
|
56
|
+
in_progress: 'i-lucide-loader-circle',
|
|
57
|
+
pending: 'i-lucide-circle',
|
|
58
|
+
}
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<template>
|
|
62
|
+
<div v-if="showSteps" class="mt-2 space-y-1 border-t border-slate-800 pt-2">
|
|
63
|
+
<div class="flex items-center gap-1 text-[9px] uppercase tracking-wide text-slate-500">
|
|
64
|
+
<UIcon name="i-lucide-workflow" class="h-2.5 w-2.5" />
|
|
65
|
+
Build steps
|
|
66
|
+
</div>
|
|
67
|
+
<div v-for="(s, i) in steps" :key="i" class="rounded bg-slate-900/60 px-1.5 py-1">
|
|
68
|
+
<div
|
|
69
|
+
class="flex cursor-pointer items-center gap-1"
|
|
70
|
+
:title="`${agentKindMeta(s.agentKind).label} — ${agentKindMeta(s.agentKind).description}\nClick to view step details & output`"
|
|
71
|
+
@click.stop="openStep(i)"
|
|
72
|
+
>
|
|
73
|
+
<UIcon
|
|
74
|
+
:name="agentKindMeta(s.agentKind).icon"
|
|
75
|
+
class="h-3 w-3 shrink-0"
|
|
76
|
+
:style="{ color: agentKindMeta(s.agentKind).color }"
|
|
77
|
+
/>
|
|
78
|
+
<span class="truncate text-[10px] text-slate-200">
|
|
79
|
+
{{ agentKindMeta(s.agentKind).label }}
|
|
80
|
+
</span>
|
|
81
|
+
<UIcon
|
|
82
|
+
v-if="s.output"
|
|
83
|
+
name="i-lucide-file-text"
|
|
84
|
+
class="h-2.5 w-2.5 shrink-0 text-slate-500"
|
|
85
|
+
/>
|
|
86
|
+
<span
|
|
87
|
+
v-if="s.subtasks && s.subtasks.total > 0"
|
|
88
|
+
class="ml-auto shrink-0 font-mono text-[9px] tabular-nums text-slate-400"
|
|
89
|
+
>
|
|
90
|
+
{{ s.subtasks.completed }}/{{ s.subtasks.total }}
|
|
91
|
+
</span>
|
|
92
|
+
<UIcon
|
|
93
|
+
v-else
|
|
94
|
+
:name="
|
|
95
|
+
isFailedStep(s.state, runFailed) ? FAILED_STEP_META.icon : STATE_META[s.state].icon
|
|
96
|
+
"
|
|
97
|
+
class="ml-auto h-2.5 w-2.5 shrink-0"
|
|
98
|
+
:class="s.state === 'working' && !runFailed ? 'animate-spin' : ''"
|
|
99
|
+
:style="{
|
|
100
|
+
color: isFailedStep(s.state, runFailed)
|
|
101
|
+
? FAILED_STEP_META.color
|
|
102
|
+
: STATE_META[s.state].color,
|
|
103
|
+
}"
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<!-- pending approval gate: jump straight to the conclusions reader. Suppressed
|
|
108
|
+
while a reviewer gate is folding/re-reviewing in the background (no human needed). -->
|
|
109
|
+
<button
|
|
110
|
+
v-if="
|
|
111
|
+
s.approval &&
|
|
112
|
+
s.approval.status === 'pending' &&
|
|
113
|
+
instance &&
|
|
114
|
+
!reviews.isBackground(s.agentKind, props.taskId)
|
|
115
|
+
"
|
|
116
|
+
type="button"
|
|
117
|
+
class="mt-1 flex w-full items-center justify-center gap-1 rounded bg-amber-500 px-1.5 py-0.5 text-[9px] font-semibold text-amber-950 transition hover:bg-amber-400"
|
|
118
|
+
@click.stop="ui.openApprovalDetail(instance.id, s.approval.id)"
|
|
119
|
+
>
|
|
120
|
+
<UIcon name="i-lucide-shield-check" class="h-2.5 w-2.5" />
|
|
121
|
+
Review & approve
|
|
122
|
+
</button>
|
|
123
|
+
|
|
124
|
+
<!-- per-step subtask progress bar -->
|
|
125
|
+
<div
|
|
126
|
+
v-if="s.subtasks && s.subtasks.total > 0"
|
|
127
|
+
class="mt-1 h-0.5 w-full overflow-hidden rounded bg-slate-700/60"
|
|
128
|
+
>
|
|
129
|
+
<div
|
|
130
|
+
class="h-full rounded bg-indigo-400 transition-all"
|
|
131
|
+
:style="{ width: `${(s.subtasks.completed / s.subtasks.total) * 100}%` }"
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<!-- deepest band: the actual todo list (done / in-progress / pending) -->
|
|
136
|
+
<ul v-if="showItems && s.subtasks?.items?.length" class="mt-1 space-y-0.5">
|
|
137
|
+
<li
|
|
138
|
+
v-for="(item, j) in s.subtasks.items"
|
|
139
|
+
:key="j"
|
|
140
|
+
class="flex items-start gap-1 text-[9px]"
|
|
141
|
+
:class="
|
|
142
|
+
item.status === 'completed'
|
|
143
|
+
? 'text-slate-500 line-through'
|
|
144
|
+
: item.status === 'in_progress'
|
|
145
|
+
? 'text-slate-100'
|
|
146
|
+
: 'text-slate-400'
|
|
147
|
+
"
|
|
148
|
+
>
|
|
149
|
+
<UIcon
|
|
150
|
+
:name="ITEM_ICON[item.status]"
|
|
151
|
+
class="mt-px h-2.5 w-2.5 shrink-0"
|
|
152
|
+
:class="subtaskIconClass(item.status, runFailed)"
|
|
153
|
+
/>
|
|
154
|
+
<span>{{ item.label }}</span>
|
|
155
|
+
</li>
|
|
156
|
+
</ul>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</template>
|