@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Igor Savin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# `@cat-factory/app` — Frontend (Nuxt layer)
|
|
2
|
+
|
|
3
|
+
The user-facing app, packaged as a **reusable Nuxt 4 layer**: a single-page app
|
|
4
|
+
that runs entirely in the browser and renders the architecture board, drives agent
|
|
5
|
+
pipelines, and reflects live execution. A deployment consumes it via
|
|
6
|
+
`extends: ['@cat-factory/app']` (see [`deploy/frontend`](../../deploy/frontend)).
|
|
7
|
+
It talks to the [backend Worker](../../backend/README.md) over REST and a single
|
|
8
|
+
WebSocket, sharing wire types from
|
|
9
|
+
[`@cat-factory/contracts`](../../backend/packages/contracts).
|
|
10
|
+
|
|
11
|
+
The SPA source lives under `app/` (the Nuxt srcDir).
|
|
12
|
+
|
|
13
|
+
## Table of contents
|
|
14
|
+
|
|
15
|
+
- [What it is](#what-it-is)
|
|
16
|
+
- [Tech stack](#tech-stack)
|
|
17
|
+
- [Layout](#layout)
|
|
18
|
+
- [Key UI surfaces](#key-ui-surfaces)
|
|
19
|
+
- [Develop & test](#develop--test)
|
|
20
|
+
|
|
21
|
+
## What it is
|
|
22
|
+
|
|
23
|
+
A spatial planning surface. You lay out a system as a **board** of frames
|
|
24
|
+
(services), modules and tasks on a [Vue Flow](https://vueflow.dev) canvas, wire up
|
|
25
|
+
dependencies, attach requirements, and apply **agent pipelines** to blocks.
|
|
26
|
+
Execution streams back in real time — step/subtask progress bars, decision
|
|
27
|
+
prompts, failures with retry — so the canvas doubles as a live dashboard.
|
|
28
|
+
|
|
29
|
+
It is a thin client: there is **no business logic here**. Every mutation calls the
|
|
30
|
+
Worker API and the stores hydrate from server snapshots and live updates pushed
|
|
31
|
+
over the WebSocket. How that sync works is written up in
|
|
32
|
+
[`app/docs/architecture.md`](./app/docs/architecture.md).
|
|
33
|
+
|
|
34
|
+
## Tech stack
|
|
35
|
+
|
|
36
|
+
- **Nuxt 4 / Vue 3** SPA — single route (`pages/index.vue`).
|
|
37
|
+
- **Pinia** (+ `pinia-plugin-persistedstate`) — feature stores.
|
|
38
|
+
- **Vue Flow** (`core`, `background`, `controls`, `minimap`, `node-resizer`) — the canvas.
|
|
39
|
+
- **Nuxt UI** + Tailwind — components and styling.
|
|
40
|
+
- **VueUse** — composable utilities.
|
|
41
|
+
- Lint/format via **oxlint** + **oxfmt**; tests via **vitest** + **happy-dom**.
|
|
42
|
+
|
|
43
|
+
## Layout
|
|
44
|
+
|
|
45
|
+
| Path | Contents |
|
|
46
|
+
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
47
|
+
| `app.vue` | Root; wraps the page in `AuthGate`. |
|
|
48
|
+
| `pages/index.vue` | The only route — mounts the sidebar, canvas, toolbar, inspector, focus view, and all modals. |
|
|
49
|
+
| `components/` | UI grouped by area (see [Key UI surfaces](#key-ui-surfaces)). |
|
|
50
|
+
| `composables/` | `useApi` (typed client), `useWorkspaceStream` (WebSocket sync), `useBlockDrag`, `useBlockQueries`, `useBoardFlow`, `useSemanticZoom`, `useDepLabels`. |
|
|
51
|
+
| `stores/` | Pinia stores, one per feature domain. |
|
|
52
|
+
| `types/` | TypeScript domain unions (`domain.ts`) and wire types mirroring the contracts. |
|
|
53
|
+
| `utils/` | Small pure helpers. |
|
|
54
|
+
|
|
55
|
+
## Key UI surfaces
|
|
56
|
+
|
|
57
|
+
- **Board canvas** (`components/board`) — `BoardCanvas` + `nodes/` (`BlockNode`,
|
|
58
|
+
`ModuleFrame`, `TaskCard`), dependency edges, the per-block `AgentFailureCard` /
|
|
59
|
+
`AgentStopButton`, and a deep-zoom `focus/BlockFocusView`.
|
|
60
|
+
- **Sidebar & chrome** (`components/layout`) — board/account switchers, palettes
|
|
61
|
+
entry points, the `SpendWarningBanner`, and the toolbar (zoom, LOD, decision
|
|
62
|
+
queue).
|
|
63
|
+
- **Palettes** (`components/palettes`) — drag blocks, pipelines and agents onto
|
|
64
|
+
the board.
|
|
65
|
+
- **Inspector** (`components/panels` + `panels/inspector`) — per-block tabs:
|
|
66
|
+
structure, dependencies, model + fragment picker, live execution, and linked
|
|
67
|
+
docs/issues/scenarios. Decisions resolve via `DecisionModal`.
|
|
68
|
+
- **Pipeline builder** (`components/pipeline`) — assemble/edit agent chains and
|
|
69
|
+
watch `PipelineProgress`.
|
|
70
|
+
- **Integrations** — modals/panels for `github`, `bootstrap`, `documents`,
|
|
71
|
+
`tasks`, `requirements` (review), `scenarios` (acceptance), and `fragments`
|
|
72
|
+
(the prompt-fragment library).
|
|
73
|
+
- **Auth** (`components/auth`) — `AuthGate` / `LoginScreen` / `UserMenu`; the app
|
|
74
|
+
is gated when the backend requires sign-in.
|
|
75
|
+
|
|
76
|
+
## Develop & test
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pnpm install
|
|
80
|
+
pnpm dev # Nuxt dev server (expects the Worker running; set NUXT_PUBLIC_API_BASE)
|
|
81
|
+
pnpm test # vitest
|
|
82
|
+
pnpm typecheck # nuxt typecheck
|
|
83
|
+
pnpm lint # oxlint + oxfmt --check
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
> Building/deploying the static site is covered in the deployment docs — see the
|
|
87
|
+
> [top-level README → Deployment](../../README.md#deployment) and
|
|
88
|
+
> [`deploy/frontend/README.md`](../../deploy/frontend/README.md).
|
package/app/app.vue
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
@import '@nuxt/ui';
|
|
3
|
+
|
|
4
|
+
/* Toast text should be selectable. Reka UI sets `user-select: none` inline on
|
|
5
|
+
* the toast root (to support swipe-to-dismiss); since `user-select` is
|
|
6
|
+
* inherited, declaring it directly on the text slots overrides that. */
|
|
7
|
+
[data-slot='viewport'] [data-slot='title'],
|
|
8
|
+
[data-slot='viewport'] [data-slot='description'] {
|
|
9
|
+
user-select: text;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* ---- Agent Architecture Board: shared visual language ---------------------- */
|
|
13
|
+
|
|
14
|
+
:root {
|
|
15
|
+
--board-bg: #0b1020;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
html,
|
|
19
|
+
body,
|
|
20
|
+
#__nuxt {
|
|
21
|
+
height: 100%;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
body {
|
|
25
|
+
margin: 0;
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Vue Flow surface tuned to a dark "design board" look */
|
|
30
|
+
.vue-flow__background {
|
|
31
|
+
background-color: var(--board-bg);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Status-pulse used by blocked / decision-needed blocks */
|
|
35
|
+
@keyframes board-pulse {
|
|
36
|
+
0%,
|
|
37
|
+
100% {
|
|
38
|
+
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.55);
|
|
39
|
+
}
|
|
40
|
+
50% {
|
|
41
|
+
box-shadow: 0 0 0 10px rgba(245, 158, 11, 0);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.board-pulse {
|
|
46
|
+
animation: board-pulse 1.4s ease-in-out infinite;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Green pulse used when a block reaches the "PR ready" state */
|
|
50
|
+
@keyframes board-pulse-green {
|
|
51
|
+
0%,
|
|
52
|
+
100% {
|
|
53
|
+
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.6);
|
|
54
|
+
}
|
|
55
|
+
50% {
|
|
56
|
+
box-shadow: 0 0 0 12px rgba(34, 197, 94, 0);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.board-pulse-green {
|
|
61
|
+
animation: board-pulse-green 1.2s ease-in-out infinite;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Marching-ants on edges feeding an in-progress block */
|
|
65
|
+
.vue-flow__edge.is-live .vue-flow__edge-path {
|
|
66
|
+
stroke-dasharray: 6 4;
|
|
67
|
+
animation: board-dash 0.6s linear infinite;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* A selected edge: highlight it so the user knows what Delete/Backspace removes */
|
|
71
|
+
.vue-flow__edge.selected .vue-flow__edge-path {
|
|
72
|
+
stroke: #f43f5e !important;
|
|
73
|
+
stroke-width: 2.5 !important;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Fatter invisible hit area so thin edges are easy to click */
|
|
77
|
+
.vue-flow__edge .vue-flow__edge-interaction {
|
|
78
|
+
stroke-width: 18;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@keyframes board-dash {
|
|
82
|
+
to {
|
|
83
|
+
stroke-dashoffset: -20;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* Custom block node should not show the default node chrome */
|
|
88
|
+
.vue-flow__node-block {
|
|
89
|
+
padding: 0;
|
|
90
|
+
border: none;
|
|
91
|
+
background: transparent;
|
|
92
|
+
width: auto;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Make palette drag images feel intentional. Scoped to a dedicated class so it
|
|
96
|
+
* never collides with Vue Flow's own `dragging` class, which it applies to the
|
|
97
|
+
* pane while panning (a bare `.dragging` rule would dim the whole board). */
|
|
98
|
+
.palette-dragging {
|
|
99
|
+
opacity: 0.5;
|
|
100
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import LoginScreen from '~/components/auth/LoginScreen.vue'
|
|
3
|
+
|
|
4
|
+
// Resolves auth state once on mount, then either renders the app (auth off, or
|
|
5
|
+
// on with a signed-in user) or the login screen. The board's own bootstrap runs
|
|
6
|
+
// inside the default slot, so it only fires once the user is allowed in.
|
|
7
|
+
const auth = useAuthStore()
|
|
8
|
+
|
|
9
|
+
onMounted(() => auth.bootstrap())
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<div
|
|
14
|
+
v-if="!auth.ready"
|
|
15
|
+
class="flex h-screen w-screen flex-col items-center justify-center gap-3 bg-slate-950 text-slate-400"
|
|
16
|
+
>
|
|
17
|
+
<UIcon name="i-lucide-loader" class="h-8 w-8 animate-spin" />
|
|
18
|
+
<span class="text-sm">Loading…</span>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<LoginScreen v-else-if="auth.required && !auth.user" />
|
|
22
|
+
|
|
23
|
+
<slot v-else />
|
|
24
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const auth = useAuthStore()
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<div class="flex h-screen w-screen items-center justify-center bg-slate-950 text-slate-100">
|
|
7
|
+
<div
|
|
8
|
+
class="w-full max-w-sm rounded-xl border border-slate-800 bg-slate-900/80 p-8 text-center backdrop-blur"
|
|
9
|
+
>
|
|
10
|
+
<UIcon name="i-lucide-layout-dashboard" class="mx-auto mb-3 h-10 w-10 text-indigo-400" />
|
|
11
|
+
<h1 class="mb-1 text-lg font-semibold text-white">Architecture Board</h1>
|
|
12
|
+
<p class="mb-6 text-sm text-slate-400">Sign in with your GitHub account to continue.</p>
|
|
13
|
+
<UButton block size="lg" color="primary" icon="i-lucide-github" @click="auth.login()">
|
|
14
|
+
Sign in with GitHub
|
|
15
|
+
</UButton>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
3
|
+
|
|
4
|
+
// Signed-in identity + sign-out, shown in the sidebar when auth is enabled.
|
|
5
|
+
const auth = useAuthStore()
|
|
6
|
+
|
|
7
|
+
const items = computed<DropdownMenuItem[][]>(() => [
|
|
8
|
+
[
|
|
9
|
+
{
|
|
10
|
+
label: 'Sign out',
|
|
11
|
+
icon: 'i-lucide-log-out',
|
|
12
|
+
onSelect: () => auth.logout(),
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
])
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<UDropdownMenu v-if="auth.user" :items="items" :content="{ side: 'top', align: 'start' }">
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
class="flex w-full items-center gap-2 rounded-lg border border-slate-800 bg-slate-900/60 p-2 text-left transition hover:bg-slate-800/60"
|
|
23
|
+
>
|
|
24
|
+
<UAvatar
|
|
25
|
+
:src="auth.user.avatarUrl ?? undefined"
|
|
26
|
+
:alt="auth.user.login"
|
|
27
|
+
size="xs"
|
|
28
|
+
icon="i-lucide-user"
|
|
29
|
+
/>
|
|
30
|
+
<div class="min-w-0 flex-1">
|
|
31
|
+
<div class="truncate text-xs font-medium text-white">
|
|
32
|
+
{{ auth.user.name || auth.user.login }}
|
|
33
|
+
</div>
|
|
34
|
+
<div class="truncate text-[10px] text-slate-500">@{{ auth.user.login }}</div>
|
|
35
|
+
</div>
|
|
36
|
+
<UIcon name="i-lucide-chevron-up" class="h-4 w-4 shrink-0 text-slate-500" />
|
|
37
|
+
</button>
|
|
38
|
+
</UDropdownMenu>
|
|
39
|
+
</template>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Shared failure banner + retry for any failed "agent run" (a bootstrap or a
|
|
3
|
+
// task execution). Self-contained: it owns the in-flight retry guard and calls
|
|
4
|
+
// the unified retry through the agentRuns store, so every surface (board card,
|
|
5
|
+
// inspector, task panel) gets identical behaviour from one place. Replaces the
|
|
6
|
+
// three hand-rolled bootstrap banners that used to duplicate this logic.
|
|
7
|
+
import type { AgentRunSummary } from '~/stores/agentRuns'
|
|
8
|
+
|
|
9
|
+
const props = withDefaults(
|
|
10
|
+
defineProps<{ run: AgentRunSummary; variant?: 'compact' | 'expanded' }>(),
|
|
11
|
+
{ variant: 'expanded' },
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const agentRuns = useAgentRunsStore()
|
|
15
|
+
const toast = useToast()
|
|
16
|
+
|
|
17
|
+
const compact = computed(() => props.variant === 'compact')
|
|
18
|
+
const failure = computed(() => props.run.failure)
|
|
19
|
+
const title = computed(() => (props.run.kind === 'bootstrap' ? 'Bootstrap failed' : 'Run failed'))
|
|
20
|
+
const retryLabel = computed(() =>
|
|
21
|
+
props.run.kind === 'bootstrap' ? 'Retry bootstrap' : 'Retry run',
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
const retrying = ref(false)
|
|
25
|
+
async function retry() {
|
|
26
|
+
if (retrying.value) return
|
|
27
|
+
retrying.value = true
|
|
28
|
+
try {
|
|
29
|
+
await agentRuns.retry(props.run.runId)
|
|
30
|
+
} catch (e) {
|
|
31
|
+
toast.add({
|
|
32
|
+
title: 'Retry failed',
|
|
33
|
+
description: e instanceof Error ? e.message : String(e),
|
|
34
|
+
color: 'error',
|
|
35
|
+
})
|
|
36
|
+
} finally {
|
|
37
|
+
retrying.value = false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<div
|
|
44
|
+
class="nodrag rounded-lg border border-rose-900/60 bg-rose-950/40"
|
|
45
|
+
:class="compact ? 'px-3 py-2' : 'px-3 py-2.5'"
|
|
46
|
+
>
|
|
47
|
+
<div class="flex items-center gap-1.5" :class="compact ? 'text-[11px]' : 'text-xs'">
|
|
48
|
+
<UIcon
|
|
49
|
+
name="i-lucide-alert-triangle"
|
|
50
|
+
class="shrink-0 text-rose-400"
|
|
51
|
+
:class="compact ? 'h-3.5 w-3.5' : 'h-4 w-4'"
|
|
52
|
+
/>
|
|
53
|
+
<span class="text-rose-300">{{ title }}</span>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<p
|
|
57
|
+
v-if="failure?.message"
|
|
58
|
+
class="mt-1 leading-snug text-rose-300/90"
|
|
59
|
+
:class="compact ? 'line-clamp-2 text-[10px]' : 'text-[11px]'"
|
|
60
|
+
:title="failure.message"
|
|
61
|
+
>
|
|
62
|
+
{{ failure.message }}
|
|
63
|
+
</p>
|
|
64
|
+
|
|
65
|
+
<p
|
|
66
|
+
v-if="failure?.hint"
|
|
67
|
+
class="mt-1 leading-snug text-rose-400/70"
|
|
68
|
+
:class="compact ? 'text-[10px]' : 'text-[11px]'"
|
|
69
|
+
>
|
|
70
|
+
{{ failure.hint }}
|
|
71
|
+
</p>
|
|
72
|
+
|
|
73
|
+
<details v-if="!compact && failure?.detail && failure.detail !== failure.message" class="mt-1">
|
|
74
|
+
<summary class="cursor-pointer text-[10px] text-rose-400/60 hover:text-rose-300">
|
|
75
|
+
Show detail
|
|
76
|
+
</summary>
|
|
77
|
+
<pre
|
|
78
|
+
class="mt-1 max-h-32 overflow-auto whitespace-pre-wrap rounded bg-rose-950/60 p-1.5 text-[10px] text-rose-200/80"
|
|
79
|
+
>{{ failure.detail }}</pre
|
|
80
|
+
>
|
|
81
|
+
</details>
|
|
82
|
+
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
class="nodrag mt-2 flex items-center gap-1 rounded-md bg-rose-900/40 text-rose-200 hover:bg-rose-900/70 disabled:opacity-60"
|
|
86
|
+
:class="compact ? 'px-2 py-0.5 text-[10px]' : 'px-2 py-1 text-[11px]'"
|
|
87
|
+
:disabled="retrying"
|
|
88
|
+
@click.stop="retry"
|
|
89
|
+
>
|
|
90
|
+
<UIcon
|
|
91
|
+
:name="retrying ? 'i-lucide-loader-circle' : 'i-lucide-rotate-ccw'"
|
|
92
|
+
:class="[compact ? 'h-3 w-3' : 'h-3.5 w-3.5', { 'animate-spin': retrying }]"
|
|
93
|
+
/>
|
|
94
|
+
{{ retrying ? 'Retrying…' : compact ? 'Retry' : retryLabel }}
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
</template>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Self-contained "Stop" control for a RUNNING agent run (bootstrap or execution).
|
|
3
|
+
// Calls the unified stop through the agentRuns store — which kills the per-run
|
|
4
|
+
// container and tears down the durable driver server-side — then toasts the
|
|
5
|
+
// outcome so the user is told it actually happened. Mirrors AgentFailureCard's
|
|
6
|
+
// self-contained pattern so every surface (board card, inspector, task panel)
|
|
7
|
+
// behaves identically from one place.
|
|
8
|
+
import type { AgentRunKind } from '~/types/domain'
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(
|
|
11
|
+
defineProps<{
|
|
12
|
+
runId: string
|
|
13
|
+
/** Hint for the button label only; the backend resolves the real kind. */
|
|
14
|
+
kind?: AgentRunKind
|
|
15
|
+
size?: 'xs' | 'sm' | 'md'
|
|
16
|
+
variant?: 'solid' | 'soft' | 'ghost' | 'subtle' | 'outline'
|
|
17
|
+
label?: string
|
|
18
|
+
}>(),
|
|
19
|
+
{ size: 'xs', variant: 'soft', label: 'Stop' },
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const agentRuns = useAgentRunsStore()
|
|
23
|
+
const toast = useToast()
|
|
24
|
+
const stopping = ref(false)
|
|
25
|
+
|
|
26
|
+
async function stop() {
|
|
27
|
+
if (stopping.value) return
|
|
28
|
+
stopping.value = true
|
|
29
|
+
try {
|
|
30
|
+
const kind = await agentRuns.stop(props.runId)
|
|
31
|
+
toast.add({
|
|
32
|
+
title: kind === 'bootstrap' ? 'Bootstrap stopped' : 'Run stopped',
|
|
33
|
+
description: 'The container was killed and the run was cancelled.',
|
|
34
|
+
icon: 'i-lucide-circle-stop',
|
|
35
|
+
color: 'warning',
|
|
36
|
+
})
|
|
37
|
+
} catch (e) {
|
|
38
|
+
toast.add({
|
|
39
|
+
title: 'Stop failed',
|
|
40
|
+
description: e instanceof Error ? e.message : String(e),
|
|
41
|
+
color: 'error',
|
|
42
|
+
})
|
|
43
|
+
} finally {
|
|
44
|
+
stopping.value = false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<template>
|
|
50
|
+
<UButton
|
|
51
|
+
class="nodrag"
|
|
52
|
+
color="warning"
|
|
53
|
+
:variant="variant"
|
|
54
|
+
:size="size"
|
|
55
|
+
icon="i-lucide-circle-stop"
|
|
56
|
+
:loading="stopping"
|
|
57
|
+
@click.stop="stop"
|
|
58
|
+
>
|
|
59
|
+
{{ label }}
|
|
60
|
+
</UButton>
|
|
61
|
+
</template>
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { VueFlow, useVueFlow, type NodeMouseEvent } from '@vue-flow/core'
|
|
3
|
+
import { Background } from '@vue-flow/background'
|
|
4
|
+
import { Controls } from '@vue-flow/controls'
|
|
5
|
+
import { MiniMap } from '@vue-flow/minimap'
|
|
6
|
+
import BlockNode from './nodes/BlockNode.vue'
|
|
7
|
+
import TaskDependencyEdges from './TaskDependencyEdges.vue'
|
|
8
|
+
import { STATUS_META } from '~/utils/catalog'
|
|
9
|
+
import { readDndPayload, blockIdFromEvent } from '~/utils/dnd'
|
|
10
|
+
import { BOARD_FLOW_ID } from '~/composables/useBoardFlow'
|
|
11
|
+
|
|
12
|
+
const board = useBoardStore()
|
|
13
|
+
const pipelines = usePipelinesStore()
|
|
14
|
+
const execution = useExecutionStore()
|
|
15
|
+
const ui = useUiStore()
|
|
16
|
+
const toast = useToast()
|
|
17
|
+
|
|
18
|
+
const { onNodeDragStop, onViewportChange, screenToFlowCoordinate } = useVueFlow(BOARD_FLOW_ID)
|
|
19
|
+
|
|
20
|
+
// Only frames are board nodes. Dependencies live on tasks (rendered inside the
|
|
21
|
+
// frames), so there are no frame-to-frame edges on the canvas.
|
|
22
|
+
//
|
|
23
|
+
// Vue Flow tags every *draggable* node with the `nopan` class, which makes the
|
|
24
|
+
// pane refuse to pan while the pointer is over it. An expanded frame fills much
|
|
25
|
+
// of the viewport, so leaving it draggable turns the whole canvas into a dead
|
|
26
|
+
// zone once tasks appear. We therefore make expanded frames non-draggable (the
|
|
27
|
+
// pane pans straight through them) and move them via their header handle
|
|
28
|
+
// instead — collapsed chips stay node-draggable since they're small.
|
|
29
|
+
function frameExpanded(id: string) {
|
|
30
|
+
return ui.isFrameExpanded(id) && ui.lod !== 'far'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const nodes = computed(() =>
|
|
34
|
+
board.frames.map((b) => ({
|
|
35
|
+
id: b.id,
|
|
36
|
+
type: 'block',
|
|
37
|
+
position: b.position,
|
|
38
|
+
draggable: !frameExpanded(b.id),
|
|
39
|
+
data: {},
|
|
40
|
+
})),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
onNodeDragStop(({ node }) => {
|
|
44
|
+
board.moveBlock(node.id, { x: node.position.x, y: node.position.y })
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
onViewportChange((vp) => {
|
|
48
|
+
ui.zoom = vp.zoom
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
function onNodeClick({ node }: NodeMouseEvent) {
|
|
52
|
+
ui.select(node.id)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function onNodeDoubleClick({ node }: NodeMouseEvent) {
|
|
56
|
+
// Frames are containers: double-click expands to reveal their tasks.
|
|
57
|
+
ui.toggleFrame(node.id)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function onPaneClick() {
|
|
61
|
+
ui.select(null)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function minimapColor(node: { id: string }) {
|
|
65
|
+
const b = board.getBlock(node.id)
|
|
66
|
+
return b ? STATUS_META[board.frameStatus(b.id)].color : '#475569'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---- palette drag & drop onto the canvas ----------------------------------
|
|
70
|
+
function onDragOver(event: DragEvent) {
|
|
71
|
+
event.preventDefault()
|
|
72
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function onDrop(event: DragEvent) {
|
|
76
|
+
event.preventDefault()
|
|
77
|
+
const payload = readDndPayload(event)
|
|
78
|
+
if (!payload) return
|
|
79
|
+
|
|
80
|
+
if (payload.kind === 'block') {
|
|
81
|
+
const position = screenToFlowCoordinate({ x: event.clientX, y: event.clientY })
|
|
82
|
+
try {
|
|
83
|
+
const block = await board.addBlock(payload.blockType, position)
|
|
84
|
+
ui.select(block.id)
|
|
85
|
+
} catch {
|
|
86
|
+
toast.add({
|
|
87
|
+
title: 'Could not add block',
|
|
88
|
+
description: 'The backend rejected the request.',
|
|
89
|
+
color: 'error',
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (payload.kind === 'pipeline') {
|
|
96
|
+
// Pipelines run against tasks, not frames. The nearest [data-block-id] under
|
|
97
|
+
// the cursor is the task card when dropped inside an expanded frame.
|
|
98
|
+
const blockId = blockIdFromEvent(event)
|
|
99
|
+
const target = blockId ? board.getBlock(blockId) : undefined
|
|
100
|
+
const pipeline = pipelines.getPipeline(payload.pipelineId)
|
|
101
|
+
if (!target || !pipeline) return
|
|
102
|
+
if (target.level !== 'task') {
|
|
103
|
+
toast.add({
|
|
104
|
+
title: 'Drop onto a task',
|
|
105
|
+
description: 'Pipelines run against tasks, not services.',
|
|
106
|
+
})
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
if (!board.isRunnable(target.id)) {
|
|
110
|
+
toast.add({ title: 'Task is blocked', description: 'Its dependencies haven’t merged yet.' })
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
execution.start(target.id, pipeline)
|
|
114
|
+
ui.select(target.id)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
</script>
|
|
118
|
+
|
|
119
|
+
<template>
|
|
120
|
+
<div class="relative h-full w-full" @drop="onDrop" @dragover="onDragOver">
|
|
121
|
+
<VueFlow
|
|
122
|
+
:id="BOARD_FLOW_ID"
|
|
123
|
+
:nodes="nodes"
|
|
124
|
+
:min-zoom="0.2"
|
|
125
|
+
:max-zoom="2.5"
|
|
126
|
+
:default-viewport="{ x: 40, y: 20, zoom: 0.85 }"
|
|
127
|
+
:pan-on-drag="[0, 2]"
|
|
128
|
+
fit-view-on-init
|
|
129
|
+
@node-click="onNodeClick"
|
|
130
|
+
@node-double-click="onNodeDoubleClick"
|
|
131
|
+
@pane-click="onPaneClick"
|
|
132
|
+
@contextmenu.prevent
|
|
133
|
+
>
|
|
134
|
+
<Background pattern-color="#1e293b" :gap="22" :size="1.4" />
|
|
135
|
+
<MiniMap pannable zoomable :node-color="minimapColor" class="!bg-slate-900/80" />
|
|
136
|
+
<Controls position="bottom-left" />
|
|
137
|
+
|
|
138
|
+
<template #node-block="props">
|
|
139
|
+
<BlockNode :id="props.id" />
|
|
140
|
+
</template>
|
|
141
|
+
</VueFlow>
|
|
142
|
+
|
|
143
|
+
<!-- task dependency arrows, overlaid in screen space -->
|
|
144
|
+
<TaskDependencyEdges />
|
|
145
|
+
</div>
|
|
146
|
+
</template>
|