@cat-factory/app 1.0.0

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