@cat-factory/app 1.0.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 +18 -0
- package/app/components/auth/UserMenu.vue +39 -0
- package/app/components/board/AgentFailureCard.vue +97 -0
- package/app/components/board/AgentStopButton.vue +61 -0
- package/app/components/board/BoardCanvas.vue +146 -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 +347 -0
- package/app/components/board/nodes/DecisionBadge.vue +21 -0
- package/app/components/board/nodes/DraggableTask.vue +69 -0
- package/app/components/board/nodes/ModuleFrame.vue +70 -0
- package/app/components/board/nodes/TaskCard.vue +237 -0
- package/app/components/bootstrap/BootstrapModal.vue +665 -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 +161 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
- package/app/components/github/GitHubConnect.vue +183 -0
- package/app/components/github/GitHubPanel.vue +584 -0
- package/app/components/layout/BoardSwitcher.vue +202 -0
- package/app/components/layout/BoardToolbar.vue +109 -0
- package/app/components/layout/SideBar.vue +193 -0
- package/app/components/layout/SpendWarningBanner.vue +107 -0
- package/app/components/palettes/AgentPalette.vue +33 -0
- package/app/components/palettes/BlockPalette.vue +41 -0
- package/app/components/palettes/PipelinePalette.vue +74 -0
- package/app/components/panels/DecisionModal.vue +71 -0
- package/app/components/panels/InspectorPanel.vue +296 -0
- package/app/components/panels/inspector/ContainerSummary.vue +74 -0
- package/app/components/panels/inspector/TaskDependencies.vue +70 -0
- package/app/components/panels/inspector/TaskExecution.vue +175 -0
- package/app/components/panels/inspector/TaskModelSettings.vue +128 -0
- package/app/components/panels/inspector/TaskStructure.vue +139 -0
- package/app/components/pipeline/PipelineBuilder.vue +227 -0
- package/app/components/pipeline/PipelineProgress.vue +246 -0
- package/app/components/requirements/RequirementReviewModal.vue +328 -0
- package/app/components/scenarios/FeatureScenarios.vue +162 -0
- package/app/components/scenarios/ScenarioCard.vue +109 -0
- package/app/components/tasks/TaskContextIssues.vue +88 -0
- package/app/components/tasks/TaskImportModal.vue +140 -0
- package/app/components/tasks/TaskSourceConnectModal.vue +122 -0
- package/app/composables/useApi.ts +535 -0
- package/app/composables/useBlockDrag.ts +75 -0
- package/app/composables/useBlockQueries.ts +136 -0
- package/app/composables/useBoardFlow.ts +11 -0
- package/app/composables/useDepLabels.ts +26 -0
- package/app/composables/useSemanticZoom.ts +16 -0
- package/app/composables/useWorkspaceStream.ts +125 -0
- package/app/docs/architecture.md +31 -0
- package/app/pages/index.vue +80 -0
- package/app/stores/accounts.ts +64 -0
- package/app/stores/agentRuns.ts +117 -0
- package/app/stores/agents.ts +40 -0
- package/app/stores/auth.ts +97 -0
- package/app/stores/board.spec.ts +197 -0
- package/app/stores/board.ts +147 -0
- package/app/stores/bootstrap.ts +97 -0
- package/app/stores/documents.ts +165 -0
- package/app/stores/execution.ts +115 -0
- package/app/stores/fragmentLibrary.ts +147 -0
- package/app/stores/fragments.ts +40 -0
- package/app/stores/github.ts +291 -0
- package/app/stores/models.ts +48 -0
- package/app/stores/pipelines.ts +77 -0
- package/app/stores/requirements.ts +133 -0
- package/app/stores/scenarios.spec.ts +82 -0
- package/app/stores/scenarios.ts +196 -0
- package/app/stores/tasks.spec.ts +71 -0
- package/app/stores/tasks.ts +149 -0
- package/app/stores/ui.ts +204 -0
- package/app/stores/workspace.ts +201 -0
- package/app/types/accounts.ts +38 -0
- package/app/types/bootstrap.ts +83 -0
- package/app/types/documents.ts +92 -0
- package/app/types/domain.ts +216 -0
- package/app/types/execution.ts +110 -0
- package/app/types/fragments.ts +72 -0
- package/app/types/github.ts +153 -0
- package/app/types/models.ts +48 -0
- package/app/types/requirements.ts +38 -0
- package/app/types/scenarios.ts +36 -0
- package/app/types/tasks.ts +67 -0
- package/app/utils/catalog.spec.ts +82 -0
- package/app/utils/catalog.ts +185 -0
- package/app/utils/dnd.ts +29 -0
- package/nuxt.config.ts +43 -0
- package/package.json +43 -0
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
|
|
6
|
+
const props = defineProps<{ moduleId: string }>()
|
|
7
|
+
const board = useBoardStore()
|
|
8
|
+
const ui = useUiStore()
|
|
9
|
+
|
|
10
|
+
const mod = computed(() => board.getBlock(props.moduleId))
|
|
11
|
+
const tasks = computed(() => board.tasksOf(props.moduleId))
|
|
12
|
+
const size = computed(() => board.containerSize(props.moduleId))
|
|
13
|
+
const selected = computed(() => ui.selectedBlockId === props.moduleId)
|
|
14
|
+
|
|
15
|
+
// A module groups the features left behind by its merged tasks. We label it by
|
|
16
|
+
// feature count once any work has merged, falling back to a task count while
|
|
17
|
+
// work is still in flight inside it.
|
|
18
|
+
const featureCount = computed(() =>
|
|
19
|
+
tasks.value.filter((t) => t.status === 'done').reduce((n, t) => n + (t.features?.length ?? 0), 0),
|
|
20
|
+
)
|
|
21
|
+
const inflight = computed(() => tasks.value.filter((t) => t.status !== 'done').length)
|
|
22
|
+
|
|
23
|
+
const { draggingId, startDrag } = useBlockDrag()
|
|
24
|
+
|
|
25
|
+
// modules move within their service but don't get reparented
|
|
26
|
+
function onHandle(e: PointerEvent) {
|
|
27
|
+
if (mod.value) startDrag(mod.value, e)
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<div
|
|
33
|
+
v-if="mod"
|
|
34
|
+
:data-block-id="mod.id"
|
|
35
|
+
class="absolute rounded-xl border border-violet-500/40 bg-violet-500/[0.06]"
|
|
36
|
+
:class="{ 'ring-1 ring-white': selected }"
|
|
37
|
+
:style="{
|
|
38
|
+
left: mod.position.x + 'px',
|
|
39
|
+
top: mod.position.y + 'px',
|
|
40
|
+
width: size.w + 'px',
|
|
41
|
+
height: size.h + 'px',
|
|
42
|
+
zIndex: draggingId === moduleId ? 50 : 5,
|
|
43
|
+
}"
|
|
44
|
+
>
|
|
45
|
+
<!-- module header / drag handle -->
|
|
46
|
+
<div
|
|
47
|
+
class="nodrag flex h-[30px] cursor-grab items-center gap-1 rounded-t-xl bg-violet-500/15 px-2 active:cursor-grabbing"
|
|
48
|
+
@pointerdown="onHandle"
|
|
49
|
+
@click.stop="ui.select(moduleId)"
|
|
50
|
+
>
|
|
51
|
+
<UIcon
|
|
52
|
+
:name="MODULE_META.icon"
|
|
53
|
+
class="h-3.5 w-3.5 shrink-0"
|
|
54
|
+
:style="{ color: MODULE_META.color }"
|
|
55
|
+
/>
|
|
56
|
+
<span class="truncate text-[11px] font-semibold text-violet-100">{{ mod.title }}</span>
|
|
57
|
+
<span v-if="featureCount" class="ml-auto shrink-0 text-[9px] text-violet-300/70">
|
|
58
|
+
{{ featureCount }} feature{{ featureCount === 1 ? '' : 's' }}
|
|
59
|
+
</span>
|
|
60
|
+
<span v-else class="ml-auto shrink-0 text-[9px] text-violet-300/70">
|
|
61
|
+
{{ inflight }} task{{ inflight === 1 ? '' : 's' }}
|
|
62
|
+
</span>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<!-- drop zone for this module's tasks -->
|
|
66
|
+
<div :data-drop-zone="mod.id" class="relative" :style="{ height: size.h - 30 + 'px' }">
|
|
67
|
+
<DraggableTask v-for="t in tasks" :key="t.id" :task-id="t.id" />
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</template>
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Block } from '~/types/domain'
|
|
3
|
+
import {
|
|
4
|
+
STATUS_META,
|
|
5
|
+
FEATURE_META,
|
|
6
|
+
MODULE_META,
|
|
7
|
+
DEFAULT_CONFIDENCE_THRESHOLD,
|
|
8
|
+
} from '~/utils/catalog'
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{ taskId: string }>()
|
|
11
|
+
|
|
12
|
+
const board = useBoardStore()
|
|
13
|
+
const execution = useExecutionStore()
|
|
14
|
+
const pipelines = usePipelinesStore()
|
|
15
|
+
const ui = useUiStore()
|
|
16
|
+
const toast = useToast()
|
|
17
|
+
|
|
18
|
+
const task = computed<Block | undefined>(() => board.getBlock(props.taskId))
|
|
19
|
+
const statusMeta = computed(() => (task.value ? STATUS_META[task.value.status] : null))
|
|
20
|
+
const features = computed(() => task.value?.features ?? [])
|
|
21
|
+
const selected = computed(() => ui.selectedBlockId === props.taskId)
|
|
22
|
+
|
|
23
|
+
// ---- dependencies (gate execution order; may point across frames) ----------
|
|
24
|
+
const deps = computed(() =>
|
|
25
|
+
(task.value?.dependsOn ?? []).map((id) => board.getBlock(id)).filter((b): b is Block => !!b),
|
|
26
|
+
)
|
|
27
|
+
/** Deps that haven't merged yet — these block this task from running. */
|
|
28
|
+
const unmet = computed(() => board.unmetDeps(props.taskId))
|
|
29
|
+
const runnable = computed(() => board.isRunnable(props.taskId))
|
|
30
|
+
|
|
31
|
+
/** Label a dependency, noting its frame when it lives in another one. */
|
|
32
|
+
const { depLabel: labelDep } = useDepLabels()
|
|
33
|
+
const depLabel = (dep: Block) => labelDep(dep, task.value?.parentId)
|
|
34
|
+
|
|
35
|
+
const threshold = computed(() => task.value?.confidenceThreshold ?? DEFAULT_CONFIDENCE_THRESHOLD)
|
|
36
|
+
/** The pipeline a plain "Start" will use (first defined pipeline). */
|
|
37
|
+
const defaultPipeline = computed(() => pipelines.pipelines[0])
|
|
38
|
+
const confidencePct = computed(() =>
|
|
39
|
+
task.value?.confidence != null ? Math.round(task.value.confidence * 100) : null,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
/** The PR the implementer agent opened for this task, if any. */
|
|
43
|
+
const pr = computed(() => task.value?.pullRequest)
|
|
44
|
+
const prLabel = computed(() => (pr.value?.number ? `PR #${pr.value.number}` : 'PR'))
|
|
45
|
+
|
|
46
|
+
function run() {
|
|
47
|
+
if (!runnable.value) {
|
|
48
|
+
toast.add({
|
|
49
|
+
title: 'Blocked by dependencies',
|
|
50
|
+
description: `Waiting on: ${unmet.value.map((d) => d.title).join(', ')}`,
|
|
51
|
+
icon: 'i-lucide-lock',
|
|
52
|
+
})
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
const pipeline = pipelines.pipelines[0]
|
|
56
|
+
if (!pipeline) {
|
|
57
|
+
toast.add({ title: 'No pipeline defined', description: 'Create one in the builder first.' })
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
execution.start(props.taskId, pipeline)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function review() {
|
|
64
|
+
ui.select(props.taskId)
|
|
65
|
+
ui.focus(props.taskId)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function merge() {
|
|
69
|
+
execution.mergePr(props.taskId)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A task with an unresolved decision: clicking it jumps straight to the modal.
|
|
73
|
+
const pendingDecision = computed(() =>
|
|
74
|
+
execution.openDecisions.find((d) => d.blockId === props.taskId),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
function selectTask() {
|
|
78
|
+
ui.select(props.taskId)
|
|
79
|
+
const d = pendingDecision.value
|
|
80
|
+
if (d) ui.openDecision(d.instanceId, d.decision.id)
|
|
81
|
+
}
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<template>
|
|
85
|
+
<div
|
|
86
|
+
v-if="task && statusMeta"
|
|
87
|
+
:data-block-id="task.id"
|
|
88
|
+
class="nodrag w-full cursor-pointer rounded-lg border bg-slate-950/70 p-2 text-left transition"
|
|
89
|
+
:class="[
|
|
90
|
+
selected ? 'border-white' : 'border-slate-700 hover:border-slate-500',
|
|
91
|
+
task.status === 'pr_ready' ? 'board-pulse-green' : '',
|
|
92
|
+
]"
|
|
93
|
+
@click.stop="selectTask"
|
|
94
|
+
>
|
|
95
|
+
<!-- header row -->
|
|
96
|
+
<div class="flex items-center gap-1.5">
|
|
97
|
+
<span class="h-2 w-2 shrink-0 rounded-full" :style="{ backgroundColor: statusMeta.color }" />
|
|
98
|
+
<span class="truncate text-[11px] font-semibold text-slate-100">{{ task.title }}</span>
|
|
99
|
+
<span class="ml-auto shrink-0 text-[9px] uppercase tracking-wide text-slate-500">
|
|
100
|
+
{{ statusMeta.label }}
|
|
101
|
+
</span>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<!-- progress while a pipeline runs -->
|
|
105
|
+
<UProgress
|
|
106
|
+
v-if="task.status === 'in_progress' || task.status === 'blocked'"
|
|
107
|
+
:model-value="Math.round(task.progress * 100)"
|
|
108
|
+
size="xs"
|
|
109
|
+
class="mt-1.5"
|
|
110
|
+
/>
|
|
111
|
+
|
|
112
|
+
<!-- dependencies (run order) -->
|
|
113
|
+
<div v-if="deps.length" class="mt-1.5 flex flex-wrap items-center gap-1">
|
|
114
|
+
<UIcon
|
|
115
|
+
:name="runnable ? 'i-lucide-link' : 'i-lucide-lock'"
|
|
116
|
+
class="h-3 w-3"
|
|
117
|
+
:class="runnable ? 'text-slate-500' : 'text-amber-400'"
|
|
118
|
+
/>
|
|
119
|
+
<span
|
|
120
|
+
v-for="d in deps"
|
|
121
|
+
:key="d.id"
|
|
122
|
+
class="inline-flex items-center gap-0.5 rounded bg-slate-800/80 px-1 py-0.5 text-[9px]"
|
|
123
|
+
:class="d.status === 'done' ? 'text-slate-400' : 'text-amber-300'"
|
|
124
|
+
:title="depLabel(d)"
|
|
125
|
+
>
|
|
126
|
+
<UIcon
|
|
127
|
+
:name="d.status === 'done' ? 'i-lucide-check' : 'i-lucide-clock'"
|
|
128
|
+
class="h-2.5 w-2.5"
|
|
129
|
+
/>
|
|
130
|
+
<span class="max-w-[110px] truncate">{{ depLabel(d) }}</span>
|
|
131
|
+
</span>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<!-- confidence vs threshold (once scored) -->
|
|
135
|
+
<div v-if="confidencePct != null" class="mt-1.5 flex items-center gap-1 text-[9px]">
|
|
136
|
+
<UIcon name="i-lucide-gauge" class="h-3 w-3 text-slate-500" />
|
|
137
|
+
<span :class="task.confidence! >= threshold ? 'text-emerald-400' : 'text-amber-400'">
|
|
138
|
+
{{ confidencePct }}% conf
|
|
139
|
+
</span>
|
|
140
|
+
<span class="text-slate-600">· need {{ Math.round(threshold * 100) }}%</span>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<!-- actions by state -->
|
|
144
|
+
<div class="nodrag mt-2 flex flex-wrap items-center gap-1">
|
|
145
|
+
<template v-if="task.status === 'planned' || task.status === 'ready'">
|
|
146
|
+
<UButton
|
|
147
|
+
:color="runnable ? 'primary' : 'neutral'"
|
|
148
|
+
variant="soft"
|
|
149
|
+
size="xs"
|
|
150
|
+
:icon="runnable ? 'i-lucide-play' : 'i-lucide-lock'"
|
|
151
|
+
:disabled="!runnable"
|
|
152
|
+
:title="
|
|
153
|
+
runnable
|
|
154
|
+
? `Start ${defaultPipeline?.name ?? 'pipeline'}`
|
|
155
|
+
: `Waiting on: ${unmet.map((d) => d.title).join(', ')}`
|
|
156
|
+
"
|
|
157
|
+
@click.stop="run"
|
|
158
|
+
>
|
|
159
|
+
{{ runnable ? 'Start' : 'Blocked' }}
|
|
160
|
+
</UButton>
|
|
161
|
+
<span
|
|
162
|
+
v-if="runnable && defaultPipeline"
|
|
163
|
+
class="inline-flex items-center gap-0.5 text-[9px] text-slate-500"
|
|
164
|
+
>
|
|
165
|
+
<UIcon name="i-lucide-workflow" class="h-2.5 w-2.5" />{{ defaultPipeline.name }}
|
|
166
|
+
</span>
|
|
167
|
+
</template>
|
|
168
|
+
|
|
169
|
+
<template v-if="task.status === 'pr_ready'">
|
|
170
|
+
<UButton
|
|
171
|
+
v-if="pr"
|
|
172
|
+
:to="pr.url"
|
|
173
|
+
target="_blank"
|
|
174
|
+
rel="noopener"
|
|
175
|
+
external
|
|
176
|
+
color="neutral"
|
|
177
|
+
variant="soft"
|
|
178
|
+
size="xs"
|
|
179
|
+
icon="i-lucide-git-pull-request"
|
|
180
|
+
:title="`Open ${prLabel} on GitHub`"
|
|
181
|
+
@click.stop
|
|
182
|
+
>
|
|
183
|
+
{{ prLabel }}
|
|
184
|
+
</UButton>
|
|
185
|
+
<UButton
|
|
186
|
+
color="neutral"
|
|
187
|
+
variant="soft"
|
|
188
|
+
size="xs"
|
|
189
|
+
icon="i-lucide-scan-eye"
|
|
190
|
+
@click.stop="review"
|
|
191
|
+
>
|
|
192
|
+
Review
|
|
193
|
+
</UButton>
|
|
194
|
+
<UButton
|
|
195
|
+
color="success"
|
|
196
|
+
variant="solid"
|
|
197
|
+
size="xs"
|
|
198
|
+
icon="i-lucide-git-merge"
|
|
199
|
+
@click.stop="merge"
|
|
200
|
+
>
|
|
201
|
+
Merge
|
|
202
|
+
</UButton>
|
|
203
|
+
</template>
|
|
204
|
+
|
|
205
|
+
<span
|
|
206
|
+
v-else-if="task.status === 'done'"
|
|
207
|
+
class="inline-flex items-center gap-1 text-[9px] text-emerald-400"
|
|
208
|
+
>
|
|
209
|
+
<UIcon name="i-lucide-check-check" class="h-3 w-3" /> implemented
|
|
210
|
+
</span>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<!-- structural metadata: assigned module + implemented features -->
|
|
214
|
+
<div
|
|
215
|
+
v-if="task.moduleName || features.length"
|
|
216
|
+
class="mt-2 flex flex-wrap items-center gap-1 border-t border-slate-800 pt-2"
|
|
217
|
+
>
|
|
218
|
+
<span
|
|
219
|
+
v-if="task.moduleName"
|
|
220
|
+
class="inline-flex items-center gap-1 rounded bg-violet-500/15 px-1.5 py-0.5 text-[9px] text-violet-200"
|
|
221
|
+
:title="`Module: ${task.moduleName}`"
|
|
222
|
+
>
|
|
223
|
+
<UIcon :name="MODULE_META.icon" class="h-3 w-3" :style="{ color: MODULE_META.color }" />
|
|
224
|
+
{{ task.moduleName }}
|
|
225
|
+
</span>
|
|
226
|
+
<span
|
|
227
|
+
v-for="f in features"
|
|
228
|
+
:key="f"
|
|
229
|
+
class="inline-flex items-center gap-1 rounded bg-slate-800/80 px-1.5 py-0.5 text-[9px] text-slate-200"
|
|
230
|
+
:title="`Feature: ${f}`"
|
|
231
|
+
>
|
|
232
|
+
<UIcon :name="FEATURE_META.icon" class="h-3 w-3" :style="{ color: FEATURE_META.color }" />
|
|
233
|
+
<span class="max-w-[110px] truncate">{{ f }}</span>
|
|
234
|
+
</span>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</template>
|