@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,202 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
3
|
+
|
|
4
|
+
// Account + board switching. Picks the active account (personal / org) and the
|
|
5
|
+
// active board within it, and manages boards (new / rename / delete). The account
|
|
6
|
+
// row is shown only when accounts exist (auth on); in dev it falls back to a plain
|
|
7
|
+
// board switcher over the single unscoped context.
|
|
8
|
+
const accounts = useAccountsStore()
|
|
9
|
+
const workspace = useWorkspaceStore()
|
|
10
|
+
const toast = useToast()
|
|
11
|
+
|
|
12
|
+
const busy = ref(false)
|
|
13
|
+
|
|
14
|
+
function notifyError(title: string, e: unknown) {
|
|
15
|
+
toast.add({
|
|
16
|
+
title,
|
|
17
|
+
description: e instanceof Error ? e.message : String(e),
|
|
18
|
+
icon: 'i-lucide-triangle-alert',
|
|
19
|
+
color: 'error',
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ---- account + board menus -------------------------------------------------
|
|
24
|
+
const accountItems = computed<DropdownMenuItem[][]>(() => [
|
|
25
|
+
accounts.accounts.map((a) => ({
|
|
26
|
+
label: a.name,
|
|
27
|
+
icon: a.type === 'org' ? 'i-lucide-users' : 'i-lucide-user',
|
|
28
|
+
trailingIcon: a.id === accounts.activeAccountId ? 'i-lucide-check' : undefined,
|
|
29
|
+
onSelect: () => void selectAccount(a.id),
|
|
30
|
+
})),
|
|
31
|
+
[{ label: 'New organization…', icon: 'i-lucide-plus', onSelect: () => openPrompt('account') }],
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
const boardItems = computed<DropdownMenuItem[][]>(() => [
|
|
35
|
+
workspace.accountWorkspaces.map((w) => ({
|
|
36
|
+
label: w.name,
|
|
37
|
+
icon: 'i-lucide-layout-dashboard',
|
|
38
|
+
trailingIcon: w.id === workspace.workspaceId ? 'i-lucide-check' : undefined,
|
|
39
|
+
onSelect: () => void switchBoard(w.id),
|
|
40
|
+
})),
|
|
41
|
+
[
|
|
42
|
+
{ label: 'New board…', icon: 'i-lucide-plus', onSelect: () => openPrompt('board') },
|
|
43
|
+
{ label: 'Rename board…', icon: 'i-lucide-pencil', onSelect: () => openPrompt('rename') },
|
|
44
|
+
{
|
|
45
|
+
label: 'Delete board',
|
|
46
|
+
icon: 'i-lucide-trash-2',
|
|
47
|
+
color: 'error' as const,
|
|
48
|
+
onSelect: () => void removeBoard(),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
async function selectAccount(id: string) {
|
|
54
|
+
if (id === accounts.activeAccountId) return
|
|
55
|
+
busy.value = true
|
|
56
|
+
try {
|
|
57
|
+
await workspace.selectAccount(id)
|
|
58
|
+
} catch (e) {
|
|
59
|
+
notifyError('Could not switch account', e)
|
|
60
|
+
} finally {
|
|
61
|
+
busy.value = false
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function switchBoard(id: string) {
|
|
66
|
+
busy.value = true
|
|
67
|
+
try {
|
|
68
|
+
await workspace.switchTo(id)
|
|
69
|
+
} catch (e) {
|
|
70
|
+
notifyError('Could not open board', e)
|
|
71
|
+
} finally {
|
|
72
|
+
busy.value = false
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function removeBoard() {
|
|
77
|
+
const id = workspace.workspaceId
|
|
78
|
+
if (!id) return
|
|
79
|
+
busy.value = true
|
|
80
|
+
try {
|
|
81
|
+
await workspace.remove(id)
|
|
82
|
+
toast.add({ title: 'Board deleted', icon: 'i-lucide-check' })
|
|
83
|
+
} catch (e) {
|
|
84
|
+
notifyError('Could not delete board', e)
|
|
85
|
+
} finally {
|
|
86
|
+
busy.value = false
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---- prompt modal (create account / create board / rename) -----------------
|
|
91
|
+
type PromptKind = 'account' | 'board' | 'rename'
|
|
92
|
+
const prompt = ref<PromptKind | null>(null)
|
|
93
|
+
const promptValue = ref('')
|
|
94
|
+
const promptOpen = computed({
|
|
95
|
+
get: () => prompt.value !== null,
|
|
96
|
+
set: (v: boolean) => {
|
|
97
|
+
if (!v) prompt.value = null
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
const promptMeta: Record<PromptKind, { title: string; placeholder: string; cta: string }> = {
|
|
101
|
+
account: { title: 'New organization', placeholder: 'Acme Inc.', cta: 'Create' },
|
|
102
|
+
board: { title: 'New board', placeholder: 'Untitled board', cta: 'Create' },
|
|
103
|
+
rename: { title: 'Rename board', placeholder: 'Board name', cta: 'Save' },
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function openPrompt(kind: PromptKind) {
|
|
107
|
+
prompt.value = kind
|
|
108
|
+
promptValue.value = kind === 'rename' ? (workspace.activeWorkspace?.name ?? '') : ''
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function submitPrompt() {
|
|
112
|
+
const kind = prompt.value
|
|
113
|
+
const name = promptValue.value.trim()
|
|
114
|
+
if (!kind || (!name && kind !== 'board')) return
|
|
115
|
+
busy.value = true
|
|
116
|
+
try {
|
|
117
|
+
if (kind === 'account') {
|
|
118
|
+
await accounts.createOrg(name)
|
|
119
|
+
// The new org starts empty — open (create) its first board.
|
|
120
|
+
await workspace.selectAccount(accounts.activeAccountId!)
|
|
121
|
+
} else if (kind === 'board') {
|
|
122
|
+
await workspace.create(name || undefined)
|
|
123
|
+
} else if (workspace.workspaceId) {
|
|
124
|
+
await workspace.rename(workspace.workspaceId, name)
|
|
125
|
+
}
|
|
126
|
+
prompt.value = null
|
|
127
|
+
} catch (e) {
|
|
128
|
+
notifyError('Action failed', e)
|
|
129
|
+
} finally {
|
|
130
|
+
busy.value = false
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
</script>
|
|
134
|
+
|
|
135
|
+
<template>
|
|
136
|
+
<div class="space-y-1.5">
|
|
137
|
+
<!-- account selector (only when accounts exist) -->
|
|
138
|
+
<UDropdownMenu
|
|
139
|
+
v-if="accounts.enabled"
|
|
140
|
+
:items="accountItems"
|
|
141
|
+
:content="{ align: 'start' }"
|
|
142
|
+
class="w-full"
|
|
143
|
+
>
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
class="flex w-full items-center gap-2 rounded-md px-1.5 py-1 text-left transition hover:bg-slate-800/60"
|
|
147
|
+
:disabled="busy"
|
|
148
|
+
>
|
|
149
|
+
<UIcon
|
|
150
|
+
:name="accounts.activeAccount?.type === 'org' ? 'i-lucide-users' : 'i-lucide-user'"
|
|
151
|
+
class="h-3.5 w-3.5 shrink-0 text-slate-400"
|
|
152
|
+
/>
|
|
153
|
+
<span class="truncate text-[11px] font-medium uppercase tracking-wide text-slate-400">
|
|
154
|
+
{{ accounts.activeAccount?.name ?? 'Account' }}
|
|
155
|
+
</span>
|
|
156
|
+
<UIcon
|
|
157
|
+
name="i-lucide-chevrons-up-down"
|
|
158
|
+
class="ml-auto h-3.5 w-3.5 shrink-0 text-slate-600"
|
|
159
|
+
/>
|
|
160
|
+
</button>
|
|
161
|
+
</UDropdownMenu>
|
|
162
|
+
|
|
163
|
+
<!-- board selector -->
|
|
164
|
+
<UDropdownMenu :items="boardItems" :content="{ align: 'start' }" class="w-full">
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
class="flex w-full items-center gap-2 rounded-lg border border-slate-800 bg-slate-900/60 px-2.5 py-1.5 text-left transition hover:bg-slate-800/60"
|
|
168
|
+
:disabled="busy"
|
|
169
|
+
>
|
|
170
|
+
<UIcon name="i-lucide-layout-dashboard" class="h-4 w-4 shrink-0 text-indigo-400" />
|
|
171
|
+
<span class="truncate text-sm font-medium text-white">
|
|
172
|
+
{{ workspace.activeWorkspace?.name ?? 'Board' }}
|
|
173
|
+
</span>
|
|
174
|
+
<UIcon name="i-lucide-chevron-down" class="ml-auto h-4 w-4 shrink-0 text-slate-500" />
|
|
175
|
+
</button>
|
|
176
|
+
</UDropdownMenu>
|
|
177
|
+
|
|
178
|
+
<!-- create / rename prompt -->
|
|
179
|
+
<UModal v-model:open="promptOpen" :title="prompt ? promptMeta[prompt].title : ''">
|
|
180
|
+
<template #body>
|
|
181
|
+
<form class="space-y-3" @submit.prevent="submitPrompt">
|
|
182
|
+
<UFormField label="Name">
|
|
183
|
+
<UInput
|
|
184
|
+
v-model="promptValue"
|
|
185
|
+
autofocus
|
|
186
|
+
:placeholder="prompt ? promptMeta[prompt].placeholder : ''"
|
|
187
|
+
class="w-full"
|
|
188
|
+
/>
|
|
189
|
+
</UFormField>
|
|
190
|
+
<div class="flex justify-end gap-2">
|
|
191
|
+
<UButton color="neutral" variant="ghost" :disabled="busy" @click="prompt = null">
|
|
192
|
+
Cancel
|
|
193
|
+
</UButton>
|
|
194
|
+
<UButton type="submit" color="primary" :loading="busy">
|
|
195
|
+
{{ prompt ? promptMeta[prompt].cta : '' }}
|
|
196
|
+
</UButton>
|
|
197
|
+
</div>
|
|
198
|
+
</form>
|
|
199
|
+
</template>
|
|
200
|
+
</UModal>
|
|
201
|
+
</div>
|
|
202
|
+
</template>
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useBoardFlow } from '~/composables/useBoardFlow'
|
|
3
|
+
|
|
4
|
+
const ui = useUiStore()
|
|
5
|
+
const board = useBoardStore()
|
|
6
|
+
const execution = useExecutionStore()
|
|
7
|
+
const workspace = useWorkspaceStore()
|
|
8
|
+
const { fitView, zoomIn, zoomOut } = useBoardFlow()
|
|
9
|
+
|
|
10
|
+
const zoomPct = computed(() => Math.round(ui.zoom * 100))
|
|
11
|
+
const lodLabel = computed(() => ({ far: 'Overview', mid: 'Summary', close: 'Detail' })[ui.lod])
|
|
12
|
+
|
|
13
|
+
// Live spend indicator: shown once any tokens have been metered this period.
|
|
14
|
+
const spend = computed(() => workspace.spend)
|
|
15
|
+
const showSpend = computed(() => !!spend.value && spend.value.costSpent > 0)
|
|
16
|
+
const spendLabel = computed(() => {
|
|
17
|
+
const s = spend.value
|
|
18
|
+
if (!s) return ''
|
|
19
|
+
const fmt = (n: number) => {
|
|
20
|
+
try {
|
|
21
|
+
return new Intl.NumberFormat(undefined, { style: 'currency', currency: s.currency }).format(n)
|
|
22
|
+
} catch {
|
|
23
|
+
return `${n.toFixed(2)} ${s.currency}`
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return `${fmt(s.costSpent)} / ${fmt(s.costLimit)}`
|
|
27
|
+
})
|
|
28
|
+
const spendColor = computed(() => (spend.value?.exceeded ? 'error' : 'neutral'))
|
|
29
|
+
|
|
30
|
+
const decisionItems = computed(() =>
|
|
31
|
+
execution.openDecisions.map((d) => {
|
|
32
|
+
const b = board.getBlock(d.blockId)
|
|
33
|
+
return {
|
|
34
|
+
label: b?.title ?? 'Block',
|
|
35
|
+
description: d.decision.question,
|
|
36
|
+
icon: 'i-lucide-circle-help',
|
|
37
|
+
onSelect: () => ui.openDecision(d.instanceId, d.decision.id),
|
|
38
|
+
}
|
|
39
|
+
}),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
async function resetBoard() {
|
|
43
|
+
await workspace.reset()
|
|
44
|
+
ui.select(null)
|
|
45
|
+
ui.focus(null)
|
|
46
|
+
setTimeout(() => fitView({ padding: 0.2 }), 50)
|
|
47
|
+
}
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<template>
|
|
51
|
+
<div
|
|
52
|
+
class="absolute left-1/2 top-4 z-20 flex -translate-x-1/2 items-center gap-1 rounded-full border border-slate-700 bg-slate-900/90 px-2 py-1.5 shadow-xl backdrop-blur"
|
|
53
|
+
>
|
|
54
|
+
<!-- zoom controls -->
|
|
55
|
+
<UButton
|
|
56
|
+
icon="i-lucide-zoom-out"
|
|
57
|
+
color="neutral"
|
|
58
|
+
variant="ghost"
|
|
59
|
+
size="sm"
|
|
60
|
+
@click="zoomOut()"
|
|
61
|
+
/>
|
|
62
|
+
<div class="w-20 text-center text-xs tabular-nums text-slate-300">
|
|
63
|
+
{{ zoomPct }}%
|
|
64
|
+
<div class="text-[9px] uppercase tracking-wide text-slate-500">{{ lodLabel }}</div>
|
|
65
|
+
</div>
|
|
66
|
+
<UButton icon="i-lucide-zoom-in" color="neutral" variant="ghost" size="sm" @click="zoomIn()" />
|
|
67
|
+
<UButton
|
|
68
|
+
icon="i-lucide-maximize"
|
|
69
|
+
color="neutral"
|
|
70
|
+
variant="ghost"
|
|
71
|
+
size="sm"
|
|
72
|
+
@click="fitView({ padding: 0.2 })"
|
|
73
|
+
/>
|
|
74
|
+
|
|
75
|
+
<USeparator orientation="vertical" class="mx-1 h-6" />
|
|
76
|
+
|
|
77
|
+
<!-- decisions queue -->
|
|
78
|
+
<UDropdownMenu v-if="execution.pendingDecisionCount" :items="decisionItems">
|
|
79
|
+
<UButton color="warning" variant="soft" size="sm" icon="i-lucide-circle-help">
|
|
80
|
+
{{ execution.pendingDecisionCount }} decision{{
|
|
81
|
+
execution.pendingDecisionCount === 1 ? '' : 's'
|
|
82
|
+
}}
|
|
83
|
+
</UButton>
|
|
84
|
+
</UDropdownMenu>
|
|
85
|
+
|
|
86
|
+
<!-- spend safeguard usage -->
|
|
87
|
+
<UButton
|
|
88
|
+
v-if="showSpend"
|
|
89
|
+
:color="spendColor"
|
|
90
|
+
variant="soft"
|
|
91
|
+
size="sm"
|
|
92
|
+
icon="i-lucide-wallet"
|
|
93
|
+
:title="spend?.exceeded ? 'Spend limit reached — runs paused' : 'Token spend this month'"
|
|
94
|
+
>
|
|
95
|
+
{{ spendLabel }}
|
|
96
|
+
</UButton>
|
|
97
|
+
|
|
98
|
+
<USeparator orientation="vertical" class="mx-1 h-6" />
|
|
99
|
+
|
|
100
|
+
<UButton
|
|
101
|
+
icon="i-lucide-rotate-ccw"
|
|
102
|
+
color="neutral"
|
|
103
|
+
variant="ghost"
|
|
104
|
+
size="sm"
|
|
105
|
+
title="Reset board to sample"
|
|
106
|
+
@click="resetBoard"
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
</template>
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import BlockPalette from '~/components/palettes/BlockPalette.vue'
|
|
3
|
+
import PipelinePalette from '~/components/palettes/PipelinePalette.vue'
|
|
4
|
+
import BoardSwitcher from '~/components/layout/BoardSwitcher.vue'
|
|
5
|
+
import UserMenu from '~/components/auth/UserMenu.vue'
|
|
6
|
+
|
|
7
|
+
const documents = useDocumentsStore()
|
|
8
|
+
const tasks = useTasksStore()
|
|
9
|
+
const github = useGitHubStore()
|
|
10
|
+
const library = useFragmentLibraryStore()
|
|
11
|
+
const workspace = useWorkspaceStore()
|
|
12
|
+
const ui = useUiStore()
|
|
13
|
+
|
|
14
|
+
// Resolve whether the document-source / task-source / GitHub integrations are
|
|
15
|
+
// enabled on the backend, so each section is hidden entirely when it is off
|
|
16
|
+
// (mirrors how auth gates its UI). A 503 from a probe flips its `available` to
|
|
17
|
+
// false. Re-probe whenever the active board changes — connections are per board.
|
|
18
|
+
watch(
|
|
19
|
+
() => workspace.workspaceId,
|
|
20
|
+
(id) => {
|
|
21
|
+
if (!id) return
|
|
22
|
+
void documents.probe()
|
|
23
|
+
void tasks.probe()
|
|
24
|
+
void github.probe()
|
|
25
|
+
void library.probe()
|
|
26
|
+
},
|
|
27
|
+
{ immediate: true },
|
|
28
|
+
)
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<aside
|
|
33
|
+
class="flex h-full w-64 shrink-0 flex-col gap-4 overflow-y-auto border-r border-slate-800 bg-slate-900/80 p-3 backdrop-blur"
|
|
34
|
+
>
|
|
35
|
+
<BoardSwitcher />
|
|
36
|
+
|
|
37
|
+
<USeparator />
|
|
38
|
+
|
|
39
|
+
<section>
|
|
40
|
+
<h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
41
|
+
Building blocks
|
|
42
|
+
</h2>
|
|
43
|
+
<BlockPalette />
|
|
44
|
+
</section>
|
|
45
|
+
|
|
46
|
+
<USeparator />
|
|
47
|
+
|
|
48
|
+
<section>
|
|
49
|
+
<h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
50
|
+
Pipelines
|
|
51
|
+
</h2>
|
|
52
|
+
<PipelinePalette />
|
|
53
|
+
</section>
|
|
54
|
+
|
|
55
|
+
<USeparator />
|
|
56
|
+
<section>
|
|
57
|
+
<h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
58
|
+
Repositories
|
|
59
|
+
</h2>
|
|
60
|
+
<UButton
|
|
61
|
+
block
|
|
62
|
+
color="neutral"
|
|
63
|
+
variant="soft"
|
|
64
|
+
size="sm"
|
|
65
|
+
icon="i-lucide-git-branch-plus"
|
|
66
|
+
class="justify-start"
|
|
67
|
+
@click="ui.openBootstrap()"
|
|
68
|
+
>
|
|
69
|
+
Bootstrap repo
|
|
70
|
+
</UButton>
|
|
71
|
+
</section>
|
|
72
|
+
|
|
73
|
+
<template v-if="library.available">
|
|
74
|
+
<USeparator />
|
|
75
|
+
<section>
|
|
76
|
+
<h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
77
|
+
Prompt library
|
|
78
|
+
</h2>
|
|
79
|
+
<UButton
|
|
80
|
+
block
|
|
81
|
+
color="neutral"
|
|
82
|
+
variant="soft"
|
|
83
|
+
size="sm"
|
|
84
|
+
icon="i-lucide-book-marked"
|
|
85
|
+
class="justify-start"
|
|
86
|
+
@click="ui.openFragmentLibrary()"
|
|
87
|
+
>
|
|
88
|
+
Best-practice fragments
|
|
89
|
+
</UButton>
|
|
90
|
+
</section>
|
|
91
|
+
</template>
|
|
92
|
+
|
|
93
|
+
<template v-if="github.available">
|
|
94
|
+
<USeparator />
|
|
95
|
+
<section>
|
|
96
|
+
<h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
97
|
+
GitHub
|
|
98
|
+
</h2>
|
|
99
|
+
<UButton
|
|
100
|
+
block
|
|
101
|
+
color="neutral"
|
|
102
|
+
variant="soft"
|
|
103
|
+
size="sm"
|
|
104
|
+
icon="i-lucide-github"
|
|
105
|
+
class="justify-start"
|
|
106
|
+
@click="ui.openGitHub()"
|
|
107
|
+
>
|
|
108
|
+
<span class="truncate">
|
|
109
|
+
{{ github.connected ? github.connection?.accountLogin : 'Connect GitHub' }}
|
|
110
|
+
</span>
|
|
111
|
+
</UButton>
|
|
112
|
+
</section>
|
|
113
|
+
</template>
|
|
114
|
+
|
|
115
|
+
<template v-if="documents.available && documents.sources.length">
|
|
116
|
+
<USeparator />
|
|
117
|
+
<section>
|
|
118
|
+
<h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
119
|
+
Document sources
|
|
120
|
+
</h2>
|
|
121
|
+
<div class="space-y-1.5">
|
|
122
|
+
<UButton
|
|
123
|
+
v-for="src in documents.sources"
|
|
124
|
+
:key="src.source"
|
|
125
|
+
block
|
|
126
|
+
color="neutral"
|
|
127
|
+
variant="soft"
|
|
128
|
+
size="sm"
|
|
129
|
+
:icon="src.icon"
|
|
130
|
+
class="justify-start"
|
|
131
|
+
@click="ui.openDocumentConnect(src.source)"
|
|
132
|
+
>
|
|
133
|
+
<span class="truncate">
|
|
134
|
+
{{ documents.isConnected(src.source) ? src.label : `Connect ${src.label}` }}
|
|
135
|
+
</span>
|
|
136
|
+
</UButton>
|
|
137
|
+
<UButton
|
|
138
|
+
v-if="documents.anyConnected"
|
|
139
|
+
block
|
|
140
|
+
color="neutral"
|
|
141
|
+
variant="soft"
|
|
142
|
+
size="sm"
|
|
143
|
+
icon="i-lucide-file-down"
|
|
144
|
+
class="justify-start"
|
|
145
|
+
@click="ui.openDocumentImport(null)"
|
|
146
|
+
>
|
|
147
|
+
Import & spawn
|
|
148
|
+
</UButton>
|
|
149
|
+
</div>
|
|
150
|
+
</section>
|
|
151
|
+
</template>
|
|
152
|
+
|
|
153
|
+
<template v-if="tasks.available && tasks.sources.length">
|
|
154
|
+
<USeparator />
|
|
155
|
+
<section>
|
|
156
|
+
<h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
157
|
+
Task sources
|
|
158
|
+
</h2>
|
|
159
|
+
<div class="space-y-1.5">
|
|
160
|
+
<UButton
|
|
161
|
+
v-for="src in tasks.sources"
|
|
162
|
+
:key="src.source"
|
|
163
|
+
block
|
|
164
|
+
color="neutral"
|
|
165
|
+
variant="soft"
|
|
166
|
+
size="sm"
|
|
167
|
+
:icon="src.icon"
|
|
168
|
+
class="justify-start"
|
|
169
|
+
@click="ui.openTaskConnect(src.source)"
|
|
170
|
+
>
|
|
171
|
+
<span class="truncate">
|
|
172
|
+
{{ tasks.isConnected(src.source) ? src.label : `Connect ${src.label}` }}
|
|
173
|
+
</span>
|
|
174
|
+
</UButton>
|
|
175
|
+
<UButton
|
|
176
|
+
v-if="tasks.anyConnected"
|
|
177
|
+
block
|
|
178
|
+
color="neutral"
|
|
179
|
+
variant="soft"
|
|
180
|
+
size="sm"
|
|
181
|
+
icon="i-lucide-file-down"
|
|
182
|
+
class="justify-start"
|
|
183
|
+
@click="ui.openTaskImport(null)"
|
|
184
|
+
>
|
|
185
|
+
Import issues
|
|
186
|
+
</UButton>
|
|
187
|
+
</div>
|
|
188
|
+
</section>
|
|
189
|
+
</template>
|
|
190
|
+
|
|
191
|
+
<UserMenu class="mt-auto" />
|
|
192
|
+
</aside>
|
|
193
|
+
</template>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
const workspace = useWorkspaceStore()
|
|
5
|
+
|
|
6
|
+
const spend = computed(() => workspace.spend)
|
|
7
|
+
/** Show the large warning only once the budget has been reached. */
|
|
8
|
+
const exceeded = computed(() => spend.value?.exceeded ?? false)
|
|
9
|
+
|
|
10
|
+
function money(amount: number, currency: string) {
|
|
11
|
+
try {
|
|
12
|
+
return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(amount)
|
|
13
|
+
} catch {
|
|
14
|
+
// Fall back if the currency code isn't recognised by the runtime.
|
|
15
|
+
return `${amount.toFixed(2)} ${currency}`
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const tokens = computed(() => {
|
|
20
|
+
const s = spend.value
|
|
21
|
+
if (!s) return ''
|
|
22
|
+
return new Intl.NumberFormat().format(s.inputTokens + s.outputTokens)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const resuming = ref(false)
|
|
26
|
+
async function resume() {
|
|
27
|
+
resuming.value = true
|
|
28
|
+
try {
|
|
29
|
+
await workspace.resumeSpend()
|
|
30
|
+
} finally {
|
|
31
|
+
resuming.value = false
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<Transition name="fade">
|
|
38
|
+
<div
|
|
39
|
+
v-if="exceeded && spend"
|
|
40
|
+
class="absolute inset-x-0 top-0 z-50 flex justify-center px-4 pt-4"
|
|
41
|
+
>
|
|
42
|
+
<div
|
|
43
|
+
class="w-full max-w-3xl rounded-2xl border-2 border-red-500/70 bg-red-950/95 p-5 shadow-2xl backdrop-blur"
|
|
44
|
+
role="alert"
|
|
45
|
+
>
|
|
46
|
+
<div class="flex items-start gap-4">
|
|
47
|
+
<UIcon name="i-lucide-octagon-alert" class="mt-0.5 h-10 w-10 shrink-0 text-red-400" />
|
|
48
|
+
<div class="min-w-0 flex-1">
|
|
49
|
+
<h2 class="text-lg font-semibold text-red-100">
|
|
50
|
+
Spend limit reached — agent execution paused
|
|
51
|
+
</h2>
|
|
52
|
+
<p class="mt-1 text-sm text-red-200/90">
|
|
53
|
+
This month's token spend has hit the configured budget, so running pipelines were
|
|
54
|
+
paused to avoid further cost. Execution resumes automatically when the budget rolls
|
|
55
|
+
over next month, or once the limit is raised.
|
|
56
|
+
</p>
|
|
57
|
+
|
|
58
|
+
<dl class="mt-4 grid grid-cols-2 gap-3 sm:grid-cols-3">
|
|
59
|
+
<div class="rounded-lg bg-red-900/50 px-3 py-2">
|
|
60
|
+
<dt class="text-[11px] uppercase tracking-wide text-red-300/80">Spent</dt>
|
|
61
|
+
<dd class="text-base font-semibold tabular-nums text-red-50">
|
|
62
|
+
{{ money(spend.costSpent, spend.currency) }}
|
|
63
|
+
</dd>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="rounded-lg bg-red-900/50 px-3 py-2">
|
|
66
|
+
<dt class="text-[11px] uppercase tracking-wide text-red-300/80">Budget</dt>
|
|
67
|
+
<dd class="text-base font-semibold tabular-nums text-red-50">
|
|
68
|
+
{{ money(spend.costLimit, spend.currency) }}
|
|
69
|
+
</dd>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="rounded-lg bg-red-900/50 px-3 py-2">
|
|
72
|
+
<dt class="text-[11px] uppercase tracking-wide text-red-300/80">Tokens</dt>
|
|
73
|
+
<dd class="text-base font-semibold tabular-nums text-red-50">{{ tokens }}</dd>
|
|
74
|
+
</div>
|
|
75
|
+
</dl>
|
|
76
|
+
|
|
77
|
+
<div class="mt-4 flex items-center gap-3">
|
|
78
|
+
<UButton
|
|
79
|
+
color="error"
|
|
80
|
+
variant="solid"
|
|
81
|
+
icon="i-lucide-play"
|
|
82
|
+
:loading="resuming"
|
|
83
|
+
@click="resume"
|
|
84
|
+
>
|
|
85
|
+
Resume anyway
|
|
86
|
+
</UButton>
|
|
87
|
+
<span class="text-xs text-red-300/70">
|
|
88
|
+
Resuming continues spending; it will pause again if still over budget.
|
|
89
|
+
</span>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</Transition>
|
|
96
|
+
</template>
|
|
97
|
+
|
|
98
|
+
<style scoped>
|
|
99
|
+
.fade-enter-active,
|
|
100
|
+
.fade-leave-active {
|
|
101
|
+
transition: opacity 0.2s ease;
|
|
102
|
+
}
|
|
103
|
+
.fade-enter-from,
|
|
104
|
+
.fade-leave-to {
|
|
105
|
+
opacity: 0;
|
|
106
|
+
}
|
|
107
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { AgentKind } from '~/types/domain'
|
|
3
|
+
|
|
4
|
+
const agents = useAgentsStore()
|
|
5
|
+
defineEmits<{ (e: 'add', kind: AgentKind): void }>()
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<div class="space-y-2">
|
|
10
|
+
<p class="px-1 text-[11px] text-slate-500">Click an agent to append it to the pipeline.</p>
|
|
11
|
+
<div class="space-y-1.5">
|
|
12
|
+
<button
|
|
13
|
+
v-for="a in agents.archetypes"
|
|
14
|
+
:key="a.kind"
|
|
15
|
+
type="button"
|
|
16
|
+
class="flex w-full items-center gap-2.5 rounded-lg border border-slate-700 bg-slate-800/60 p-2 text-left transition hover:border-slate-500 hover:bg-slate-800"
|
|
17
|
+
@click="$emit('add', a.kind)"
|
|
18
|
+
>
|
|
19
|
+
<div
|
|
20
|
+
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg"
|
|
21
|
+
:style="{ backgroundColor: a.color + '22' }"
|
|
22
|
+
>
|
|
23
|
+
<UIcon :name="a.icon" class="h-4 w-4" :style="{ color: a.color }" />
|
|
24
|
+
</div>
|
|
25
|
+
<div class="min-w-0">
|
|
26
|
+
<div class="text-xs font-semibold text-slate-100">{{ a.label }}</div>
|
|
27
|
+
<div class="truncate text-[10px] text-slate-400">{{ a.description }}</div>
|
|
28
|
+
</div>
|
|
29
|
+
<UIcon name="i-lucide-plus" class="ml-auto h-4 w-4 shrink-0 text-slate-500" />
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { BlockType } from '~/types/domain'
|
|
3
|
+
import { BLOCK_TYPE_META } from '~/utils/catalog'
|
|
4
|
+
import { setDndPayload } from '~/utils/dnd'
|
|
5
|
+
|
|
6
|
+
const types = Object.keys(BLOCK_TYPE_META) as BlockType[]
|
|
7
|
+
|
|
8
|
+
function onDragStart(event: DragEvent, blockType: BlockType) {
|
|
9
|
+
setDndPayload(event, { kind: 'block', blockType })
|
|
10
|
+
;(event.target as HTMLElement).classList.add('palette-dragging')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function onDragEnd(event: DragEvent) {
|
|
14
|
+
;(event.target as HTMLElement).classList.remove('palette-dragging')
|
|
15
|
+
}
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<div class="space-y-2">
|
|
20
|
+
<p class="px-1 text-[11px] text-slate-500">Drag a block onto the board.</p>
|
|
21
|
+
<div class="grid grid-cols-2 gap-2">
|
|
22
|
+
<div
|
|
23
|
+
v-for="t in types"
|
|
24
|
+
:key="t"
|
|
25
|
+
draggable="true"
|
|
26
|
+
class="flex cursor-grab select-none flex-col items-center gap-1 rounded-lg border border-slate-700 bg-slate-800/60 p-2.5 transition hover:border-slate-500 hover:bg-slate-800 active:cursor-grabbing"
|
|
27
|
+
@dragstart="onDragStart($event, t)"
|
|
28
|
+
@dragend="onDragEnd"
|
|
29
|
+
>
|
|
30
|
+
<UIcon
|
|
31
|
+
:name="BLOCK_TYPE_META[t].icon"
|
|
32
|
+
class="h-5 w-5"
|
|
33
|
+
:style="{ color: BLOCK_TYPE_META[t].accent }"
|
|
34
|
+
/>
|
|
35
|
+
<span class="text-[11px] font-medium text-slate-200">
|
|
36
|
+
{{ BLOCK_TYPE_META[t].label }}
|
|
37
|
+
</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</template>
|