@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,74 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Pipeline } from '~/types/domain'
|
|
3
|
+
import { AGENT_BY_KIND } from '~/utils/catalog'
|
|
4
|
+
import { setDndPayload } from '~/utils/dnd'
|
|
5
|
+
|
|
6
|
+
const pipelines = usePipelinesStore()
|
|
7
|
+
const ui = useUiStore()
|
|
8
|
+
|
|
9
|
+
function onDragStart(event: DragEvent, pipeline: Pipeline) {
|
|
10
|
+
setDndPayload(event, { kind: 'pipeline', pipelineId: pipeline.id })
|
|
11
|
+
;(event.target as HTMLElement).classList.add('palette-dragging')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function onDragEnd(event: DragEvent) {
|
|
15
|
+
;(event.target as HTMLElement).classList.remove('palette-dragging')
|
|
16
|
+
}
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<div class="space-y-3">
|
|
21
|
+
<div class="flex items-center justify-between px-1">
|
|
22
|
+
<p class="text-[11px] text-slate-500">Drag a pipeline onto a block to run it.</p>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<UButton
|
|
26
|
+
block
|
|
27
|
+
color="primary"
|
|
28
|
+
variant="soft"
|
|
29
|
+
icon="i-lucide-plus"
|
|
30
|
+
size="sm"
|
|
31
|
+
@click="ui.openBuilder()"
|
|
32
|
+
>
|
|
33
|
+
Build a pipeline
|
|
34
|
+
</UButton>
|
|
35
|
+
|
|
36
|
+
<div class="space-y-2">
|
|
37
|
+
<div
|
|
38
|
+
v-for="p in pipelines.pipelines"
|
|
39
|
+
:key="p.id"
|
|
40
|
+
draggable="true"
|
|
41
|
+
class="group cursor-grab select-none rounded-lg border border-slate-700 bg-slate-800/60 p-2.5 transition hover:border-indigo-500/70 hover:bg-slate-800 active:cursor-grabbing"
|
|
42
|
+
@dragstart="onDragStart($event, p)"
|
|
43
|
+
@dragend="onDragEnd"
|
|
44
|
+
>
|
|
45
|
+
<div class="mb-2 flex items-center justify-between">
|
|
46
|
+
<span class="text-xs font-semibold text-slate-100">{{ p.name }}</span>
|
|
47
|
+
<UButton
|
|
48
|
+
icon="i-lucide-trash-2"
|
|
49
|
+
color="neutral"
|
|
50
|
+
variant="ghost"
|
|
51
|
+
size="xs"
|
|
52
|
+
class="opacity-0 transition group-hover:opacity-100"
|
|
53
|
+
@click.stop="pipelines.removePipeline(p.id)"
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="flex flex-wrap items-center gap-1">
|
|
57
|
+
<template v-for="(k, i) in p.agentKinds" :key="i">
|
|
58
|
+
<UIcon
|
|
59
|
+
:name="AGENT_BY_KIND[k].icon"
|
|
60
|
+
class="h-4 w-4"
|
|
61
|
+
:style="{ color: AGENT_BY_KIND[k].color }"
|
|
62
|
+
:title="AGENT_BY_KIND[k].label"
|
|
63
|
+
/>
|
|
64
|
+
<UIcon
|
|
65
|
+
v-if="i < p.agentKinds.length - 1"
|
|
66
|
+
name="i-lucide-chevron-right"
|
|
67
|
+
class="h-3 w-3 text-slate-600"
|
|
68
|
+
/>
|
|
69
|
+
</template>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</template>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { AGENT_BY_KIND } from '~/utils/catalog'
|
|
3
|
+
|
|
4
|
+
const execution = useExecutionStore()
|
|
5
|
+
const board = useBoardStore()
|
|
6
|
+
const ui = useUiStore()
|
|
7
|
+
|
|
8
|
+
const ctx = computed(() => ui.decisionContext)
|
|
9
|
+
|
|
10
|
+
const instance = computed(() => execution.getInstance(ctx.value?.instanceId))
|
|
11
|
+
const step = computed(() =>
|
|
12
|
+
instance.value?.steps.find((s) => s.decision?.id === ctx.value?.decisionId),
|
|
13
|
+
)
|
|
14
|
+
const decision = computed(() => step.value?.decision ?? null)
|
|
15
|
+
const block = computed(() => (instance.value ? board.getBlock(instance.value.blockId) : undefined))
|
|
16
|
+
const agent = computed(() => (step.value ? AGENT_BY_KIND[step.value.agentKind] : null))
|
|
17
|
+
|
|
18
|
+
const open = computed({
|
|
19
|
+
get: () => !!ctx.value && !!decision.value,
|
|
20
|
+
set: (v: boolean) => {
|
|
21
|
+
if (!v) ui.closeDecision()
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
function choose(option: string) {
|
|
26
|
+
if (!ctx.value) return
|
|
27
|
+
execution.resolveDecision(ctx.value.instanceId, ctx.value.decisionId, option)
|
|
28
|
+
ui.closeDecision()
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<UModal v-model:open="open" title="Decision required">
|
|
34
|
+
<template #body>
|
|
35
|
+
<div v-if="decision && agent" class="space-y-4">
|
|
36
|
+
<div class="flex items-center gap-2 text-sm text-slate-400">
|
|
37
|
+
<div
|
|
38
|
+
class="flex h-8 w-8 items-center justify-center rounded-lg"
|
|
39
|
+
:style="{ backgroundColor: agent.color + '22' }"
|
|
40
|
+
>
|
|
41
|
+
<UIcon :name="agent.icon" class="h-4 w-4" :style="{ color: agent.color }" />
|
|
42
|
+
</div>
|
|
43
|
+
<div>
|
|
44
|
+
<span class="font-medium text-slate-200">{{ agent.label }}</span>
|
|
45
|
+
<span v-if="block"> on </span>
|
|
46
|
+
<span v-if="block" class="font-medium text-slate-200">{{ block.title }}</span>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<p class="text-base font-medium text-white">{{ decision.question }}</p>
|
|
51
|
+
|
|
52
|
+
<div class="grid gap-2">
|
|
53
|
+
<UButton
|
|
54
|
+
v-for="opt in decision.options"
|
|
55
|
+
:key="opt"
|
|
56
|
+
color="primary"
|
|
57
|
+
variant="soft"
|
|
58
|
+
block
|
|
59
|
+
class="justify-start"
|
|
60
|
+
@click="choose(opt)"
|
|
61
|
+
>
|
|
62
|
+
{{ opt }}
|
|
63
|
+
</UButton>
|
|
64
|
+
</div>
|
|
65
|
+
<p class="text-[11px] text-slate-500">
|
|
66
|
+
This is a visualization — any choice simply resumes the pipeline.
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
</UModal>
|
|
71
|
+
</template>
|
|
@@ -0,0 +1,296 @@
|
|
|
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 TaskContextDocs from '~/components/documents/TaskContextDocs.vue'
|
|
5
|
+
import TaskContextIssues from '~/components/tasks/TaskContextIssues.vue'
|
|
6
|
+
import FeatureScenarios from '~/components/scenarios/FeatureScenarios.vue'
|
|
7
|
+
import ContainerSummary from '~/components/panels/inspector/ContainerSummary.vue'
|
|
8
|
+
import TaskDependencies from '~/components/panels/inspector/TaskDependencies.vue'
|
|
9
|
+
import TaskStructure from '~/components/panels/inspector/TaskStructure.vue'
|
|
10
|
+
import TaskModelSettings from '~/components/panels/inspector/TaskModelSettings.vue'
|
|
11
|
+
import TaskExecution from '~/components/panels/inspector/TaskExecution.vue'
|
|
12
|
+
import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
|
|
13
|
+
import AgentStopButton from '~/components/board/AgentStopButton.vue'
|
|
14
|
+
|
|
15
|
+
const board = useBoardStore()
|
|
16
|
+
const pipelines = usePipelinesStore()
|
|
17
|
+
const execution = useExecutionStore()
|
|
18
|
+
const ui = useUiStore()
|
|
19
|
+
const documents = useDocumentsStore()
|
|
20
|
+
const tasks = useTasksStore()
|
|
21
|
+
const fragments = useFragmentsStore()
|
|
22
|
+
const models = useModelsStore()
|
|
23
|
+
const agentRuns = useAgentRunsStore()
|
|
24
|
+
const github = useGitHubStore()
|
|
25
|
+
const requirements = useRequirementsStore()
|
|
26
|
+
|
|
27
|
+
onMounted(() => {
|
|
28
|
+
fragments.ensureLoaded()
|
|
29
|
+
models.ensureLoaded()
|
|
30
|
+
github.ensureLoaded()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Pull this block's requirements review (if any) when the selection changes, so
|
|
34
|
+
// the inspector can show an open-question count and probe feature availability.
|
|
35
|
+
watch(
|
|
36
|
+
() => ui.selectedBlockId,
|
|
37
|
+
(id) => {
|
|
38
|
+
if (id) void requirements.load(id)
|
|
39
|
+
},
|
|
40
|
+
{ immediate: true },
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
/** Open the document import/spawn flow, targeting this container's frame. */
|
|
44
|
+
function spawnFromDocument() {
|
|
45
|
+
if (!block.value) return
|
|
46
|
+
const frameId = isFrame.value ? block.value.id : (board.serviceOf(block.value)?.id ?? null)
|
|
47
|
+
ui.openDocumentImport(frameId)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const block = computed<Block | undefined>(() =>
|
|
51
|
+
ui.selectedBlockId ? board.getBlock(ui.selectedBlockId) : undefined,
|
|
52
|
+
)
|
|
53
|
+
const level = computed(() => block.value?.level ?? 'frame')
|
|
54
|
+
const isFrame = computed(() => level.value === 'frame')
|
|
55
|
+
const isContainer = computed(() => level.value === 'frame' || level.value === 'module')
|
|
56
|
+
const isTask = computed(() => level.value === 'task')
|
|
57
|
+
|
|
58
|
+
const instance = computed(() => execution.getInstance(block.value?.executionId))
|
|
59
|
+
const typeMeta = computed(() => (block.value ? BLOCK_TYPE_META[block.value.type] : null))
|
|
60
|
+
|
|
61
|
+
// Containers show a derived activity status (never "done"); tasks use their own.
|
|
62
|
+
const FRAME_LABEL: Record<BlockStatus, string> = {
|
|
63
|
+
planned: 'No tasks',
|
|
64
|
+
ready: 'Live',
|
|
65
|
+
in_progress: 'Active',
|
|
66
|
+
blocked: 'Needs attention',
|
|
67
|
+
pr_ready: 'Active',
|
|
68
|
+
done: 'Live',
|
|
69
|
+
}
|
|
70
|
+
const effectiveStatus = computed<BlockStatus>(() =>
|
|
71
|
+
isContainer.value ? board.frameStatus(block.value!.id) : block.value!.status,
|
|
72
|
+
)
|
|
73
|
+
const statusMeta = computed(() => (block.value ? STATUS_META[effectiveStatus.value] : null))
|
|
74
|
+
const statusLabel = computed(() =>
|
|
75
|
+
isContainer.value ? FRAME_LABEL[effectiveStatus.value] : statusMeta.value!.label,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const runnable = computed(() => (block.value ? board.isRunnable(block.value.id) : false))
|
|
79
|
+
|
|
80
|
+
// Requirements review (questions / gaps the AI raised about this block's
|
|
81
|
+
// requirements). Hidden only when the feature is known to be off.
|
|
82
|
+
const reviewAvailable = computed(() => requirements.available !== false)
|
|
83
|
+
const blockReview = computed(() => (block.value ? requirements.reviewFor(block.value.id) : null))
|
|
84
|
+
const openReviewCount = computed(() =>
|
|
85
|
+
blockReview.value ? requirements.openCount(blockReview.value) : 0,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
// The GitHub repo backing this service (a frame), if one is linked. Linkage lives
|
|
89
|
+
// on the github_repos projection (its `blockId`), not on the block itself.
|
|
90
|
+
const serviceRepo = computed(() =>
|
|
91
|
+
isFrame.value && block.value ? github.repoForBlock(block.value.id) : undefined,
|
|
92
|
+
)
|
|
93
|
+
const serviceRepoUrl = computed(() =>
|
|
94
|
+
serviceRepo.value ? github.repoUrl(serviceRepo.value.githubId) : null,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const runMenu = computed(() =>
|
|
98
|
+
pipelines.pipelines.map((p) => ({
|
|
99
|
+
label: p.name,
|
|
100
|
+
icon: 'i-lucide-play',
|
|
101
|
+
onSelect: () => block.value && execution.start(block.value.id, p),
|
|
102
|
+
})),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
function remove() {
|
|
106
|
+
if (!block.value) return
|
|
107
|
+
execution.cancel(block.value.id)
|
|
108
|
+
board.removeBlock(block.value.id)
|
|
109
|
+
ui.select(null)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---- failed agent run (bootstrap or execution) ------------------------------
|
|
113
|
+
// A block whose current run failed surfaces the shared failure banner + retry,
|
|
114
|
+
// keyed by block id — covering a failed "bootstrap repo" frame and (for tasks) a
|
|
115
|
+
// failed pipeline execution alike.
|
|
116
|
+
const failedRun = computed(() => {
|
|
117
|
+
const run = block.value ? agentRuns.byBlock[block.value.id] : undefined
|
|
118
|
+
return run && run.status === 'failed' ? run : null
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// A running run on a container frame (a "bootstrapping…" service). Tasks surface
|
|
122
|
+
// their own Stop in TaskExecution, so this covers the bootstrap case the board was
|
|
123
|
+
// previously unable to stop. Drives the inspector's Stop control.
|
|
124
|
+
const runningRun = computed(() => {
|
|
125
|
+
if (!isContainer.value) return null
|
|
126
|
+
const run = block.value ? agentRuns.byBlock[block.value.id] : undefined
|
|
127
|
+
return run && run.status === 'running' ? run : null
|
|
128
|
+
})
|
|
129
|
+
</script>
|
|
130
|
+
|
|
131
|
+
<template>
|
|
132
|
+
<div
|
|
133
|
+
v-if="block && statusMeta && typeMeta"
|
|
134
|
+
class="absolute right-4 top-4 z-20 w-80 overflow-hidden rounded-2xl border border-slate-700 bg-slate-900/95 shadow-2xl backdrop-blur"
|
|
135
|
+
>
|
|
136
|
+
<div class="h-1.5 w-full" :style="{ backgroundColor: statusMeta.color }" />
|
|
137
|
+
<div class="space-y-4 p-4">
|
|
138
|
+
<!-- header -->
|
|
139
|
+
<div class="flex items-start justify-between gap-2">
|
|
140
|
+
<div class="flex items-center gap-2">
|
|
141
|
+
<div
|
|
142
|
+
class="flex h-9 w-9 items-center justify-center rounded-lg"
|
|
143
|
+
:style="{ backgroundColor: typeMeta.accent + '22' }"
|
|
144
|
+
>
|
|
145
|
+
<UIcon :name="typeMeta.icon" class="h-5 w-5" :style="{ color: typeMeta.accent }" />
|
|
146
|
+
</div>
|
|
147
|
+
<div>
|
|
148
|
+
<div class="text-sm font-semibold text-white">{{ block.title }}</div>
|
|
149
|
+
<div class="mt-0.5 flex items-center gap-1.5">
|
|
150
|
+
<UBadge :color="statusMeta.chip as any" variant="subtle" size="sm">
|
|
151
|
+
{{ statusLabel }}
|
|
152
|
+
</UBadge>
|
|
153
|
+
<span class="text-[10px] uppercase tracking-wide text-slate-500">{{ level }}</span>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
<UButton
|
|
158
|
+
icon="i-lucide-x"
|
|
159
|
+
color="neutral"
|
|
160
|
+
variant="ghost"
|
|
161
|
+
size="xs"
|
|
162
|
+
@click="ui.select(null)"
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<UTextarea
|
|
167
|
+
v-model="block.description"
|
|
168
|
+
:rows="2"
|
|
169
|
+
autoresize
|
|
170
|
+
size="sm"
|
|
171
|
+
class="w-full"
|
|
172
|
+
placeholder="Describe this block…"
|
|
173
|
+
/>
|
|
174
|
+
|
|
175
|
+
<!-- requirements review: surface the AI's questions / gaps -->
|
|
176
|
+
<UButton
|
|
177
|
+
v-if="reviewAvailable"
|
|
178
|
+
block
|
|
179
|
+
color="neutral"
|
|
180
|
+
variant="soft"
|
|
181
|
+
size="sm"
|
|
182
|
+
icon="i-lucide-clipboard-check"
|
|
183
|
+
@click="ui.openRequirementReview(block.id)"
|
|
184
|
+
>
|
|
185
|
+
Review requirements
|
|
186
|
+
<UBadge v-if="openReviewCount" size="xs" color="warning" variant="solid" class="ml-auto">
|
|
187
|
+
{{ openReviewCount }}
|
|
188
|
+
</UBadge>
|
|
189
|
+
</UButton>
|
|
190
|
+
|
|
191
|
+
<!-- failed run (bootstrap or execution): shared failure banner + retry -->
|
|
192
|
+
<AgentFailureCard v-if="failedRun" :run="failedRun" />
|
|
193
|
+
|
|
194
|
+
<!-- running bootstrap: let the user stop it (kills the container) -->
|
|
195
|
+
<div
|
|
196
|
+
v-else-if="runningRun"
|
|
197
|
+
class="flex items-center justify-between gap-2 rounded-lg border border-amber-900/60 bg-amber-950/30 px-3 py-2"
|
|
198
|
+
>
|
|
199
|
+
<span class="flex items-center gap-1.5 text-xs text-amber-300">
|
|
200
|
+
<UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 animate-spin" />
|
|
201
|
+
Bootstrapping…
|
|
202
|
+
</span>
|
|
203
|
+
<AgentStopButton :run-id="runningRun.runId" :kind="runningRun.kind" size="xs" />
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<!-- external links -->
|
|
207
|
+
<div class="flex flex-wrap gap-2">
|
|
208
|
+
<UButton
|
|
209
|
+
v-if="serviceRepoUrl"
|
|
210
|
+
:to="serviceRepoUrl"
|
|
211
|
+
target="_blank"
|
|
212
|
+
rel="noopener"
|
|
213
|
+
color="neutral"
|
|
214
|
+
variant="soft"
|
|
215
|
+
size="xs"
|
|
216
|
+
icon="i-lucide-github"
|
|
217
|
+
trailing-icon="i-lucide-external-link"
|
|
218
|
+
>
|
|
219
|
+
{{ serviceRepo!.owner }}/{{ serviceRepo!.name }}
|
|
220
|
+
</UButton>
|
|
221
|
+
<UButton
|
|
222
|
+
v-if="tasks.available"
|
|
223
|
+
color="neutral"
|
|
224
|
+
variant="soft"
|
|
225
|
+
size="xs"
|
|
226
|
+
icon="i-lucide-ticket"
|
|
227
|
+
@click="ui.openTaskImport()"
|
|
228
|
+
>
|
|
229
|
+
{{ tasks.anyConnected ? 'Import Jira issue' : 'Connect Jira' }}
|
|
230
|
+
</UButton>
|
|
231
|
+
<UButton
|
|
232
|
+
v-if="isContainer && documents.available && documents.anyConnected"
|
|
233
|
+
color="neutral"
|
|
234
|
+
variant="soft"
|
|
235
|
+
size="xs"
|
|
236
|
+
icon="i-lucide-wand-sparkles"
|
|
237
|
+
@click="spawnFromDocument"
|
|
238
|
+
>
|
|
239
|
+
Spawn from document
|
|
240
|
+
</UButton>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<!-- task: context documents -->
|
|
244
|
+
<TaskContextDocs v-if="isTask" :block="block" />
|
|
245
|
+
|
|
246
|
+
<!-- task: context issues (tracker) -->
|
|
247
|
+
<TaskContextIssues v-if="isTask" :block="block" />
|
|
248
|
+
|
|
249
|
+
<!-- service / module: tasks summary -->
|
|
250
|
+
<ContainerSummary v-if="isContainer" :block="block" />
|
|
251
|
+
|
|
252
|
+
<!-- task: dependencies, structure, scenarios, run settings, execution -->
|
|
253
|
+
<template v-else-if="isTask">
|
|
254
|
+
<TaskDependencies :block="block" />
|
|
255
|
+
<TaskStructure :block="block" />
|
|
256
|
+
<FeatureScenarios :block="block" />
|
|
257
|
+
<TaskModelSettings :block="block" />
|
|
258
|
+
<TaskExecution :block="block" />
|
|
259
|
+
</template>
|
|
260
|
+
|
|
261
|
+
<!-- actions -->
|
|
262
|
+
<div class="flex items-center gap-2">
|
|
263
|
+
<UDropdownMenu v-if="isTask" :items="runMenu">
|
|
264
|
+
<UButton
|
|
265
|
+
:color="runnable ? 'primary' : 'neutral'"
|
|
266
|
+
variant="soft"
|
|
267
|
+
size="sm"
|
|
268
|
+
:icon="runnable ? 'i-lucide-play' : 'i-lucide-lock'"
|
|
269
|
+
trailing-icon="i-lucide-chevron-down"
|
|
270
|
+
:disabled="!runnable"
|
|
271
|
+
>
|
|
272
|
+
{{ instance ? 'Re-run' : 'Run' }}
|
|
273
|
+
</UButton>
|
|
274
|
+
</UDropdownMenu>
|
|
275
|
+
<UButton
|
|
276
|
+
v-if="isTask"
|
|
277
|
+
color="neutral"
|
|
278
|
+
variant="soft"
|
|
279
|
+
size="sm"
|
|
280
|
+
icon="i-lucide-maximize-2"
|
|
281
|
+
@click="ui.focus(block.id)"
|
|
282
|
+
>
|
|
283
|
+
Focus
|
|
284
|
+
</UButton>
|
|
285
|
+
<UButton
|
|
286
|
+
color="error"
|
|
287
|
+
variant="ghost"
|
|
288
|
+
size="sm"
|
|
289
|
+
icon="i-lucide-trash-2"
|
|
290
|
+
class="ml-auto"
|
|
291
|
+
@click="remove"
|
|
292
|
+
/>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
</template>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Block } from '~/types/domain'
|
|
3
|
+
import { STATUS_META } from '~/utils/catalog'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{ block: Block }>()
|
|
6
|
+
|
|
7
|
+
const board = useBoardStore()
|
|
8
|
+
const ui = useUiStore()
|
|
9
|
+
|
|
10
|
+
const isFrame = computed(() => (props.block.level ?? 'frame') === 'frame')
|
|
11
|
+
const modules = computed(() => (isFrame.value ? board.modulesOf(props.block.id) : []))
|
|
12
|
+
const tasks = computed(() =>
|
|
13
|
+
isFrame.value ? board.allTasksUnder(props.block.id) : board.tasksOf(props.block.id),
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
function addTask() {
|
|
17
|
+
board.addTask(props.block.id)
|
|
18
|
+
}
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<div class="space-y-4">
|
|
23
|
+
<!-- modules (services only) -->
|
|
24
|
+
<div v-if="modules.length">
|
|
25
|
+
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
26
|
+
Modules ({{ modules.length }})
|
|
27
|
+
</div>
|
|
28
|
+
<ul class="space-y-1">
|
|
29
|
+
<li
|
|
30
|
+
v-for="m in modules"
|
|
31
|
+
:key="m.id"
|
|
32
|
+
class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1 hover:bg-slate-800/60"
|
|
33
|
+
@click="ui.select(m.id)"
|
|
34
|
+
>
|
|
35
|
+
<UIcon name="i-lucide-package" class="h-3.5 w-3.5 text-violet-400" />
|
|
36
|
+
<span class="truncate text-xs text-slate-200">{{ m.title }}</span>
|
|
37
|
+
<span class="ml-auto text-[10px] text-slate-500"
|
|
38
|
+
>{{ board.tasksOf(m.id).length }} task(s)</span
|
|
39
|
+
>
|
|
40
|
+
</li>
|
|
41
|
+
</ul>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div>
|
|
45
|
+
<div class="mb-1 flex items-center justify-between">
|
|
46
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
47
|
+
{{ isFrame ? 'All tasks' : 'Tasks' }} ({{ tasks.length }})
|
|
48
|
+
</span>
|
|
49
|
+
<UButton size="xs" variant="soft" color="primary" icon="i-lucide-plus" @click="addTask">
|
|
50
|
+
Add task
|
|
51
|
+
</UButton>
|
|
52
|
+
</div>
|
|
53
|
+
<ul v-if="tasks.length" class="space-y-1">
|
|
54
|
+
<li
|
|
55
|
+
v-for="t in tasks"
|
|
56
|
+
:key="t.id"
|
|
57
|
+
class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1 hover:bg-slate-800/60"
|
|
58
|
+
@click="ui.select(t.id)"
|
|
59
|
+
>
|
|
60
|
+
<span
|
|
61
|
+
class="h-2 w-2 shrink-0 rounded-full"
|
|
62
|
+
:style="{ backgroundColor: STATUS_META[t.status].color }"
|
|
63
|
+
/>
|
|
64
|
+
<span class="truncate text-xs text-slate-200">{{ t.title }}</span>
|
|
65
|
+
<span class="ml-auto text-[10px] text-slate-500">{{ STATUS_META[t.status].label }}</span>
|
|
66
|
+
</li>
|
|
67
|
+
</ul>
|
|
68
|
+
<div v-else class="text-[11px] text-slate-500">No tasks yet — add one to start work.</div>
|
|
69
|
+
</div>
|
|
70
|
+
<p v-if="isFrame" class="text-[11px] text-slate-500">
|
|
71
|
+
Services are long-lived — they don't "complete". Work happens in their tasks & modules.
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
</template>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Block } from '~/types/domain'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{ block: Block }>()
|
|
5
|
+
|
|
6
|
+
const board = useBoardStore()
|
|
7
|
+
const { depLabel } = useDepLabels()
|
|
8
|
+
|
|
9
|
+
const deps = computed(() =>
|
|
10
|
+
(props.block.dependsOn ?? []).map((id) => board.getBlock(id)).filter((b): b is Block => !!b),
|
|
11
|
+
)
|
|
12
|
+
const runnable = computed(() => board.isRunnable(props.block.id))
|
|
13
|
+
|
|
14
|
+
/** Label a dependency relative to this task's container. */
|
|
15
|
+
const label = (dep: Block) => depLabel(dep, props.block.parentId)
|
|
16
|
+
|
|
17
|
+
// candidate tasks to depend on: any other task not already a dependency
|
|
18
|
+
const depMenu = computed(() => {
|
|
19
|
+
const current = new Set(props.block.dependsOn)
|
|
20
|
+
return board.allTasks
|
|
21
|
+
.filter((t) => t.id !== props.block.id && !current.has(t.id))
|
|
22
|
+
.map((t) => ({
|
|
23
|
+
label: label(t),
|
|
24
|
+
icon: 'i-lucide-plus',
|
|
25
|
+
onSelect: () => board.toggleDependency(props.block.id, t.id),
|
|
26
|
+
}))
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
function removeDep(depId: string) {
|
|
30
|
+
board.removeDependency(props.block.id, depId)
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<div>
|
|
36
|
+
<div class="mb-1 flex items-center justify-between">
|
|
37
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
38
|
+
Depends on
|
|
39
|
+
</span>
|
|
40
|
+
<UDropdownMenu v-if="depMenu.length" :items="depMenu">
|
|
41
|
+
<UButton
|
|
42
|
+
size="xs"
|
|
43
|
+
variant="ghost"
|
|
44
|
+
color="neutral"
|
|
45
|
+
icon="i-lucide-plus"
|
|
46
|
+
trailing-icon="i-lucide-chevron-down"
|
|
47
|
+
/>
|
|
48
|
+
</UDropdownMenu>
|
|
49
|
+
</div>
|
|
50
|
+
<div v-if="deps.length" class="flex flex-wrap gap-1">
|
|
51
|
+
<UBadge
|
|
52
|
+
v-for="d in deps"
|
|
53
|
+
:key="d.id"
|
|
54
|
+
:color="d.status === 'done' ? 'neutral' : 'warning'"
|
|
55
|
+
variant="subtle"
|
|
56
|
+
size="sm"
|
|
57
|
+
class="cursor-pointer"
|
|
58
|
+
:title="d.status === 'done' ? 'Merged' : 'Not merged yet'"
|
|
59
|
+
@click="removeDep(d.id)"
|
|
60
|
+
>
|
|
61
|
+
{{ label(d) }}
|
|
62
|
+
<UIcon name="i-lucide-x" class="ml-0.5 h-3 w-3" />
|
|
63
|
+
</UBadge>
|
|
64
|
+
</div>
|
|
65
|
+
<div v-else class="text-[11px] text-slate-500">No dependencies — can run any time.</div>
|
|
66
|
+
<div v-if="!runnable" class="mt-1 text-[10px] text-amber-400">
|
|
67
|
+
Blocked until dependencies merge.
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</template>
|