@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,161 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { DocumentSourceKind } from '~/types/domain'
|
|
3
|
+
|
|
4
|
+
// Import pages from a connected document source and pick one to expand into
|
|
5
|
+
// board structure. A source selector lets the user choose which connected source
|
|
6
|
+
// to import from (Confluence, Notion, …). Carries an optional target frame from
|
|
7
|
+
// the inspector so "Preview & spawn" lands the structure inside that frame.
|
|
8
|
+
const ui = useUiStore()
|
|
9
|
+
const documents = useDocumentsStore()
|
|
10
|
+
const board = useBoardStore()
|
|
11
|
+
const toast = useToast()
|
|
12
|
+
|
|
13
|
+
const open = computed({
|
|
14
|
+
get: () => ui.documentImport !== null,
|
|
15
|
+
set: (v: boolean) => {
|
|
16
|
+
if (!v) ui.closeDocumentImport()
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const targetFrameId = computed(() => ui.documentImport?.targetFrameId ?? null)
|
|
21
|
+
const targetFrameTitle = computed(() =>
|
|
22
|
+
targetFrameId.value ? board.getBlock(targetFrameId.value)?.title : null,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
/** Which connected source we're importing from (defaults to the first). */
|
|
26
|
+
const source = ref<DocumentSourceKind | undefined>(undefined)
|
|
27
|
+
const ref_ = ref('')
|
|
28
|
+
const importing = ref(false)
|
|
29
|
+
|
|
30
|
+
const sourceItems = computed(() =>
|
|
31
|
+
documents.connectedSources.map((s) => ({ label: s.label, value: s.source })),
|
|
32
|
+
)
|
|
33
|
+
const descriptor = computed(() =>
|
|
34
|
+
source.value ? documents.descriptorFor(source.value) : undefined,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
/** Documents imported from the currently selected source. */
|
|
38
|
+
const sourceDocs = computed(() =>
|
|
39
|
+
source.value ? documents.documents.filter((d) => d.source === source.value) : [],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
watch(open, (isOpen) => {
|
|
43
|
+
if (isOpen) {
|
|
44
|
+
ref_.value = ''
|
|
45
|
+
source.value = ui.documentImport?.source ?? documents.connectedSources[0]?.source ?? undefined
|
|
46
|
+
documents.loadDocuments().catch(() => {})
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
async function doImport() {
|
|
51
|
+
const value = ref_.value.trim()
|
|
52
|
+
if (!value || !source.value) return
|
|
53
|
+
importing.value = true
|
|
54
|
+
try {
|
|
55
|
+
const doc = await documents.importDocument(source.value, value)
|
|
56
|
+
ref_.value = ''
|
|
57
|
+
toast.add({ title: `Imported "${doc.title}"`, icon: 'i-lucide-file-down' })
|
|
58
|
+
} catch (e) {
|
|
59
|
+
toast.add({
|
|
60
|
+
title: 'Import failed',
|
|
61
|
+
description: e instanceof Error ? e.message : String(e),
|
|
62
|
+
icon: 'i-lucide-triangle-alert',
|
|
63
|
+
color: 'error',
|
|
64
|
+
})
|
|
65
|
+
} finally {
|
|
66
|
+
importing.value = false
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function preview(externalId: string) {
|
|
71
|
+
if (source.value) ui.openSpawnPreview(source.value, externalId, targetFrameId.value)
|
|
72
|
+
}
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<UModal v-model:open="open" title="Import from a document source">
|
|
77
|
+
<template #body>
|
|
78
|
+
<div v-if="!documents.anyConnected" class="space-y-3 text-center">
|
|
79
|
+
<UIcon name="i-lucide-plug" class="mx-auto h-8 w-8 text-slate-500" />
|
|
80
|
+
<p class="text-sm text-slate-400">Connect a document source first.</p>
|
|
81
|
+
<div class="flex justify-center gap-2">
|
|
82
|
+
<UButton
|
|
83
|
+
v-for="s in documents.sources"
|
|
84
|
+
:key="s.source"
|
|
85
|
+
color="primary"
|
|
86
|
+
variant="soft"
|
|
87
|
+
:icon="s.icon"
|
|
88
|
+
@click="ui.openDocumentConnect(s.source)"
|
|
89
|
+
>
|
|
90
|
+
Connect {{ s.label }}
|
|
91
|
+
</UButton>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div v-else class="space-y-4">
|
|
96
|
+
<p v-if="targetFrameTitle" class="text-xs text-slate-400">
|
|
97
|
+
Spawning into <span class="font-medium text-slate-200">{{ targetFrameTitle }}</span>
|
|
98
|
+
</p>
|
|
99
|
+
|
|
100
|
+
<UFormField v-if="sourceItems.length > 1" label="Source">
|
|
101
|
+
<USelect v-model="source" :items="sourceItems" class="w-full" />
|
|
102
|
+
</UFormField>
|
|
103
|
+
|
|
104
|
+
<div class="flex items-end gap-2">
|
|
105
|
+
<UFormField :label="descriptor?.refLabel ?? 'Page URL or ID'" class="flex-1">
|
|
106
|
+
<UInput
|
|
107
|
+
v-model="ref_"
|
|
108
|
+
:placeholder="descriptor?.refPlaceholder"
|
|
109
|
+
class="w-full"
|
|
110
|
+
@keydown.enter="doImport"
|
|
111
|
+
/>
|
|
112
|
+
</UFormField>
|
|
113
|
+
<UButton
|
|
114
|
+
color="primary"
|
|
115
|
+
icon="i-lucide-file-down"
|
|
116
|
+
:loading="importing"
|
|
117
|
+
:disabled="!ref_.trim()"
|
|
118
|
+
@click="doImport"
|
|
119
|
+
>
|
|
120
|
+
Import
|
|
121
|
+
</UButton>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div v-if="sourceDocs.length" class="space-y-2">
|
|
125
|
+
<h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
126
|
+
Imported documents
|
|
127
|
+
</h3>
|
|
128
|
+
<div
|
|
129
|
+
v-for="doc in sourceDocs"
|
|
130
|
+
:key="`${doc.source}:${doc.externalId}`"
|
|
131
|
+
class="rounded-lg border border-slate-800 bg-slate-900/60 p-3"
|
|
132
|
+
>
|
|
133
|
+
<div class="flex items-start justify-between gap-2">
|
|
134
|
+
<div class="min-w-0">
|
|
135
|
+
<a
|
|
136
|
+
:href="doc.url"
|
|
137
|
+
target="_blank"
|
|
138
|
+
rel="noopener"
|
|
139
|
+
class="truncate text-sm font-medium text-white hover:underline"
|
|
140
|
+
>
|
|
141
|
+
{{ doc.title }}
|
|
142
|
+
</a>
|
|
143
|
+
<p class="mt-0.5 line-clamp-2 text-xs text-slate-500">{{ doc.excerpt }}</p>
|
|
144
|
+
</div>
|
|
145
|
+
<UButton
|
|
146
|
+
color="primary"
|
|
147
|
+
variant="soft"
|
|
148
|
+
size="xs"
|
|
149
|
+
icon="i-lucide-wand-sparkles"
|
|
150
|
+
@click="preview(doc.externalId)"
|
|
151
|
+
>
|
|
152
|
+
Preview & spawn
|
|
153
|
+
</UButton>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
<p v-else class="text-center text-xs text-slate-500">No documents imported yet.</p>
|
|
158
|
+
</div>
|
|
159
|
+
</template>
|
|
160
|
+
</UModal>
|
|
161
|
+
</template>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Connect (or disconnect) the workspace to a document source. The form is
|
|
3
|
+
// rendered generically from the source's descriptor (credential fields), so the
|
|
4
|
+
// same modal serves Confluence, Notion and any future source. Secret credentials
|
|
5
|
+
// are write-only — the backend never returns them, so on reload we show
|
|
6
|
+
// "Connected" with empty fields.
|
|
7
|
+
const ui = useUiStore()
|
|
8
|
+
const documents = useDocumentsStore()
|
|
9
|
+
const toast = useToast()
|
|
10
|
+
|
|
11
|
+
const source = computed(() => ui.documentConnect?.source ?? null)
|
|
12
|
+
const descriptor = computed(() =>
|
|
13
|
+
source.value ? documents.descriptorFor(source.value) : undefined,
|
|
14
|
+
)
|
|
15
|
+
const connection = computed(() =>
|
|
16
|
+
source.value ? documents.connectionFor(source.value) : undefined,
|
|
17
|
+
)
|
|
18
|
+
const connected = computed(() => connection.value !== undefined)
|
|
19
|
+
|
|
20
|
+
const open = computed({
|
|
21
|
+
get: () => ui.documentConnect !== null,
|
|
22
|
+
set: (v: boolean) => {
|
|
23
|
+
if (!v) ui.closeDocumentConnect()
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
/** One value per credential field, reset whenever the modal (re)opens. */
|
|
28
|
+
const values = ref<Record<string, string>>({})
|
|
29
|
+
const saving = ref(false)
|
|
30
|
+
|
|
31
|
+
watch(open, (isOpen) => {
|
|
32
|
+
if (isOpen) values.value = {}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const canSubmit = computed(() => {
|
|
36
|
+
const fields = descriptor.value?.credentialFields ?? []
|
|
37
|
+
return fields.length > 0 && fields.every((f) => (values.value[f.key] ?? '').trim())
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
async function submit() {
|
|
41
|
+
if (!canSubmit.value || !source.value) return
|
|
42
|
+
const credentials: Record<string, string> = {}
|
|
43
|
+
for (const f of descriptor.value!.credentialFields) {
|
|
44
|
+
credentials[f.key] = values.value[f.key]!.trim()
|
|
45
|
+
}
|
|
46
|
+
saving.value = true
|
|
47
|
+
try {
|
|
48
|
+
await documents.connect(source.value, credentials)
|
|
49
|
+
toast.add({
|
|
50
|
+
title: `${descriptor.value!.label} connected`,
|
|
51
|
+
icon: 'i-lucide-check',
|
|
52
|
+
color: 'success',
|
|
53
|
+
})
|
|
54
|
+
ui.closeDocumentConnect()
|
|
55
|
+
} catch (e) {
|
|
56
|
+
toast.add({
|
|
57
|
+
title: 'Could not connect',
|
|
58
|
+
description: e instanceof Error ? e.message : String(e),
|
|
59
|
+
icon: 'i-lucide-triangle-alert',
|
|
60
|
+
color: 'error',
|
|
61
|
+
})
|
|
62
|
+
} finally {
|
|
63
|
+
saving.value = false
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function disconnect() {
|
|
68
|
+
if (!source.value) return
|
|
69
|
+
await documents.disconnect(source.value)
|
|
70
|
+
toast.add({
|
|
71
|
+
title: `${descriptor.value?.label ?? 'Source'} disconnected`,
|
|
72
|
+
icon: 'i-lucide-unplug',
|
|
73
|
+
})
|
|
74
|
+
ui.closeDocumentConnect()
|
|
75
|
+
}
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<template>
|
|
79
|
+
<UModal v-model:open="open" :title="descriptor?.label ?? 'Connect source'">
|
|
80
|
+
<template #body>
|
|
81
|
+
<div v-if="descriptor" class="space-y-4">
|
|
82
|
+
<p class="text-sm text-slate-400">
|
|
83
|
+
Connect {{ descriptor.label }} to import requirements, RFCs and PRDs, then spawn board
|
|
84
|
+
structure or attach them to tasks as agent context.
|
|
85
|
+
</p>
|
|
86
|
+
|
|
87
|
+
<div class="space-y-3">
|
|
88
|
+
<UFormField
|
|
89
|
+
v-for="field in descriptor.credentialFields"
|
|
90
|
+
:key="field.key"
|
|
91
|
+
:label="field.label"
|
|
92
|
+
:help="field.help"
|
|
93
|
+
>
|
|
94
|
+
<UInput
|
|
95
|
+
v-model="values[field.key]"
|
|
96
|
+
:type="field.secret ? 'password' : 'text'"
|
|
97
|
+
:placeholder="field.placeholder"
|
|
98
|
+
class="w-full"
|
|
99
|
+
/>
|
|
100
|
+
</UFormField>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="flex items-center justify-between gap-2 pt-1">
|
|
104
|
+
<UButton
|
|
105
|
+
v-if="connected"
|
|
106
|
+
color="error"
|
|
107
|
+
variant="ghost"
|
|
108
|
+
icon="i-lucide-unplug"
|
|
109
|
+
@click="disconnect"
|
|
110
|
+
>
|
|
111
|
+
Disconnect
|
|
112
|
+
</UButton>
|
|
113
|
+
<div v-else />
|
|
114
|
+
<UButton
|
|
115
|
+
color="primary"
|
|
116
|
+
icon="i-lucide-plug"
|
|
117
|
+
:loading="saving"
|
|
118
|
+
:disabled="!canSubmit"
|
|
119
|
+
@click="submit"
|
|
120
|
+
>
|
|
121
|
+
{{ connected ? 'Update connection' : 'Connect' }}
|
|
122
|
+
</UButton>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</template>
|
|
126
|
+
</UModal>
|
|
127
|
+
</template>
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { DocumentBoardPlan } from '~/types/domain'
|
|
3
|
+
|
|
4
|
+
// Preview the structure an imported document expands into, then spawn it. The
|
|
5
|
+
// plan is fetched fresh on open; a badge makes clear whether an LLM or the
|
|
6
|
+
// deterministic heading parser produced it.
|
|
7
|
+
const ui = useUiStore()
|
|
8
|
+
const documents = useDocumentsStore()
|
|
9
|
+
const board = useBoardStore()
|
|
10
|
+
const toast = useToast()
|
|
11
|
+
|
|
12
|
+
const open = computed({
|
|
13
|
+
get: () => ui.spawnPreview !== null,
|
|
14
|
+
set: (v: boolean) => {
|
|
15
|
+
if (!v) ui.closeSpawnPreview()
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const targetFrameId = computed(() => ui.spawnPreview?.targetFrameId ?? null)
|
|
20
|
+
const targetFrameTitle = computed(() =>
|
|
21
|
+
targetFrameId.value ? board.getBlock(targetFrameId.value)?.title : null,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
const plan = ref<DocumentBoardPlan | null>(null)
|
|
25
|
+
const loadingPlan = ref(false)
|
|
26
|
+
const spawning = ref(false)
|
|
27
|
+
|
|
28
|
+
watch(
|
|
29
|
+
() => ui.spawnPreview?.externalId,
|
|
30
|
+
async (externalId) => {
|
|
31
|
+
plan.value = null
|
|
32
|
+
const preview = ui.spawnPreview
|
|
33
|
+
if (!externalId || !preview) return
|
|
34
|
+
loadingPlan.value = true
|
|
35
|
+
try {
|
|
36
|
+
plan.value = await documents.plan(preview.source, externalId)
|
|
37
|
+
} catch (e) {
|
|
38
|
+
toast.add({
|
|
39
|
+
title: 'Could not build a plan',
|
|
40
|
+
description: e instanceof Error ? e.message : String(e),
|
|
41
|
+
icon: 'i-lucide-triangle-alert',
|
|
42
|
+
color: 'error',
|
|
43
|
+
})
|
|
44
|
+
} finally {
|
|
45
|
+
loadingPlan.value = false
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{ immediate: true },
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
async function spawn() {
|
|
52
|
+
const preview = ui.spawnPreview
|
|
53
|
+
if (!preview) return
|
|
54
|
+
spawning.value = true
|
|
55
|
+
try {
|
|
56
|
+
const result = await documents.spawn(
|
|
57
|
+
preview.source,
|
|
58
|
+
preview.externalId,
|
|
59
|
+
targetFrameId.value ?? undefined,
|
|
60
|
+
)
|
|
61
|
+
toast.add({
|
|
62
|
+
title: 'Structure spawned',
|
|
63
|
+
description: `${result.frames} frames · ${result.modules} modules · ${result.tasks} tasks`,
|
|
64
|
+
icon: 'i-lucide-check',
|
|
65
|
+
color: 'success',
|
|
66
|
+
})
|
|
67
|
+
ui.closeSpawnPreview()
|
|
68
|
+
ui.closeDocumentImport()
|
|
69
|
+
} catch (e) {
|
|
70
|
+
toast.add({
|
|
71
|
+
title: 'Spawn failed',
|
|
72
|
+
description: e instanceof Error ? e.message : String(e),
|
|
73
|
+
icon: 'i-lucide-triangle-alert',
|
|
74
|
+
color: 'error',
|
|
75
|
+
})
|
|
76
|
+
} finally {
|
|
77
|
+
spawning.value = false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<template>
|
|
83
|
+
<UModal v-model:open="open" title="Preview structure">
|
|
84
|
+
<template #body>
|
|
85
|
+
<div class="space-y-4">
|
|
86
|
+
<div v-if="plan" class="flex items-center justify-between gap-2">
|
|
87
|
+
<UBadge
|
|
88
|
+
:color="plan.planner === 'llm' ? 'primary' : 'neutral'"
|
|
89
|
+
variant="subtle"
|
|
90
|
+
size="sm"
|
|
91
|
+
>
|
|
92
|
+
{{ plan.planner === 'llm' ? 'AI-generated' : 'From headings' }}
|
|
93
|
+
</UBadge>
|
|
94
|
+
<span v-if="targetFrameTitle" class="text-xs text-slate-400">
|
|
95
|
+
into <span class="font-medium text-slate-200">{{ targetFrameTitle }}</span>
|
|
96
|
+
</span>
|
|
97
|
+
<span v-else class="text-xs text-slate-400">as new top-level frames</span>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div v-if="loadingPlan" class="flex items-center gap-2 text-sm text-slate-400">
|
|
101
|
+
<UIcon name="i-lucide-loader" class="h-4 w-4 animate-spin" /> Building plan…
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div v-else-if="plan" class="max-h-80 space-y-3 overflow-y-auto pr-1">
|
|
105
|
+
<div
|
|
106
|
+
v-for="(frame, fi) in plan.frames"
|
|
107
|
+
:key="fi"
|
|
108
|
+
class="rounded-lg border border-slate-800 bg-slate-900/60 p-3"
|
|
109
|
+
>
|
|
110
|
+
<div class="flex items-center gap-2">
|
|
111
|
+
<UIcon name="i-lucide-box" class="h-4 w-4 text-indigo-400" />
|
|
112
|
+
<span class="text-sm font-semibold text-white">{{ frame.title }}</span>
|
|
113
|
+
<UBadge variant="subtle" size="sm" color="neutral">{{ frame.type }}</UBadge>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<ul v-if="frame.tasks.length" class="mt-2 space-y-1 pl-6">
|
|
117
|
+
<li
|
|
118
|
+
v-for="(task, ti) in frame.tasks"
|
|
119
|
+
:key="`t-${ti}`"
|
|
120
|
+
class="flex items-center gap-1.5 text-xs text-slate-300"
|
|
121
|
+
>
|
|
122
|
+
<UIcon name="i-lucide-square-check-big" class="h-3 w-3 text-slate-500" />
|
|
123
|
+
{{ task.title }}
|
|
124
|
+
</li>
|
|
125
|
+
</ul>
|
|
126
|
+
|
|
127
|
+
<div v-for="(mod, mi) in frame.modules" :key="`m-${mi}`" class="mt-2 pl-4">
|
|
128
|
+
<div class="flex items-center gap-1.5 text-xs font-medium text-slate-200">
|
|
129
|
+
<UIcon name="i-lucide-folder" class="h-3.5 w-3.5 text-amber-400" />
|
|
130
|
+
{{ mod.name }}
|
|
131
|
+
</div>
|
|
132
|
+
<ul class="mt-1 space-y-1 pl-5">
|
|
133
|
+
<li
|
|
134
|
+
v-for="(task, ti) in mod.tasks"
|
|
135
|
+
:key="`mt-${ti}`"
|
|
136
|
+
class="flex items-center gap-1.5 text-xs text-slate-300"
|
|
137
|
+
>
|
|
138
|
+
<UIcon name="i-lucide-square-check-big" class="h-3 w-3 text-slate-500" />
|
|
139
|
+
{{ task.title }}
|
|
140
|
+
</li>
|
|
141
|
+
</ul>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div class="flex justify-end gap-2 pt-1">
|
|
147
|
+
<UButton color="neutral" variant="ghost" @click="ui.closeSpawnPreview()">Cancel</UButton>
|
|
148
|
+
<UButton
|
|
149
|
+
color="primary"
|
|
150
|
+
icon="i-lucide-wand-sparkles"
|
|
151
|
+
:loading="spawning"
|
|
152
|
+
:disabled="!plan || loadingPlan"
|
|
153
|
+
@click="spawn"
|
|
154
|
+
>
|
|
155
|
+
Spawn onto board
|
|
156
|
+
</UButton>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</template>
|
|
160
|
+
</UModal>
|
|
161
|
+
</template>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
3
|
+
import type { Block, DocumentSourceKind } from '~/types/domain'
|
|
4
|
+
|
|
5
|
+
// Documents (from any source) attached to a task as agent context, shown inside
|
|
6
|
+
// the InspectorPanel. Linked docs are fed to agents during execution (see the
|
|
7
|
+
// backend's userPromptFor). Rendered only when the integration is available.
|
|
8
|
+
const props = defineProps<{ block: Block }>()
|
|
9
|
+
|
|
10
|
+
const documents = useDocumentsStore()
|
|
11
|
+
const ui = useUiStore()
|
|
12
|
+
const toast = useToast()
|
|
13
|
+
|
|
14
|
+
onMounted(() => {
|
|
15
|
+
documents.loadDocuments().catch(() => {})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const linked = computed(() => documents.docsForBlock(props.block.id))
|
|
19
|
+
|
|
20
|
+
async function attach(source: DocumentSourceKind, externalId: string) {
|
|
21
|
+
try {
|
|
22
|
+
await documents.linkToBlock(props.block.id, source, externalId)
|
|
23
|
+
toast.add({ title: 'Document attached', icon: 'i-lucide-link' })
|
|
24
|
+
} catch (e) {
|
|
25
|
+
toast.add({
|
|
26
|
+
title: 'Could not attach',
|
|
27
|
+
description: e instanceof Error ? e.message : String(e),
|
|
28
|
+
icon: 'i-lucide-triangle-alert',
|
|
29
|
+
color: 'error',
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const attachMenu = computed<DropdownMenuItem[][]>(() => {
|
|
35
|
+
const linkedKeys = new Set(linked.value.map((d) => `${d.source}:${d.externalId}`))
|
|
36
|
+
const items: DropdownMenuItem[] = documents.documents
|
|
37
|
+
.filter((d) => !linkedKeys.has(`${d.source}:${d.externalId}`))
|
|
38
|
+
.map((d) => ({
|
|
39
|
+
label: d.title,
|
|
40
|
+
icon: documents.descriptorFor(d.source)?.icon ?? 'i-lucide-file-text',
|
|
41
|
+
onSelect: () => attach(d.source, d.externalId),
|
|
42
|
+
}))
|
|
43
|
+
items.push({
|
|
44
|
+
label: 'Import a page…',
|
|
45
|
+
icon: 'i-lucide-file-down',
|
|
46
|
+
onSelect: () => ui.openDocumentImport(null),
|
|
47
|
+
})
|
|
48
|
+
return [items]
|
|
49
|
+
})
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<div v-if="documents.available" class="space-y-2">
|
|
54
|
+
<div class="flex items-center justify-between">
|
|
55
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
56
|
+
Context documents
|
|
57
|
+
</span>
|
|
58
|
+
<UDropdownMenu :items="attachMenu" :content="{ side: 'bottom', align: 'end' }">
|
|
59
|
+
<UButton color="neutral" variant="soft" size="xs" icon="i-lucide-plus">Attach</UButton>
|
|
60
|
+
</UDropdownMenu>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div v-if="linked.length" class="space-y-1">
|
|
64
|
+
<a
|
|
65
|
+
v-for="doc in linked"
|
|
66
|
+
:key="`${doc.source}:${doc.externalId}`"
|
|
67
|
+
:href="doc.url"
|
|
68
|
+
target="_blank"
|
|
69
|
+
rel="noopener"
|
|
70
|
+
class="flex items-center gap-1.5 rounded-md border border-slate-800 bg-slate-900/60 px-2 py-1.5 text-xs text-slate-300 hover:bg-slate-800/60"
|
|
71
|
+
>
|
|
72
|
+
<UIcon
|
|
73
|
+
:name="documents.descriptorFor(doc.source)?.icon ?? 'i-lucide-file-text'"
|
|
74
|
+
class="h-3.5 w-3.5 shrink-0 text-indigo-400"
|
|
75
|
+
/>
|
|
76
|
+
<span class="truncate">{{ doc.title }}</span>
|
|
77
|
+
</a>
|
|
78
|
+
</div>
|
|
79
|
+
<p v-else class="text-[11px] text-slate-500">
|
|
80
|
+
Attach a requirement, RFC or PRD so agents see it while implementing this task.
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
</template>
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onKeyStroke } from '@vueuse/core'
|
|
3
|
+
import type { Block } from '~/types/domain'
|
|
4
|
+
import { BLOCK_TYPE_META, STATUS_META } from '~/utils/catalog'
|
|
5
|
+
import PipelineProgress from '~/components/pipeline/PipelineProgress.vue'
|
|
6
|
+
|
|
7
|
+
const board = useBoardStore()
|
|
8
|
+
const pipelines = usePipelinesStore()
|
|
9
|
+
const execution = useExecutionStore()
|
|
10
|
+
const ui = useUiStore()
|
|
11
|
+
const models = useModelsStore()
|
|
12
|
+
|
|
13
|
+
onMounted(() => models.ensureLoaded())
|
|
14
|
+
|
|
15
|
+
const block = computed<Block | undefined>(() =>
|
|
16
|
+
ui.focusBlockId ? board.getBlock(ui.focusBlockId) : undefined,
|
|
17
|
+
)
|
|
18
|
+
const instance = computed(() => execution.getInstance(block.value?.executionId))
|
|
19
|
+
const statusMeta = computed(() => (block.value ? STATUS_META[block.value.status] : null))
|
|
20
|
+
const typeMeta = computed(() => (block.value ? BLOCK_TYPE_META[block.value.type] : null))
|
|
21
|
+
|
|
22
|
+
const deps = computed(() =>
|
|
23
|
+
(block.value?.dependsOn ?? []).map((id) => board.getBlock(id)).filter((b): b is Block => !!b),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const runMenu = computed(() =>
|
|
27
|
+
pipelines.pipelines.map((p) => ({
|
|
28
|
+
label: p.name,
|
|
29
|
+
icon: 'i-lucide-play',
|
|
30
|
+
onSelect: () => block.value && execution.start(block.value.id, p),
|
|
31
|
+
})),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
function close() {
|
|
35
|
+
ui.focus(null)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onKeyStroke('Escape', () => {
|
|
39
|
+
if (ui.focusBlockId) close()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
function openDecisionFor(decisionId: string) {
|
|
43
|
+
if (instance.value) ui.openDecision(instance.value.id, decisionId)
|
|
44
|
+
}
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<Transition name="focus-fade">
|
|
49
|
+
<div
|
|
50
|
+
v-if="block && statusMeta && typeMeta"
|
|
51
|
+
class="absolute inset-0 z-30 flex flex-col bg-slate-950/95 backdrop-blur"
|
|
52
|
+
>
|
|
53
|
+
<!-- header / breadcrumb -->
|
|
54
|
+
<header class="flex items-center gap-3 border-b border-slate-800 px-6 py-4">
|
|
55
|
+
<UButton
|
|
56
|
+
icon="i-lucide-arrow-left"
|
|
57
|
+
color="neutral"
|
|
58
|
+
variant="ghost"
|
|
59
|
+
size="sm"
|
|
60
|
+
@click="close"
|
|
61
|
+
>
|
|
62
|
+
Board
|
|
63
|
+
</UButton>
|
|
64
|
+
<UIcon name="i-lucide-chevron-right" class="h-4 w-4 text-slate-600" />
|
|
65
|
+
<div
|
|
66
|
+
class="flex h-9 w-9 items-center justify-center rounded-lg"
|
|
67
|
+
:style="{ backgroundColor: typeMeta.accent + '22' }"
|
|
68
|
+
>
|
|
69
|
+
<UIcon :name="typeMeta.icon" class="h-5 w-5" :style="{ color: typeMeta.accent }" />
|
|
70
|
+
</div>
|
|
71
|
+
<div>
|
|
72
|
+
<h1 class="text-lg font-semibold text-white">{{ block.title }}</h1>
|
|
73
|
+
<div class="text-xs text-slate-500">{{ typeMeta.label }} · focus view</div>
|
|
74
|
+
</div>
|
|
75
|
+
<UBadge :color="statusMeta.chip as any" variant="subtle" class="ml-2">
|
|
76
|
+
{{ statusMeta.label }}
|
|
77
|
+
</UBadge>
|
|
78
|
+
<div class="ml-auto flex items-center gap-2">
|
|
79
|
+
<UDropdownMenu :items="runMenu">
|
|
80
|
+
<UButton
|
|
81
|
+
color="primary"
|
|
82
|
+
variant="soft"
|
|
83
|
+
size="sm"
|
|
84
|
+
icon="i-lucide-play"
|
|
85
|
+
trailing-icon="i-lucide-chevron-down"
|
|
86
|
+
>
|
|
87
|
+
{{ instance ? 'Re-run pipeline' : 'Run pipeline' }}
|
|
88
|
+
</UButton>
|
|
89
|
+
</UDropdownMenu>
|
|
90
|
+
<UButton icon="i-lucide-x" color="neutral" variant="ghost" @click="close" />
|
|
91
|
+
</div>
|
|
92
|
+
</header>
|
|
93
|
+
|
|
94
|
+
<div class="grid flex-1 grid-cols-[1fr_300px] gap-6 overflow-hidden p-6">
|
|
95
|
+
<!-- main: pipeline flow -->
|
|
96
|
+
<section
|
|
97
|
+
class="flex flex-col overflow-auto rounded-2xl border border-slate-800 bg-slate-900/60 p-6"
|
|
98
|
+
>
|
|
99
|
+
<div class="mb-4 flex items-center gap-2">
|
|
100
|
+
<UIcon name="i-lucide-workflow" class="h-4 w-4 text-slate-500" />
|
|
101
|
+
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-400">
|
|
102
|
+
{{ instance ? instance.pipelineName : 'No pipeline running' }}
|
|
103
|
+
</h2>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<PipelineProgress v-if="instance" :instance="instance" @open-decision="openDecisionFor" />
|
|
107
|
+
|
|
108
|
+
<div
|
|
109
|
+
v-else
|
|
110
|
+
class="flex flex-1 items-center justify-center rounded-xl border border-dashed border-slate-700 text-sm text-slate-500"
|
|
111
|
+
>
|
|
112
|
+
Run a pipeline to visualize the agents working on this block.
|
|
113
|
+
</div>
|
|
114
|
+
</section>
|
|
115
|
+
|
|
116
|
+
<!-- side: details -->
|
|
117
|
+
<aside
|
|
118
|
+
class="space-y-4 overflow-auto rounded-2xl border border-slate-800 bg-slate-900/60 p-5"
|
|
119
|
+
>
|
|
120
|
+
<div>
|
|
121
|
+
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
122
|
+
Description
|
|
123
|
+
</div>
|
|
124
|
+
<p class="text-sm text-slate-300">{{ block.description }}</p>
|
|
125
|
+
</div>
|
|
126
|
+
<div v-if="instance">
|
|
127
|
+
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
128
|
+
Overall progress
|
|
129
|
+
</div>
|
|
130
|
+
<UProgress :model-value="Math.round(block.progress * 100)" />
|
|
131
|
+
<div class="mt-1 text-[11px] text-slate-400">
|
|
132
|
+
{{ Math.round(block.progress * 100) }}%
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
<div>
|
|
136
|
+
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
137
|
+
Dependencies
|
|
138
|
+
</div>
|
|
139
|
+
<div v-if="deps.length" class="flex flex-wrap gap-1">
|
|
140
|
+
<UBadge v-for="d in deps" :key="d.id" color="neutral" variant="subtle" size="sm">
|
|
141
|
+
{{ d.title }}
|
|
142
|
+
</UBadge>
|
|
143
|
+
</div>
|
|
144
|
+
<div v-else class="text-[11px] text-slate-500">None</div>
|
|
145
|
+
</div>
|
|
146
|
+
</aside>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</Transition>
|
|
150
|
+
</template>
|
|
151
|
+
|
|
152
|
+
<style scoped>
|
|
153
|
+
.focus-fade-enter-active,
|
|
154
|
+
.focus-fade-leave-active {
|
|
155
|
+
transition: opacity 0.18s ease;
|
|
156
|
+
}
|
|
157
|
+
.focus-fade-enter-from,
|
|
158
|
+
.focus-fade-leave-to {
|
|
159
|
+
opacity: 0;
|
|
160
|
+
}
|
|
161
|
+
</style>
|