@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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/app/app.config.ts +8 -0
  4. package/app/app.vue +11 -0
  5. package/app/assets/css/main.css +100 -0
  6. package/app/components/auth/AuthGate.vue +24 -0
  7. package/app/components/auth/LoginScreen.vue +18 -0
  8. package/app/components/auth/UserMenu.vue +39 -0
  9. package/app/components/board/AgentFailureCard.vue +97 -0
  10. package/app/components/board/AgentStopButton.vue +61 -0
  11. package/app/components/board/BoardCanvas.vue +146 -0
  12. package/app/components/board/TaskDependencyEdges.vue +132 -0
  13. package/app/components/board/nodes/AgentChip.vue +59 -0
  14. package/app/components/board/nodes/BlockNode.vue +347 -0
  15. package/app/components/board/nodes/DecisionBadge.vue +21 -0
  16. package/app/components/board/nodes/DraggableTask.vue +69 -0
  17. package/app/components/board/nodes/ModuleFrame.vue +70 -0
  18. package/app/components/board/nodes/TaskCard.vue +237 -0
  19. package/app/components/bootstrap/BootstrapModal.vue +665 -0
  20. package/app/components/documents/DocumentImportModal.vue +161 -0
  21. package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
  22. package/app/components/documents/SpawnPreviewModal.vue +161 -0
  23. package/app/components/documents/TaskContextDocs.vue +83 -0
  24. package/app/components/focus/BlockFocusView.vue +161 -0
  25. package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
  26. package/app/components/github/GitHubConnect.vue +183 -0
  27. package/app/components/github/GitHubPanel.vue +584 -0
  28. package/app/components/layout/BoardSwitcher.vue +202 -0
  29. package/app/components/layout/BoardToolbar.vue +109 -0
  30. package/app/components/layout/SideBar.vue +193 -0
  31. package/app/components/layout/SpendWarningBanner.vue +107 -0
  32. package/app/components/palettes/AgentPalette.vue +33 -0
  33. package/app/components/palettes/BlockPalette.vue +41 -0
  34. package/app/components/palettes/PipelinePalette.vue +74 -0
  35. package/app/components/panels/DecisionModal.vue +71 -0
  36. package/app/components/panels/InspectorPanel.vue +296 -0
  37. package/app/components/panels/inspector/ContainerSummary.vue +74 -0
  38. package/app/components/panels/inspector/TaskDependencies.vue +70 -0
  39. package/app/components/panels/inspector/TaskExecution.vue +175 -0
  40. package/app/components/panels/inspector/TaskModelSettings.vue +128 -0
  41. package/app/components/panels/inspector/TaskStructure.vue +139 -0
  42. package/app/components/pipeline/PipelineBuilder.vue +227 -0
  43. package/app/components/pipeline/PipelineProgress.vue +246 -0
  44. package/app/components/requirements/RequirementReviewModal.vue +328 -0
  45. package/app/components/scenarios/FeatureScenarios.vue +162 -0
  46. package/app/components/scenarios/ScenarioCard.vue +109 -0
  47. package/app/components/tasks/TaskContextIssues.vue +88 -0
  48. package/app/components/tasks/TaskImportModal.vue +140 -0
  49. package/app/components/tasks/TaskSourceConnectModal.vue +122 -0
  50. package/app/composables/useApi.ts +535 -0
  51. package/app/composables/useBlockDrag.ts +75 -0
  52. package/app/composables/useBlockQueries.ts +136 -0
  53. package/app/composables/useBoardFlow.ts +11 -0
  54. package/app/composables/useDepLabels.ts +26 -0
  55. package/app/composables/useSemanticZoom.ts +16 -0
  56. package/app/composables/useWorkspaceStream.ts +125 -0
  57. package/app/docs/architecture.md +31 -0
  58. package/app/pages/index.vue +80 -0
  59. package/app/stores/accounts.ts +64 -0
  60. package/app/stores/agentRuns.ts +117 -0
  61. package/app/stores/agents.ts +40 -0
  62. package/app/stores/auth.ts +97 -0
  63. package/app/stores/board.spec.ts +197 -0
  64. package/app/stores/board.ts +147 -0
  65. package/app/stores/bootstrap.ts +97 -0
  66. package/app/stores/documents.ts +165 -0
  67. package/app/stores/execution.ts +115 -0
  68. package/app/stores/fragmentLibrary.ts +147 -0
  69. package/app/stores/fragments.ts +40 -0
  70. package/app/stores/github.ts +291 -0
  71. package/app/stores/models.ts +48 -0
  72. package/app/stores/pipelines.ts +77 -0
  73. package/app/stores/requirements.ts +133 -0
  74. package/app/stores/scenarios.spec.ts +82 -0
  75. package/app/stores/scenarios.ts +196 -0
  76. package/app/stores/tasks.spec.ts +71 -0
  77. package/app/stores/tasks.ts +149 -0
  78. package/app/stores/ui.ts +204 -0
  79. package/app/stores/workspace.ts +201 -0
  80. package/app/types/accounts.ts +38 -0
  81. package/app/types/bootstrap.ts +83 -0
  82. package/app/types/documents.ts +92 -0
  83. package/app/types/domain.ts +216 -0
  84. package/app/types/execution.ts +110 -0
  85. package/app/types/fragments.ts +72 -0
  86. package/app/types/github.ts +153 -0
  87. package/app/types/models.ts +48 -0
  88. package/app/types/requirements.ts +38 -0
  89. package/app/types/scenarios.ts +36 -0
  90. package/app/types/tasks.ts +67 -0
  91. package/app/utils/catalog.spec.ts +82 -0
  92. package/app/utils/catalog.ts +185 -0
  93. package/app/utils/dnd.ts +29 -0
  94. package/nuxt.config.ts +43 -0
  95. 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>