@cat-factory/app 0.79.1 → 0.80.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/app/components/board/BoardCanvas.vue +7 -1
- package/app/components/bootstrap/BootstrapModal.vue +18 -0
- package/app/components/documents/TaskContextDocs.vue +10 -7
- package/app/components/fragments/FragmentLibraryManager.vue +2 -10
- package/app/components/github/AddServiceFromRepoModal.vue +8 -1
- package/app/components/panels/InspectorPanel.vue +35 -18
- package/app/components/panels/inspector/ContainerSummary.vue +8 -2
- package/app/components/panels/inspector/EpicChildren.vue +9 -7
- package/app/components/panels/inspector/FrontendConfig.vue +175 -248
- package/app/components/panels/inspector/InspectorSection.vue +60 -0
- package/app/components/panels/inspector/ServiceConnections.vue +10 -10
- package/app/components/panels/inspector/ServiceFragments.vue +14 -9
- package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +8 -7
- package/app/components/panels/inspector/ServiceTestConfig.vue +45 -57
- package/app/components/panels/inspector/TaskAgentConfig.vue +8 -5
- package/app/components/panels/inspector/TaskDependencies.vue +12 -8
- package/app/components/panels/inspector/TaskEstimateBadge.vue +9 -8
- package/app/components/panels/inspector/TaskExecution.vue +9 -2
- package/app/components/panels/inspector/TaskRunSettings.vue +13 -2
- package/app/components/panels/inspector/TaskStructure.vue +9 -2
- package/app/components/tasks/TaskContextIssues.vue +10 -7
- package/app/composables/useFramePlacement.ts +91 -0
- package/app/stores/board.ts +7 -1
- package/app/stores/fragmentLibrary.ts +4 -4
- package/app/utils/framePlacement.spec.ts +73 -0
- package/app/utils/framePlacement.ts +103 -0
- package/i18n/locales/en.json +25 -3
- package/i18n/locales/es.json +25 -3
- package/i18n/locales/fr.json +25 -3
- package/i18n/locales/he.json +25 -3
- package/i18n/locales/ja.json +25 -3
- package/i18n/locales/pl.json +25 -3
- package/i18n/locales/tr.json +25 -3
- package/i18n/locales/uk.json +25 -3
- package/package.json +2 -2
|
@@ -10,6 +10,7 @@ import { BOARD_FLOW_ID } from '~/composables/useBoardFlow'
|
|
|
10
10
|
import { useTaskExpansion } from '~/composables/useTaskExpansion'
|
|
11
11
|
import { useBlockDrag } from '~/composables/useBlockDrag'
|
|
12
12
|
import { useFrameStacking } from '~/composables/useFrameStacking'
|
|
13
|
+
import { useFramePlacement } from '~/composables/useFramePlacement'
|
|
13
14
|
import { useViewport } from '~/composables/useViewport'
|
|
14
15
|
import { boardPanMode } from '~/utils/boardPanMode'
|
|
15
16
|
|
|
@@ -24,6 +25,7 @@ const { t } = useI18n()
|
|
|
24
25
|
const { onNodeDragStop, onViewportChange, screenToFlowCoordinate } = useVueFlow(BOARD_FLOW_ID)
|
|
25
26
|
const { draggingId } = useBlockDrag()
|
|
26
27
|
const { hoveredFrameId } = useFrameStacking()
|
|
28
|
+
const { freeFramePosition, focusFrame } = useFramePlacement()
|
|
27
29
|
// Touch drives the canvas gestures: a touch-capable surface needs one-finger pan.
|
|
28
30
|
// We gate on `hasTouch` (any-pointer: coarse), not `isTouch` (the *primary* pointer),
|
|
29
31
|
// so a touchscreen laptop / 2-in-1 — whose primary pointer is the trackpad — still
|
|
@@ -121,10 +123,14 @@ async function onDrop(event: DragEvent) {
|
|
|
121
123
|
if (!payload) return
|
|
122
124
|
|
|
123
125
|
if (payload.kind === 'block') {
|
|
124
|
-
|
|
126
|
+
// Drop where the cursor is, but nudge off any existing frame it lands on so a new
|
|
127
|
+
// frame never overlaps a neighbour; then centre the camera on it.
|
|
128
|
+
const dropped = screenToFlowCoordinate({ x: event.clientX, y: event.clientY })
|
|
129
|
+
const position = freeFramePosition({ near: dropped })
|
|
125
130
|
try {
|
|
126
131
|
const block = await board.addBlock(payload.blockType, position)
|
|
127
132
|
ui.select(block.id)
|
|
133
|
+
await focusFrame(block.id)
|
|
128
134
|
} catch {
|
|
129
135
|
toast.add({
|
|
130
136
|
title: t('board.canvas.addBlockFailedTitle'),
|
|
@@ -13,7 +13,9 @@ const ui = useUiStore()
|
|
|
13
13
|
const bootstrap = useBootstrapStore()
|
|
14
14
|
const agentRuns = useAgentRunsStore()
|
|
15
15
|
const github = useGitHubStore()
|
|
16
|
+
const board = useBoardStore()
|
|
16
17
|
const toast = useToast()
|
|
18
|
+
const { freeFramePosition, focusFrame } = useFramePlacement()
|
|
17
19
|
const { t } = useI18n()
|
|
18
20
|
const { confirmAction, toastDone } = useConfirmAction()
|
|
19
21
|
|
|
@@ -242,6 +244,22 @@ async function launch() {
|
|
|
242
244
|
instructions.value = ''
|
|
243
245
|
// Reset the repo role too, so a later bootstrap doesn't silently inherit this one's type.
|
|
244
246
|
selectedType.value = 'service'
|
|
247
|
+
// The provisional frame arrived (bootstrap() refreshed the board). Re-home it to
|
|
248
|
+
// free space so it never overlaps an existing service — the backend places it on a
|
|
249
|
+
// fixed diagonal stagger that can land on top of a large neighbour — then centre the
|
|
250
|
+
// camera on it. Best-effort: the run has already started, so a placement hiccup must
|
|
251
|
+
// NOT surface a bootstrap-failed toast or leave the dialog open — swallow it here
|
|
252
|
+
// rather than letting it reach the outer catch.
|
|
253
|
+
if (job.blockId && board.getBlock(job.blockId)) {
|
|
254
|
+
const id = job.blockId
|
|
255
|
+
try {
|
|
256
|
+
const position = freeFramePosition({ size: board.containerSize(id), exclude: id })
|
|
257
|
+
await board.moveBlock(id, position)
|
|
258
|
+
await focusFrame(id)
|
|
259
|
+
} catch {
|
|
260
|
+
// Placement is cosmetic; the run is tracked on the board regardless.
|
|
261
|
+
}
|
|
262
|
+
}
|
|
245
263
|
// The run is now tracked on the board, so get out of the way: close the
|
|
246
264
|
// dialog as soon as bootstrapping has actually started.
|
|
247
265
|
ui.closeBootstrap()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
3
3
|
import type { Block, DocumentSourceKind } from '~/types/domain'
|
|
4
|
+
import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
|
|
4
5
|
|
|
5
6
|
// Documents (from any source) attached to a task as agent context, shown inside
|
|
6
7
|
// the InspectorPanel. Linked docs are fed to agents during execution (see the
|
|
@@ -51,17 +52,19 @@ const attachMenu = computed<DropdownMenuItem[][]>(() => {
|
|
|
51
52
|
</script>
|
|
52
53
|
|
|
53
54
|
<template>
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
<InspectorSection
|
|
56
|
+
v-if="documents.available"
|
|
57
|
+
:title="t('documents.taskDocs.heading')"
|
|
58
|
+
:hint="t('documents.taskDocs.hint')"
|
|
59
|
+
:count="linked.length"
|
|
60
|
+
>
|
|
61
|
+
<template #actions>
|
|
59
62
|
<UDropdownMenu :items="attachMenu" :content="{ side: 'bottom', align: 'end' }">
|
|
60
63
|
<UButton color="neutral" variant="soft" size="xs" icon="i-lucide-plus">{{
|
|
61
64
|
t('documents.taskDocs.attach')
|
|
62
65
|
}}</UButton>
|
|
63
66
|
</UDropdownMenu>
|
|
64
|
-
</
|
|
67
|
+
</template>
|
|
65
68
|
|
|
66
69
|
<div v-if="linked.length" class="space-y-1">
|
|
67
70
|
<a
|
|
@@ -82,5 +85,5 @@ const attachMenu = computed<DropdownMenuItem[][]>(() => {
|
|
|
82
85
|
<p v-else class="text-[11px] text-slate-500">
|
|
83
86
|
{{ t('documents.taskDocs.empty') }}
|
|
84
87
|
</p>
|
|
85
|
-
</
|
|
88
|
+
</InspectorSection>
|
|
86
89
|
</template>
|
|
@@ -298,9 +298,7 @@ async function checkSource(id: string) {
|
|
|
298
298
|
try {
|
|
299
299
|
const status = await library.checkSource(id)
|
|
300
300
|
toast.add({
|
|
301
|
-
title: status.changed
|
|
302
|
-
? t('fragments.toast.changesAvailable', { count: status.changedCount }, status.changedCount)
|
|
303
|
-
: t('fragments.toast.upToDate'),
|
|
301
|
+
title: status.changed ? t('fragments.toast.changesAvailable') : t('fragments.toast.upToDate'),
|
|
304
302
|
icon: status.changed ? 'i-lucide-bell-dot' : 'i-lucide-check',
|
|
305
303
|
})
|
|
306
304
|
} catch (e) {
|
|
@@ -585,13 +583,7 @@ async function unlinkSource(id: string) {
|
|
|
585
583
|
variant="subtle"
|
|
586
584
|
class="ms-auto"
|
|
587
585
|
>
|
|
588
|
-
{{
|
|
589
|
-
t(
|
|
590
|
-
'fragments.sources.changes',
|
|
591
|
-
{ count: library.sourceChanges[s.id] },
|
|
592
|
-
library.sourceChanges[s.id] ?? 0,
|
|
593
|
-
)
|
|
594
|
-
}}
|
|
586
|
+
{{ t('fragments.sources.changes') }}
|
|
595
587
|
</UBadge>
|
|
596
588
|
<div class="ms-auto flex gap-1">
|
|
597
589
|
<UButton
|
|
@@ -28,6 +28,7 @@ const ui = useUiStore()
|
|
|
28
28
|
const github = useGitHubStore()
|
|
29
29
|
const board = useBoardStore()
|
|
30
30
|
const toast = useToast()
|
|
31
|
+
const { freeFramePosition, focusFrame } = useFramePlacement()
|
|
31
32
|
|
|
32
33
|
const open = computed({
|
|
33
34
|
get: () => ui.addServiceOpen,
|
|
@@ -218,9 +219,14 @@ async function add() {
|
|
|
218
219
|
directory: isMonorepo.value ? selectedDirectory.value : undefined,
|
|
219
220
|
isMonorepo: isMonorepo.value,
|
|
220
221
|
type: selectedType.value,
|
|
222
|
+
// Place the imported frame in free space (centred in view) instead of the
|
|
223
|
+
// backend's default stagger, so it never overlaps an existing service.
|
|
224
|
+
position: freeFramePosition(),
|
|
221
225
|
})
|
|
222
226
|
// Refresh the projection so the new repo↔block link is reflected locally.
|
|
223
227
|
await github.load()
|
|
228
|
+
// Centre the camera on the newly imported service.
|
|
229
|
+
await focusFrame(block.id)
|
|
224
230
|
configuredBlockId.value = block.id
|
|
225
231
|
configuredDirectory.value = isMonorepo.value ? selectedDirectory.value : undefined
|
|
226
232
|
toast.add({
|
|
@@ -364,8 +370,9 @@ function done() {
|
|
|
364
370
|
<ServiceTestConfig
|
|
365
371
|
:block="configuredBlock"
|
|
366
372
|
:repo="{ githubId: selectedRepoId!, directory: configuredDirectory }"
|
|
373
|
+
default-open
|
|
367
374
|
/>
|
|
368
|
-
<ServiceFragments :block="configuredBlock" />
|
|
375
|
+
<ServiceFragments :block="configuredBlock" default-open />
|
|
369
376
|
</div>
|
|
370
377
|
|
|
371
378
|
<div class="flex flex-wrap items-center gap-2">
|
|
@@ -442,10 +442,10 @@ const showOriginalDescription = ref(false)
|
|
|
442
442
|
</div>
|
|
443
443
|
|
|
444
444
|
<!-- task: context documents -->
|
|
445
|
-
<TaskContextDocs v-if="isTask" :block="block" />
|
|
445
|
+
<TaskContextDocs v-if="isTask" :key="`context-docs-${block.id}`" :block="block" />
|
|
446
446
|
|
|
447
447
|
<!-- task: context issues (tracker) -->
|
|
448
|
-
<TaskContextIssues v-if="isTask" :block="block" />
|
|
448
|
+
<TaskContextIssues v-if="isTask" :key="`context-issues-${block.id}`" :block="block" />
|
|
449
449
|
|
|
450
450
|
<!-- service (frame): navigate the prescriptive spec tree (+ Gherkin scenarios when
|
|
451
451
|
the spec is on the repo's default branch) -->
|
|
@@ -462,35 +462,52 @@ const showOriginalDescription = ref(false)
|
|
|
462
462
|
</UButton>
|
|
463
463
|
|
|
464
464
|
<!-- service / module: tasks summary -->
|
|
465
|
-
<ContainerSummary v-if="isContainer" :block="block" />
|
|
465
|
+
<ContainerSummary v-if="isContainer" :key="`container-${block.id}`" :block="block" />
|
|
466
466
|
<!-- frontend (frame): build/serve/mock config + backend bindings (board links) -->
|
|
467
|
-
<FrontendConfig
|
|
467
|
+
<FrontendConfig
|
|
468
|
+
v-if="isFrame && block.type === 'frontend'"
|
|
469
|
+
:key="`frontend-${block.id}`"
|
|
470
|
+
:block="block"
|
|
471
|
+
/>
|
|
468
472
|
|
|
469
473
|
<!-- service (frame): directed connections to the other services it uses (board links) -->
|
|
470
|
-
<ServiceConnections
|
|
474
|
+
<ServiceConnections
|
|
475
|
+
v-if="isFrame && block.type === 'service'"
|
|
476
|
+
:key="`connections-${block.id}`"
|
|
477
|
+
:block="block"
|
|
478
|
+
/>
|
|
471
479
|
|
|
472
480
|
<!-- service (frame): test infra + provisioning configuration -->
|
|
473
|
-
<ServiceTestConfig v-if="isFrame" :block="block" />
|
|
481
|
+
<ServiceTestConfig v-if="isFrame" :key="`test-config-${block.id}`" :block="block" />
|
|
474
482
|
|
|
475
483
|
<!-- service (frame): best-practice fragments for code-aware agents -->
|
|
476
|
-
<ServiceFragments v-if="isFrame" :block="block" />
|
|
484
|
+
<ServiceFragments v-if="isFrame" :key="`fragments-${block.id}`" :block="block" />
|
|
477
485
|
|
|
478
486
|
<!-- service (frame): post-release-health monitor/SLO mapping -->
|
|
479
|
-
<ServiceReleaseHealthConfig
|
|
480
|
-
|
|
481
|
-
|
|
487
|
+
<ServiceReleaseHealthConfig
|
|
488
|
+
v-if="isFrame"
|
|
489
|
+
:key="`release-health-${block.id}`"
|
|
490
|
+
:block="block"
|
|
491
|
+
/>
|
|
492
|
+
|
|
493
|
+
<!-- task: the live execution surface first (open by default), then the estimate,
|
|
494
|
+
then the collapsed configuration sections (dependencies, run settings, agent
|
|
495
|
+
config, structure) so a running task reads top-down without scrolling. -->
|
|
496
|
+
<!-- Keyed by block id so a manual collapse/expand doesn't leak across task
|
|
497
|
+
selections: switching tasks re-mounts each section back to its default state
|
|
498
|
+
(e.g. the live Execution section is open again for the newly selected task). -->
|
|
482
499
|
<template v-else-if="isTask">
|
|
483
|
-
<RecurringScheduleSettings :block="block" />
|
|
484
|
-
<
|
|
485
|
-
<
|
|
486
|
-
<
|
|
487
|
-
<
|
|
488
|
-
<
|
|
489
|
-
<
|
|
500
|
+
<RecurringScheduleSettings :key="`schedule-${block.id}`" :block="block" />
|
|
501
|
+
<TaskExecution :key="`execution-${block.id}`" :block="block" />
|
|
502
|
+
<TaskEstimateBadge :key="`estimate-${block.id}`" :block="block" />
|
|
503
|
+
<TaskDependencies :key="`deps-${block.id}`" :block="block" />
|
|
504
|
+
<TaskRunSettings :key="`run-settings-${block.id}`" :block="block" />
|
|
505
|
+
<TaskAgentConfig :key="`agent-config-${block.id}`" :block="block" />
|
|
506
|
+
<TaskStructure :key="`structure-${block.id}`" :block="block" />
|
|
490
507
|
</template>
|
|
491
508
|
|
|
492
509
|
<!-- epic: the full tree of member tasks, grouped by service → module -->
|
|
493
|
-
<EpicChildren v-else-if="isEpic" :block="block" />
|
|
510
|
+
<EpicChildren v-else-if="isEpic" :key="`epic-${block.id}`" :block="block" />
|
|
494
511
|
|
|
495
512
|
<!-- actions -->
|
|
496
513
|
<div class="flex items-center gap-2">
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { Block } from '~/types/domain'
|
|
3
3
|
import { STATUS_META } from '~/utils/catalog'
|
|
4
|
+
import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{ block: Block }>()
|
|
6
7
|
|
|
@@ -20,7 +21,12 @@ function addTask() {
|
|
|
20
21
|
</script>
|
|
21
22
|
|
|
22
23
|
<template>
|
|
23
|
-
<
|
|
24
|
+
<InspectorSection
|
|
25
|
+
:title="t('inspector.container.title')"
|
|
26
|
+
:hint="t('inspector.container.hint')"
|
|
27
|
+
:count="tasks.length"
|
|
28
|
+
default-open
|
|
29
|
+
>
|
|
24
30
|
<!-- modules (services only) -->
|
|
25
31
|
<div v-if="modules.length">
|
|
26
32
|
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
@@ -81,5 +87,5 @@ function addTask() {
|
|
|
81
87
|
<p v-if="isFrame" class="text-[11px] text-slate-500">
|
|
82
88
|
{{ t('inspector.container.servicesHint') }}
|
|
83
89
|
</p>
|
|
84
|
-
</
|
|
90
|
+
</InspectorSection>
|
|
85
91
|
</template>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { computed } from 'vue'
|
|
3
3
|
import type { Block } from '~/types/domain'
|
|
4
4
|
import { STATUS_META } from '~/utils/catalog'
|
|
5
|
+
import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
|
|
5
6
|
|
|
6
7
|
// The epic inspector body: the full tree of member tasks (which may live under different
|
|
7
8
|
// services/modules), grouped service → module → task. Each task row selects it. Membership
|
|
@@ -45,15 +46,16 @@ const groups = computed(() => {
|
|
|
45
46
|
</script>
|
|
46
47
|
|
|
47
48
|
<template>
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
<InspectorSection
|
|
50
|
+
:title="t('inspector.epicChildren.title')"
|
|
51
|
+
:hint="t('inspector.epicChildren.hint')"
|
|
52
|
+
default-open
|
|
53
|
+
>
|
|
54
|
+
<template #actions>
|
|
53
55
|
<span class="text-[11px] text-slate-500">{{
|
|
54
56
|
t('inspector.epicChildren.doneCount', { done, total: members.length })
|
|
55
57
|
}}</span>
|
|
56
|
-
</
|
|
58
|
+
</template>
|
|
57
59
|
|
|
58
60
|
<div v-if="members.length === 0" class="text-[11px] text-slate-500">
|
|
59
61
|
{{ t('inspector.epicChildren.empty') }}
|
|
@@ -92,5 +94,5 @@ const groups = computed(() => {
|
|
|
92
94
|
</div>
|
|
93
95
|
</div>
|
|
94
96
|
</div>
|
|
95
|
-
</
|
|
97
|
+
</InspectorSection>
|
|
96
98
|
</template>
|