@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,433 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Block, BlockStatus } from '~/types/domain'
|
|
3
|
+
import { BLOCK_TYPE_META, STATUS_META } from '~/utils/catalog'
|
|
4
|
+
import DecisionBadge from './DecisionBadge.vue'
|
|
5
|
+
import DraggableTask from './DraggableTask.vue'
|
|
6
|
+
import ModuleFrame from './ModuleFrame.vue'
|
|
7
|
+
import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
|
|
8
|
+
import AgentStopButton from '~/components/board/AgentStopButton.vue'
|
|
9
|
+
import { useBlockDrag } from '~/composables/useBlockDrag'
|
|
10
|
+
import { useFrameResize } from '~/composables/useFrameResize'
|
|
11
|
+
|
|
12
|
+
// Vue Flow passes the node's `id` and `data` as props to custom node components.
|
|
13
|
+
// Only frames are rendered as board nodes; their tasks live inside the card.
|
|
14
|
+
const props = defineProps<{ id: string }>()
|
|
15
|
+
|
|
16
|
+
const board = useBoardStore()
|
|
17
|
+
const execution = useExecutionStore()
|
|
18
|
+
const ui = useUiStore()
|
|
19
|
+
const agentRuns = useAgentRunsStore()
|
|
20
|
+
const services = useServicesStore()
|
|
21
|
+
const reviews = useReviewStage()
|
|
22
|
+
const { lod } = useSemanticZoom()
|
|
23
|
+
|
|
24
|
+
const block = computed<Block | undefined>(() => board.getBlock(props.id))
|
|
25
|
+
/** This service frame is mounted on more than one board in the org. */
|
|
26
|
+
const isShared = computed(() => services.isSharedFrame(props.id))
|
|
27
|
+
const typeMeta = computed(() => (block.value ? BLOCK_TYPE_META[block.value.type] : null))
|
|
28
|
+
|
|
29
|
+
// ---- this service's children (tasks + modules) -----------------------------
|
|
30
|
+
const directTasks = computed(() => board.tasksOf(props.id))
|
|
31
|
+
const modules = computed(() => board.modulesOf(props.id))
|
|
32
|
+
const allTasks = computed(() => board.allTasksUnder(props.id))
|
|
33
|
+
const taskIds = computed(() => new Set(allTasks.value.map((t) => t.id)))
|
|
34
|
+
const taskCount = computed(() => allTasks.value.length)
|
|
35
|
+
const hasTasks = computed(() => taskCount.value > 0 || modules.value.length > 0)
|
|
36
|
+
const mergedTasks = computed(() => allTasks.value.filter((t) => t.status === 'done').length)
|
|
37
|
+
const prTasks = computed(() => allTasks.value.filter((t) => t.status === 'pr_ready').length)
|
|
38
|
+
const canvas = computed(() => board.containerSize(props.id))
|
|
39
|
+
|
|
40
|
+
// Frame status is derived from its tasks — services never reach "done".
|
|
41
|
+
const frameStatus = computed<BlockStatus>(() => board.frameStatus(props.id))
|
|
42
|
+
const statusMeta = computed(() => STATUS_META[frameStatus.value])
|
|
43
|
+
const accent = computed(() => statusMeta.value.color)
|
|
44
|
+
const FRAME_LABEL: Record<BlockStatus, string> = {
|
|
45
|
+
planned: 'No tasks',
|
|
46
|
+
ready: 'Live',
|
|
47
|
+
in_progress: 'Active',
|
|
48
|
+
blocked: 'Needs attention',
|
|
49
|
+
pr_ready: 'Active',
|
|
50
|
+
done: 'Live',
|
|
51
|
+
}
|
|
52
|
+
const statusLabel = computed(() => FRAME_LABEL[frameStatus.value])
|
|
53
|
+
|
|
54
|
+
const selected = computed(() => ui.selectedBlockId === props.id)
|
|
55
|
+
const expanded = computed(() => ui.isFrameExpanded(props.id))
|
|
56
|
+
// At far zoom we only ever show the chip; otherwise an expanded frame shows tasks.
|
|
57
|
+
const showExpanded = computed(() => expanded.value && lod.value !== 'far')
|
|
58
|
+
|
|
59
|
+
// Surface a pending decision from this frame OR any of its tasks.
|
|
60
|
+
const blockDecisions = computed(() =>
|
|
61
|
+
execution.openDecisions.filter((d) => d.blockId === props.id || taskIds.value.has(d.blockId)),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
function openFirstDecision() {
|
|
65
|
+
const d = blockDecisions.value[0]
|
|
66
|
+
if (d) ui.openDecision(d.instanceId, d.decision.id)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Surface a pending approval gate from this frame OR any of its tasks — but NOT an
|
|
70
|
+
// iterative reviewer gate (requirements-review / clarity-review) that's mid-cycle
|
|
71
|
+
// (incorporating / re-reviewing in the driver), which is background work needing no human,
|
|
72
|
+
// so it stays off the frame's "Approval" badge.
|
|
73
|
+
const blockApprovals = computed(() =>
|
|
74
|
+
execution.openApprovals.filter(
|
|
75
|
+
(a) =>
|
|
76
|
+
(a.blockId === props.id || taskIds.value.has(a.blockId)) &&
|
|
77
|
+
!reviews.isBackground(a.agentKind, a.blockId),
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
function openFirstApproval() {
|
|
82
|
+
const a = blockApprovals.value[0]
|
|
83
|
+
if (a) ui.openApprovalDetail(a.instanceId, a.approval.id)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toggleExpand() {
|
|
87
|
+
ui.toggleFrame(props.id)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Expanded frames are not Vue Flow-draggable (so the pane can pan through them),
|
|
91
|
+
// so they're repositioned by grabbing the header handle instead. Frames live in
|
|
92
|
+
// free-floating flow space, hence `clamp: false`.
|
|
93
|
+
const { startDrag } = useBlockDrag()
|
|
94
|
+
function onFrameHandle(e: PointerEvent) {
|
|
95
|
+
if (block.value) startDrag(block.value, e, { clamp: false })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Miro-style frame resizing: drag the right / bottom edges or the corner. Handles
|
|
99
|
+
// live on the expanded card's drop zone (see template); the composable clamps to
|
|
100
|
+
// the frame's content extent and persists the size on release.
|
|
101
|
+
const { startResize } = useFrameResize()
|
|
102
|
+
function onResize(e: PointerEvent, edge: 'e' | 's' | 'se') {
|
|
103
|
+
if (block.value) startResize(block.value, e, edge)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function addTask() {
|
|
107
|
+
ui.expandFrame(props.id)
|
|
108
|
+
ui.openAddTask(props.id)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function addRecurring() {
|
|
112
|
+
ui.openAddRecurring(props.id)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// A task needs merging → green pulse; a task needs a decision → amber pulse.
|
|
116
|
+
const pulseClass = computed(() => {
|
|
117
|
+
if (frameStatus.value === 'blocked') return 'board-pulse'
|
|
118
|
+
if (prTasks.value > 0) return 'board-pulse-green'
|
|
119
|
+
return ''
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// ---- agent-run overlay ------------------------------------------------------
|
|
123
|
+
// When this service frame was materialised by a "bootstrap repo" run, surface its
|
|
124
|
+
// live status + subtask progress on the card (the user watches the container adapt
|
|
125
|
+
// + push the repo), and the shared failure banner + retry if it faulted. Derived
|
|
126
|
+
// from the unified agentRuns store, keyed by this frame's block id.
|
|
127
|
+
const run = computed(() => agentRuns.byBlock[props.id])
|
|
128
|
+
const bootstrapping = computed(
|
|
129
|
+
() => run.value?.kind === 'bootstrap' && run.value.status === 'running',
|
|
130
|
+
)
|
|
131
|
+
const runFailed = computed(() => run.value?.status === 'failed')
|
|
132
|
+
const bootstrapSubtasks = computed(() =>
|
|
133
|
+
bootstrapping.value ? (run.value?.subtasks ?? null) : null,
|
|
134
|
+
)
|
|
135
|
+
const bootstrapPct = computed(() => {
|
|
136
|
+
const s = bootstrapSubtasks.value
|
|
137
|
+
if (!s || s.total <= 0) return 0
|
|
138
|
+
return Math.min(100, Math.round((s.completed / s.total) * 100))
|
|
139
|
+
})
|
|
140
|
+
// The actual todo items the agent is working through, surfaced on the expanded
|
|
141
|
+
// card so a zoomed-in user sees the task list, not just the "N/M" count.
|
|
142
|
+
const bootstrapItems = computed(() => bootstrapSubtasks.value?.items ?? [])
|
|
143
|
+
const ITEM_ICON: Record<string, string> = {
|
|
144
|
+
completed: 'i-lucide-check-circle-2',
|
|
145
|
+
in_progress: 'i-lucide-loader-circle',
|
|
146
|
+
pending: 'i-lucide-circle',
|
|
147
|
+
}
|
|
148
|
+
</script>
|
|
149
|
+
|
|
150
|
+
<template>
|
|
151
|
+
<div v-if="block" class="relative" :data-block-id="block.id">
|
|
152
|
+
<!-- decision / approval indicator floats above the card at all zoom levels -->
|
|
153
|
+
<div
|
|
154
|
+
v-if="blockDecisions.length || blockApprovals.length"
|
|
155
|
+
class="absolute -top-3 left-1/2 z-10 flex -translate-x-1/2 gap-1"
|
|
156
|
+
>
|
|
157
|
+
<DecisionBadge
|
|
158
|
+
v-if="blockDecisions.length"
|
|
159
|
+
:count="blockDecisions.length"
|
|
160
|
+
:compact="lod === 'far'"
|
|
161
|
+
@open="openFirstDecision"
|
|
162
|
+
/>
|
|
163
|
+
<DecisionBadge
|
|
164
|
+
v-if="blockApprovals.length"
|
|
165
|
+
:count="blockApprovals.length"
|
|
166
|
+
:compact="lod === 'far'"
|
|
167
|
+
label="Approval needed"
|
|
168
|
+
icon="i-lucide-shield-check"
|
|
169
|
+
@open="openFirstApproval"
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<!-- ===================== FAR: glanceable chip ===================== -->
|
|
174
|
+
<div
|
|
175
|
+
v-if="lod === 'far'"
|
|
176
|
+
class="flex w-44 items-center gap-2 rounded-xl border-2 px-3 py-3 shadow-lg backdrop-blur"
|
|
177
|
+
:class="[selected ? 'border-white' : '', pulseClass]"
|
|
178
|
+
:style="{ borderColor: accent, backgroundColor: accent + '26' }"
|
|
179
|
+
>
|
|
180
|
+
<span class="h-3 w-3 shrink-0 rounded-full" :style="{ backgroundColor: accent }" />
|
|
181
|
+
<span class="truncate text-sm font-semibold text-white">{{ block.title }}</span>
|
|
182
|
+
<UIcon
|
|
183
|
+
v-if="bootstrapping"
|
|
184
|
+
name="i-lucide-loader-circle"
|
|
185
|
+
class="ml-auto h-3.5 w-3.5 shrink-0 animate-spin text-amber-400"
|
|
186
|
+
title="Bootstrapping…"
|
|
187
|
+
/>
|
|
188
|
+
<UIcon
|
|
189
|
+
v-else-if="runFailed"
|
|
190
|
+
name="i-lucide-alert-triangle"
|
|
191
|
+
class="ml-auto h-3.5 w-3.5 shrink-0 text-rose-400"
|
|
192
|
+
title="Run failed"
|
|
193
|
+
/>
|
|
194
|
+
<span v-else-if="hasTasks" class="ml-auto shrink-0 text-[11px] text-slate-300">
|
|
195
|
+
{{ mergedTasks }}/{{ taskCount }}
|
|
196
|
+
</span>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<!-- ===================== COMPACT: summary (collapsed) ===================== -->
|
|
200
|
+
<div
|
|
201
|
+
v-else-if="!showExpanded"
|
|
202
|
+
class="w-56 overflow-hidden rounded-xl border bg-slate-900/90 shadow-xl backdrop-blur"
|
|
203
|
+
:class="[selected ? 'border-white' : 'border-slate-700', pulseClass]"
|
|
204
|
+
>
|
|
205
|
+
<div class="h-1.5 w-full" :style="{ backgroundColor: accent }" />
|
|
206
|
+
<!-- bootstrap-in-progress banner -->
|
|
207
|
+
<div v-if="bootstrapping" class="border-b border-amber-900/50 bg-amber-950/30 px-3 py-2">
|
|
208
|
+
<div class="flex items-center gap-1.5 text-[11px]">
|
|
209
|
+
<UIcon
|
|
210
|
+
name="i-lucide-loader-circle"
|
|
211
|
+
class="h-3.5 w-3.5 shrink-0 animate-spin text-amber-400"
|
|
212
|
+
/>
|
|
213
|
+
<span class="text-amber-300">Bootstrapping…</span>
|
|
214
|
+
<span v-if="bootstrapSubtasks" class="ml-auto text-amber-200/80">
|
|
215
|
+
{{ bootstrapSubtasks.completed }}/{{ bootstrapSubtasks.total }}
|
|
216
|
+
</span>
|
|
217
|
+
</div>
|
|
218
|
+
<div class="mt-1.5 h-1 w-full overflow-hidden rounded bg-amber-900/40">
|
|
219
|
+
<div
|
|
220
|
+
class="h-full rounded bg-amber-400 transition-all"
|
|
221
|
+
:style="{ width: bootstrapPct + '%' }"
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
<div v-if="run" class="mt-2 flex justify-end">
|
|
225
|
+
<AgentStopButton :run-id="run.runId" :kind="run.kind" size="xs" variant="ghost" />
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
<!-- failed run: shared failure banner + retry -->
|
|
229
|
+
<div v-else-if="runFailed && run" class="p-2">
|
|
230
|
+
<AgentFailureCard :run="run" variant="compact" />
|
|
231
|
+
</div>
|
|
232
|
+
<div class="space-y-2 p-3">
|
|
233
|
+
<div class="flex items-center gap-2">
|
|
234
|
+
<UIcon
|
|
235
|
+
:name="typeMeta!.icon"
|
|
236
|
+
class="h-4 w-4 shrink-0"
|
|
237
|
+
:style="{ color: typeMeta!.accent }"
|
|
238
|
+
/>
|
|
239
|
+
<span class="truncate text-sm font-semibold text-white">{{ block.title }}</span>
|
|
240
|
+
<UBadge
|
|
241
|
+
v-if="isShared"
|
|
242
|
+
color="info"
|
|
243
|
+
variant="subtle"
|
|
244
|
+
size="sm"
|
|
245
|
+
class="shrink-0"
|
|
246
|
+
title="Shared across workspaces in this org"
|
|
247
|
+
>
|
|
248
|
+
Shared
|
|
249
|
+
</UBadge>
|
|
250
|
+
</div>
|
|
251
|
+
<div class="flex items-center justify-between">
|
|
252
|
+
<UBadge :color="statusMeta.chip as any" variant="subtle" size="sm">{{
|
|
253
|
+
statusLabel
|
|
254
|
+
}}</UBadge>
|
|
255
|
+
<span class="text-[11px] text-slate-400"
|
|
256
|
+
>{{ taskCount }} task{{ taskCount === 1 ? '' : 's' }}</span
|
|
257
|
+
>
|
|
258
|
+
</div>
|
|
259
|
+
<button
|
|
260
|
+
type="button"
|
|
261
|
+
class="nodrag flex w-full items-center gap-1 rounded-md bg-slate-800/60 px-2 py-1 text-[10px] text-slate-300 hover:bg-slate-800"
|
|
262
|
+
@click.stop="toggleExpand"
|
|
263
|
+
>
|
|
264
|
+
<UIcon name="i-lucide-layers" class="h-3 w-3 text-slate-400" />
|
|
265
|
+
<span v-if="hasTasks">{{ mergedTasks }}/{{ taskCount }} merged</span>
|
|
266
|
+
<span v-else>No tasks yet</span>
|
|
267
|
+
<span v-if="prTasks" class="text-emerald-400">· {{ prTasks }} PR</span>
|
|
268
|
+
<UIcon name="i-lucide-chevron-down" class="ml-auto h-3 w-3" />
|
|
269
|
+
</button>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<!-- ===================== EXPANDED: 2D canvas of tasks + modules ===================== -->
|
|
274
|
+
<div
|
|
275
|
+
v-else
|
|
276
|
+
class="overflow-visible rounded-2xl border bg-slate-900/95 shadow-2xl backdrop-blur"
|
|
277
|
+
:class="[selected ? 'border-white' : 'border-slate-700', pulseClass]"
|
|
278
|
+
>
|
|
279
|
+
<div class="h-1.5 w-full rounded-t-2xl" :style="{ backgroundColor: accent }" />
|
|
280
|
+
<!-- bootstrap-in-progress banner -->
|
|
281
|
+
<div v-if="bootstrapping" class="border-b border-amber-900/50 bg-amber-950/30 px-4 py-2">
|
|
282
|
+
<div class="flex items-center gap-1.5 text-xs">
|
|
283
|
+
<UIcon
|
|
284
|
+
name="i-lucide-loader-circle"
|
|
285
|
+
class="h-4 w-4 shrink-0 animate-spin text-amber-400"
|
|
286
|
+
/>
|
|
287
|
+
<span class="text-amber-300">Bootstrapping repository…</span>
|
|
288
|
+
<span v-if="bootstrapSubtasks" class="ml-auto text-amber-200/80">
|
|
289
|
+
{{ bootstrapSubtasks.completed }}/{{ bootstrapSubtasks.total }} steps
|
|
290
|
+
</span>
|
|
291
|
+
</div>
|
|
292
|
+
<div class="mt-1.5 h-1 w-full overflow-hidden rounded bg-amber-900/40">
|
|
293
|
+
<div
|
|
294
|
+
class="h-full rounded bg-amber-400 transition-all"
|
|
295
|
+
:style="{ width: bootstrapPct + '%' }"
|
|
296
|
+
/>
|
|
297
|
+
</div>
|
|
298
|
+
<!-- the actual todo list, once the agent has reported any items -->
|
|
299
|
+
<ul v-if="bootstrapItems.length" class="mt-2 space-y-1">
|
|
300
|
+
<li
|
|
301
|
+
v-for="(item, i) in bootstrapItems"
|
|
302
|
+
:key="i"
|
|
303
|
+
class="flex items-start gap-1.5 text-[11px]"
|
|
304
|
+
:class="
|
|
305
|
+
item.status === 'completed'
|
|
306
|
+
? 'text-amber-200/60 line-through'
|
|
307
|
+
: item.status === 'in_progress'
|
|
308
|
+
? 'text-amber-100'
|
|
309
|
+
: 'text-amber-200/80'
|
|
310
|
+
"
|
|
311
|
+
>
|
|
312
|
+
<UIcon
|
|
313
|
+
:name="ITEM_ICON[item.status]"
|
|
314
|
+
class="mt-px h-3 w-3 shrink-0"
|
|
315
|
+
:class="[
|
|
316
|
+
item.status === 'in_progress' ? 'animate-spin text-amber-400' : '',
|
|
317
|
+
item.status === 'completed' ? 'text-emerald-400' : 'text-amber-400/70',
|
|
318
|
+
]"
|
|
319
|
+
/>
|
|
320
|
+
<span>{{ item.label }}</span>
|
|
321
|
+
</li>
|
|
322
|
+
</ul>
|
|
323
|
+
<div v-if="run" class="mt-2 flex justify-end">
|
|
324
|
+
<AgentStopButton :run-id="run.runId" :kind="run.kind" size="xs" variant="ghost" />
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
<!-- failed run: shared failure banner + retry -->
|
|
328
|
+
<div v-else-if="runFailed && run" class="p-3">
|
|
329
|
+
<AgentFailureCard :run="run" variant="expanded" />
|
|
330
|
+
</div>
|
|
331
|
+
<div class="space-y-3 p-4">
|
|
332
|
+
<!-- frame header (doubles as the drag handle for the expanded frame) -->
|
|
333
|
+
<div class="flex items-start justify-between gap-2">
|
|
334
|
+
<div
|
|
335
|
+
class="flex cursor-grab items-center gap-2 active:cursor-grabbing"
|
|
336
|
+
title="Drag service"
|
|
337
|
+
@pointerdown="onFrameHandle"
|
|
338
|
+
>
|
|
339
|
+
<div
|
|
340
|
+
class="flex h-8 w-8 items-center justify-center rounded-lg"
|
|
341
|
+
:style="{ backgroundColor: typeMeta!.accent + '22' }"
|
|
342
|
+
>
|
|
343
|
+
<UIcon :name="typeMeta!.icon" class="h-5 w-5" :style="{ color: typeMeta!.accent }" />
|
|
344
|
+
</div>
|
|
345
|
+
<div>
|
|
346
|
+
<div class="text-sm font-semibold text-white">{{ block.title }}</div>
|
|
347
|
+
<div class="text-[11px] text-slate-400">{{ typeMeta!.label }}</div>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
<div class="flex items-center gap-1">
|
|
351
|
+
<UBadge :color="statusMeta.chip as any" variant="subtle" size="sm">{{
|
|
352
|
+
statusLabel
|
|
353
|
+
}}</UBadge>
|
|
354
|
+
<UButton
|
|
355
|
+
class="nodrag"
|
|
356
|
+
size="xs"
|
|
357
|
+
variant="ghost"
|
|
358
|
+
color="neutral"
|
|
359
|
+
icon="i-lucide-plus"
|
|
360
|
+
title="Add task"
|
|
361
|
+
@click.stop="addTask"
|
|
362
|
+
/>
|
|
363
|
+
<UButton
|
|
364
|
+
class="nodrag"
|
|
365
|
+
size="xs"
|
|
366
|
+
variant="ghost"
|
|
367
|
+
color="neutral"
|
|
368
|
+
icon="i-lucide-repeat"
|
|
369
|
+
title="Add recurring pipeline"
|
|
370
|
+
@click.stop="addRecurring"
|
|
371
|
+
/>
|
|
372
|
+
<UButton
|
|
373
|
+
class="nodrag"
|
|
374
|
+
size="xs"
|
|
375
|
+
variant="ghost"
|
|
376
|
+
color="neutral"
|
|
377
|
+
icon="i-lucide-chevron-up"
|
|
378
|
+
title="Collapse"
|
|
379
|
+
@click.stop="toggleExpand"
|
|
380
|
+
/>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
|
|
384
|
+
<div class="flex items-center gap-2 text-[10px] uppercase tracking-wide text-slate-500">
|
|
385
|
+
<span>{{ mergedTasks }}/{{ taskCount }} implemented</span>
|
|
386
|
+
<span v-if="modules.length"
|
|
387
|
+
>· {{ modules.length }} module{{ modules.length === 1 ? '' : 's' }}</span
|
|
388
|
+
>
|
|
389
|
+
<span v-if="prTasks" class="text-emerald-400">· {{ prTasks }} PR ready</span>
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
<!-- the 2D drop zone: modules and loose tasks live here, draggable -->
|
|
393
|
+
<div
|
|
394
|
+
:data-drop-zone="block.id"
|
|
395
|
+
class="nodrag relative rounded-xl bg-slate-950/40"
|
|
396
|
+
:style="{ width: canvas.w + 'px', height: canvas.h + 'px' }"
|
|
397
|
+
>
|
|
398
|
+
<ModuleFrame v-for="m in modules" :key="m.id" :module-id="m.id" />
|
|
399
|
+
<DraggableTask v-for="t in directTasks" :key="t.id" :task-id="t.id" />
|
|
400
|
+
<button
|
|
401
|
+
v-if="!hasTasks"
|
|
402
|
+
type="button"
|
|
403
|
+
class="absolute inset-4 flex items-center justify-center gap-1 rounded-lg border border-dashed border-slate-700 text-[11px] text-slate-500 hover:border-slate-500 hover:text-slate-300"
|
|
404
|
+
@click.stop="addTask"
|
|
405
|
+
>
|
|
406
|
+
<UIcon name="i-lucide-plus" class="h-3.5 w-3.5" /> Add the first task
|
|
407
|
+
</button>
|
|
408
|
+
|
|
409
|
+
<!-- resize handles (drag the borders to resize the service, Miro-style) -->
|
|
410
|
+
<div
|
|
411
|
+
class="nodrag absolute right-0 top-0 h-full w-2 cursor-ew-resize hover:bg-sky-400/20"
|
|
412
|
+
title="Drag to resize"
|
|
413
|
+
@pointerdown="onResize($event, 'e')"
|
|
414
|
+
/>
|
|
415
|
+
<div
|
|
416
|
+
class="nodrag absolute bottom-0 left-0 h-2 w-full cursor-ns-resize hover:bg-sky-400/20"
|
|
417
|
+
title="Drag to resize"
|
|
418
|
+
@pointerdown="onResize($event, 's')"
|
|
419
|
+
/>
|
|
420
|
+
<div
|
|
421
|
+
class="nodrag absolute bottom-0 right-0 h-4 w-4 cursor-nwse-resize"
|
|
422
|
+
title="Drag to resize"
|
|
423
|
+
@pointerdown="onResize($event, 'se')"
|
|
424
|
+
>
|
|
425
|
+
<span
|
|
426
|
+
class="absolute bottom-1 right-1 h-2 w-2 rounded-sm border-b-2 border-r-2 border-slate-500"
|
|
427
|
+
/>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
withDefaults(
|
|
3
|
+
defineProps<{
|
|
4
|
+
count?: number
|
|
5
|
+
compact?: boolean
|
|
6
|
+
/** Badge copy + glyph; defaults to the decision variant. */
|
|
7
|
+
label?: string
|
|
8
|
+
icon?: string
|
|
9
|
+
}>(),
|
|
10
|
+
{ label: 'Decision needed', icon: 'i-lucide-circle-help' },
|
|
11
|
+
)
|
|
12
|
+
defineEmits<{ (e: 'open'): void }>()
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
class="board-pulse flex items-center gap-1 rounded-full bg-amber-500 px-2 py-0.5 text-xs font-semibold text-amber-950 shadow-lg transition hover:bg-amber-400"
|
|
19
|
+
@click.stop="$emit('open')"
|
|
20
|
+
>
|
|
21
|
+
<UIcon :name="icon" class="h-3.5 w-3.5" />
|
|
22
|
+
<span v-if="!compact">{{ label }}</span>
|
|
23
|
+
<span v-if="count && count > 1" class="rounded-full bg-amber-950/30 px-1">
|
|
24
|
+
{{ count }}
|
|
25
|
+
</span>
|
|
26
|
+
</button>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import TaskCard from './TaskCard.vue'
|
|
3
|
+
import { useBlockDrag } from '~/composables/useBlockDrag'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{ taskId: string }>()
|
|
6
|
+
const board = useBoardStore()
|
|
7
|
+
const task = computed(() => board.getBlock(props.taskId))
|
|
8
|
+
const { draggingId, startDrag } = useBlockDrag()
|
|
9
|
+
|
|
10
|
+
// Once a task is merged it stops being a unit of work and becomes part of the
|
|
11
|
+
// architecture: it no longer renders as a draggable card (arrows fall back to its
|
|
12
|
+
// container), so we never leave a zero-size anchor behind.
|
|
13
|
+
const merged = computed(() => task.value?.status === 'done')
|
|
14
|
+
|
|
15
|
+
function onHandle(e: PointerEvent) {
|
|
16
|
+
if (task.value) startDrag(task.value, e, { reparent: true })
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<template v-if="task">
|
|
22
|
+
<!-- in-flight task → draggable work card (merged tasks render nothing) -->
|
|
23
|
+
<div
|
|
24
|
+
v-if="!merged"
|
|
25
|
+
class="absolute w-[180px]"
|
|
26
|
+
:style="{
|
|
27
|
+
left: task.position.x + 'px',
|
|
28
|
+
top: task.position.y + 'px',
|
|
29
|
+
zIndex: draggingId === taskId ? 60 : 10,
|
|
30
|
+
// While this task is being dragged it must not capture hit-tests, so the
|
|
31
|
+
// drop-zone (service or module) beneath the cursor can be resolved on
|
|
32
|
+
// release — including the drag handle, which lives in this wrapper above
|
|
33
|
+
// the card and would otherwise mask the zone under it.
|
|
34
|
+
pointerEvents: draggingId === taskId ? 'none' : undefined,
|
|
35
|
+
}"
|
|
36
|
+
>
|
|
37
|
+
<!-- drag handle -->
|
|
38
|
+
<div
|
|
39
|
+
class="nodrag flex cursor-grab items-center justify-center rounded-t-lg border border-b-0 border-slate-700 bg-slate-800/80 py-px active:cursor-grabbing"
|
|
40
|
+
title="Drag task"
|
|
41
|
+
@pointerdown="onHandle"
|
|
42
|
+
>
|
|
43
|
+
<UIcon name="i-lucide-grip-horizontal" class="h-3 w-3 text-slate-500" />
|
|
44
|
+
</div>
|
|
45
|
+
<TaskCard :task-id="taskId" class="!rounded-t-none" />
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
</template>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import DraggableTask from './DraggableTask.vue'
|
|
3
|
+
import { MODULE_META } from '~/utils/catalog'
|
|
4
|
+
import { useBlockDrag } from '~/composables/useBlockDrag'
|
|
5
|
+
import { useFrameResize } from '~/composables/useFrameResize'
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{ moduleId: string }>()
|
|
8
|
+
const board = useBoardStore()
|
|
9
|
+
const ui = useUiStore()
|
|
10
|
+
|
|
11
|
+
const mod = computed(() => board.getBlock(props.moduleId))
|
|
12
|
+
const tasks = computed(() => board.tasksOf(props.moduleId))
|
|
13
|
+
const size = computed(() => board.containerSize(props.moduleId))
|
|
14
|
+
const selected = computed(() => ui.selectedBlockId === props.moduleId)
|
|
15
|
+
|
|
16
|
+
// A module groups the tasks inside it. We label it by how many tasks are still in
|
|
17
|
+
// flight, falling back to the total task count once everything inside has merged.
|
|
18
|
+
const inflight = computed(() => tasks.value.filter((t) => t.status !== 'done').length)
|
|
19
|
+
const total = computed(() => tasks.value.length)
|
|
20
|
+
|
|
21
|
+
const { draggingId, startDrag } = useBlockDrag()
|
|
22
|
+
|
|
23
|
+
// modules move within their service but don't get reparented
|
|
24
|
+
function onHandle(e: PointerEvent) {
|
|
25
|
+
if (mod.value) startDrag(mod.value, e)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Miro-style resizing, same as a service frame: drag the right / bottom edges or
|
|
29
|
+
// the corner. The composable clamps to the module's content extent and persists
|
|
30
|
+
// the size on release.
|
|
31
|
+
const { startResize } = useFrameResize()
|
|
32
|
+
function onResize(e: PointerEvent, edge: 'e' | 's' | 'se') {
|
|
33
|
+
if (mod.value) startResize(mod.value, e, edge)
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<div
|
|
39
|
+
v-if="mod"
|
|
40
|
+
:data-block-id="mod.id"
|
|
41
|
+
class="absolute rounded-xl border border-violet-500/40 bg-violet-500/[0.06]"
|
|
42
|
+
:class="{ 'ring-1 ring-white': selected }"
|
|
43
|
+
:style="{
|
|
44
|
+
left: mod.position.x + 'px',
|
|
45
|
+
top: mod.position.y + 'px',
|
|
46
|
+
width: size.w + 'px',
|
|
47
|
+
height: size.h + 'px',
|
|
48
|
+
zIndex: draggingId === moduleId ? 50 : 5,
|
|
49
|
+
}"
|
|
50
|
+
>
|
|
51
|
+
<!-- module header / drag handle -->
|
|
52
|
+
<div
|
|
53
|
+
class="nodrag flex h-[30px] cursor-grab items-center gap-1 rounded-t-xl bg-violet-500/15 px-2 active:cursor-grabbing"
|
|
54
|
+
@pointerdown="onHandle"
|
|
55
|
+
@click.stop="ui.select(moduleId)"
|
|
56
|
+
>
|
|
57
|
+
<UIcon
|
|
58
|
+
:name="MODULE_META.icon"
|
|
59
|
+
class="h-3.5 w-3.5 shrink-0"
|
|
60
|
+
:style="{ color: MODULE_META.color }"
|
|
61
|
+
/>
|
|
62
|
+
<span class="truncate text-[11px] font-semibold text-violet-100">{{ mod.title }}</span>
|
|
63
|
+
<span v-if="inflight" class="ml-auto shrink-0 text-[9px] text-violet-300/70">
|
|
64
|
+
{{ inflight }} task{{ inflight === 1 ? '' : 's' }}
|
|
65
|
+
</span>
|
|
66
|
+
<span v-else-if="total" class="ml-auto shrink-0 text-[9px] text-violet-300/70">
|
|
67
|
+
{{ total }} task{{ total === 1 ? '' : 's' }}
|
|
68
|
+
</span>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- drop zone for this module's tasks -->
|
|
72
|
+
<div :data-drop-zone="mod.id" class="relative" :style="{ height: size.h - 30 + 'px' }">
|
|
73
|
+
<DraggableTask v-for="t in tasks" :key="t.id" :task-id="t.id" />
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<!-- resize handles (drag the borders to resize the module, Miro-style) -->
|
|
77
|
+
<div
|
|
78
|
+
class="nodrag absolute right-0 top-0 h-full w-2 cursor-ew-resize hover:bg-violet-400/20"
|
|
79
|
+
title="Drag to resize"
|
|
80
|
+
@pointerdown="onResize($event, 'e')"
|
|
81
|
+
/>
|
|
82
|
+
<div
|
|
83
|
+
class="nodrag absolute bottom-0 left-0 h-2 w-full cursor-ns-resize hover:bg-violet-400/20"
|
|
84
|
+
title="Drag to resize"
|
|
85
|
+
@pointerdown="onResize($event, 's')"
|
|
86
|
+
/>
|
|
87
|
+
<div
|
|
88
|
+
class="nodrag absolute bottom-0 right-0 h-4 w-4 cursor-nwse-resize"
|
|
89
|
+
title="Drag to resize"
|
|
90
|
+
@pointerdown="onResize($event, 'se')"
|
|
91
|
+
>
|
|
92
|
+
<span
|
|
93
|
+
class="absolute bottom-1 right-1 h-2 w-2 rounded-sm border-b-2 border-r-2 border-violet-400/60"
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</template>
|