@cat-factory/app 0.81.0 → 0.82.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/CreateInitiativeModal.vue +113 -0
- package/app/components/board/nodes/BlockNode.vue +21 -1
- package/app/components/board/nodes/InitiativeCard.vue +107 -0
- package/app/components/initiative/InitiativeTrackerWindow.vue +279 -0
- package/app/components/panels/InspectorPanel.vue +5 -0
- package/app/components/panels/StepResultViewHost.vue +5 -0
- package/app/components/panels/inspector/InitiativeInspector.vue +78 -0
- package/app/composables/api/initiative.ts +28 -0
- package/app/composables/useApi.ts +2 -0
- package/app/composables/useBlockQueries.ts +22 -3
- package/app/composables/useWorkspaceStream.ts +5 -0
- package/app/pages/index.vue +1 -0
- package/app/stores/board.ts +20 -6
- package/app/stores/initiative.ts +98 -0
- package/app/stores/ui.ts +18 -0
- package/app/stores/workspace.ts +3 -0
- package/app/types/domain.ts +1 -0
- package/app/types/initiative.ts +18 -0
- package/app/utils/catalog.ts +24 -0
- package/app/utils/initiative.ts +57 -0
- package/i18n/locales/en.json +67 -1
- package/i18n/locales/es.json +67 -1
- package/i18n/locales/fr.json +67 -1
- package/i18n/locales/he.json +67 -1
- package/i18n/locales/ja.json +67 -1
- package/i18n/locales/pl.json +67 -1
- package/i18n/locales/tr.json +67 -1
- package/i18n/locales/uk.json +67 -1
- package/package.json +2 -2
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Create a new INITIATIVE under a service frame — the longer-running counterpart
|
|
3
|
+
// to a task. The user names it and writes the rough goal statement; the server
|
|
4
|
+
// materialises the initiative-level board block + its empty tracker entity in one
|
|
5
|
+
// call. Nothing is planned here: the user then runs the Initiative Planning
|
|
6
|
+
// pipeline (pl_initiative) on the block, which analyses the codebase, drafts the
|
|
7
|
+
// multi-phase plan for approval, and commits the in-repo tracker.
|
|
8
|
+
const ui = useUiStore()
|
|
9
|
+
const board = useBoardStore()
|
|
10
|
+
const initiatives = useInitiativesStore()
|
|
11
|
+
const toast = useToast()
|
|
12
|
+
const { t } = useI18n()
|
|
13
|
+
|
|
14
|
+
const open = computed({
|
|
15
|
+
get: () => ui.createInitiativeFrameId !== null,
|
|
16
|
+
set: (v: boolean) => {
|
|
17
|
+
if (!v) ui.closeCreateInitiative()
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const frame = computed(() =>
|
|
22
|
+
ui.createInitiativeFrameId ? board.getBlock(ui.createInitiativeFrameId) : undefined,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const title = ref('')
|
|
26
|
+
const description = ref('')
|
|
27
|
+
|
|
28
|
+
watch(open, (o) => {
|
|
29
|
+
if (o) {
|
|
30
|
+
title.value = ''
|
|
31
|
+
description.value = ''
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
async function create() {
|
|
36
|
+
const frameId = ui.createInitiativeFrameId
|
|
37
|
+
if (!frameId || !title.value.trim() || initiatives.creating) return
|
|
38
|
+
try {
|
|
39
|
+
const { block } = await initiatives.create(frameId, {
|
|
40
|
+
title: title.value.trim(),
|
|
41
|
+
description: description.value.trim() || undefined,
|
|
42
|
+
})
|
|
43
|
+
ui.closeCreateInitiative()
|
|
44
|
+
// Select the fresh block so the inspector offers "Run planning" right away.
|
|
45
|
+
ui.select(block.id)
|
|
46
|
+
} catch (e) {
|
|
47
|
+
toast.add({
|
|
48
|
+
title: t('initiative.create.failedTitle'),
|
|
49
|
+
description: e instanceof Error ? e.message : String(e),
|
|
50
|
+
icon: 'i-lucide-triangle-alert',
|
|
51
|
+
color: 'error',
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<template>
|
|
58
|
+
<UModal v-model:open="open" :title="t('initiative.create.title')">
|
|
59
|
+
<template #body>
|
|
60
|
+
<div class="space-y-4" data-testid="create-initiative-modal">
|
|
61
|
+
<p v-if="frame" class="text-xs text-slate-400">
|
|
62
|
+
<i18n-t keypath="initiative.create.inFrame" tag="span" scope="global">
|
|
63
|
+
<template #frame>
|
|
64
|
+
<span class="font-medium text-slate-200">{{ frame.title }}</span>
|
|
65
|
+
</template>
|
|
66
|
+
</i18n-t>
|
|
67
|
+
</p>
|
|
68
|
+
|
|
69
|
+
<UFormField :label="t('initiative.create.titleField')" required>
|
|
70
|
+
<UInput
|
|
71
|
+
v-model="title"
|
|
72
|
+
data-testid="create-initiative-title"
|
|
73
|
+
:placeholder="t('initiative.create.titlePlaceholder')"
|
|
74
|
+
autofocus
|
|
75
|
+
class="w-full"
|
|
76
|
+
@keydown.enter="create"
|
|
77
|
+
/>
|
|
78
|
+
</UFormField>
|
|
79
|
+
|
|
80
|
+
<UFormField :label="t('initiative.create.goalField')">
|
|
81
|
+
<UTextarea
|
|
82
|
+
v-model="description"
|
|
83
|
+
data-testid="create-initiative-goal"
|
|
84
|
+
:rows="4"
|
|
85
|
+
autoresize
|
|
86
|
+
:placeholder="t('initiative.create.goalPlaceholder')"
|
|
87
|
+
class="w-full"
|
|
88
|
+
/>
|
|
89
|
+
</UFormField>
|
|
90
|
+
|
|
91
|
+
<p class="text-[11px] text-slate-500">
|
|
92
|
+
{{ t('initiative.create.hint') }}
|
|
93
|
+
</p>
|
|
94
|
+
</div>
|
|
95
|
+
</template>
|
|
96
|
+
<template #footer>
|
|
97
|
+
<div class="flex w-full justify-end gap-2">
|
|
98
|
+
<UButton color="neutral" variant="ghost" @click="open = false">
|
|
99
|
+
{{ t('common.cancel') }}
|
|
100
|
+
</UButton>
|
|
101
|
+
<UButton
|
|
102
|
+
data-testid="create-initiative-submit"
|
|
103
|
+
color="primary"
|
|
104
|
+
:loading="initiatives.creating"
|
|
105
|
+
:disabled="!title.trim()"
|
|
106
|
+
@click="create"
|
|
107
|
+
>
|
|
108
|
+
{{ t('initiative.create.submit') }}
|
|
109
|
+
</UButton>
|
|
110
|
+
</div>
|
|
111
|
+
</template>
|
|
112
|
+
</UModal>
|
|
113
|
+
</template>
|
|
@@ -3,6 +3,7 @@ import type { Block, BlockStatus } from '~/types/domain'
|
|
|
3
3
|
import { blockTypeMeta, STATUS_META } from '~/utils/catalog'
|
|
4
4
|
import DecisionBadge from './DecisionBadge.vue'
|
|
5
5
|
import DraggableTask from './DraggableTask.vue'
|
|
6
|
+
import InitiativeCard from './InitiativeCard.vue'
|
|
6
7
|
import ModuleFrame from './ModuleFrame.vue'
|
|
7
8
|
import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
|
|
8
9
|
import AgentStopButton from '~/components/board/AgentStopButton.vue'
|
|
@@ -36,10 +37,13 @@ const typeMeta = computed(() => (block.value ? blockTypeMeta(block.value.type) :
|
|
|
36
37
|
// ---- this service's children (tasks + modules) -----------------------------
|
|
37
38
|
const directTasks = computed(() => board.tasksOf(props.id))
|
|
38
39
|
const modules = computed(() => board.modulesOf(props.id))
|
|
40
|
+
const initiativeBlocks = computed(() => board.initiativesOf(props.id))
|
|
39
41
|
const allTasks = computed(() => board.allTasksUnder(props.id))
|
|
40
42
|
const taskIds = computed(() => new Set(allTasks.value.map((t) => t.id)))
|
|
41
43
|
const taskCount = computed(() => allTasks.value.length)
|
|
42
|
-
const hasTasks = computed(
|
|
44
|
+
const hasTasks = computed(
|
|
45
|
+
() => taskCount.value > 0 || modules.value.length > 0 || initiativeBlocks.value.length > 0,
|
|
46
|
+
)
|
|
43
47
|
// Single pass over the tasks for both rollups (vs. one filter each).
|
|
44
48
|
const taskStats = computed(() => {
|
|
45
49
|
let merged = 0
|
|
@@ -156,6 +160,11 @@ function addRecurring() {
|
|
|
156
160
|
ui.openAddRecurring(props.id)
|
|
157
161
|
}
|
|
158
162
|
|
|
163
|
+
function createInitiative() {
|
|
164
|
+
ui.expandFrame(props.id)
|
|
165
|
+
ui.openCreateInitiative(props.id)
|
|
166
|
+
}
|
|
167
|
+
|
|
159
168
|
// A task needs merging → green pulse; a task needs a decision → amber pulse.
|
|
160
169
|
const pulseClass = computed(() => {
|
|
161
170
|
if (frameStatus.value === 'blocked') return 'board-pulse'
|
|
@@ -462,6 +471,16 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
462
471
|
:title="t('board.frame.addRecurringTitle')"
|
|
463
472
|
@click.stop="addRecurring"
|
|
464
473
|
/>
|
|
474
|
+
<UButton
|
|
475
|
+
class="nodrag"
|
|
476
|
+
data-testid="frame-add-initiative"
|
|
477
|
+
:size="isTouch ? 'sm' : 'xs'"
|
|
478
|
+
variant="ghost"
|
|
479
|
+
color="neutral"
|
|
480
|
+
icon="i-lucide-milestone"
|
|
481
|
+
:title="t('board.frame.createInitiativeTitle')"
|
|
482
|
+
@click.stop="createInitiative"
|
|
483
|
+
/>
|
|
465
484
|
<UButton
|
|
466
485
|
class="nodrag"
|
|
467
486
|
:size="isTouch ? 'sm' : 'xs'"
|
|
@@ -494,6 +513,7 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
494
513
|
:style="{ width: canvas.w + 'px', height: canvas.h + 'px' }"
|
|
495
514
|
>
|
|
496
515
|
<ModuleFrame v-for="m in modules" :key="m.id" :module-id="m.id" />
|
|
516
|
+
<InitiativeCard v-for="i in initiativeBlocks" :key="i.id" :block-id="i.id" />
|
|
497
517
|
<DraggableTask v-for="t in directTasks" :key="t.id" :task-id="t.id" />
|
|
498
518
|
<button
|
|
499
519
|
v-if="!hasTasks"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The board card for an `initiative`-level block (a frame child, like a module):
|
|
3
|
+
// title, the initiative's lifecycle status, and — once a plan is ingested — the
|
|
4
|
+
// item-completion progress. Clicking selects the block (the inspector offers
|
|
5
|
+
// "Run planning" / "Open tracker"); the tracker button opens the dedicated
|
|
6
|
+
// window directly. Draggable within its frame like a task card.
|
|
7
|
+
import type { InitiativeStatus } from '~/types/domain'
|
|
8
|
+
import { useBlockDrag } from '~/composables/useBlockDrag'
|
|
9
|
+
import {
|
|
10
|
+
INITIATIVE_STATUS_CHIPS,
|
|
11
|
+
INITIATIVE_STATUS_LABEL_KEYS,
|
|
12
|
+
initiativeProgress,
|
|
13
|
+
} from '~/utils/initiative'
|
|
14
|
+
|
|
15
|
+
const props = defineProps<{ blockId: string }>()
|
|
16
|
+
const board = useBoardStore()
|
|
17
|
+
const initiatives = useInitiativesStore()
|
|
18
|
+
const ui = useUiStore()
|
|
19
|
+
const { t } = useI18n()
|
|
20
|
+
const { draggingId, startDrag } = useBlockDrag()
|
|
21
|
+
|
|
22
|
+
const block = computed(() => board.getBlock(props.blockId))
|
|
23
|
+
const initiative = computed(() => initiatives.forBlock(props.blockId))
|
|
24
|
+
|
|
25
|
+
const status = computed<InitiativeStatus>(() => initiative.value?.status ?? 'planning')
|
|
26
|
+
const statusLabel = computed(() => t(INITIATIVE_STATUS_LABEL_KEYS[status.value]))
|
|
27
|
+
|
|
28
|
+
const progress = computed(() => initiativeProgress(initiative.value?.items))
|
|
29
|
+
|
|
30
|
+
const selected = computed(() => ui.selectedBlockId === props.blockId)
|
|
31
|
+
|
|
32
|
+
function select() {
|
|
33
|
+
ui.select(props.blockId)
|
|
34
|
+
}
|
|
35
|
+
function openTracker() {
|
|
36
|
+
ui.select(props.blockId)
|
|
37
|
+
ui.openInitiativeTracker(props.blockId)
|
|
38
|
+
}
|
|
39
|
+
function onHandle(e: PointerEvent) {
|
|
40
|
+
if (block.value) startDrag(block.value, e)
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<div
|
|
46
|
+
v-if="block"
|
|
47
|
+
class="absolute w-[230px]"
|
|
48
|
+
:style="{
|
|
49
|
+
left: block.position.x + 'px',
|
|
50
|
+
top: block.position.y + 'px',
|
|
51
|
+
zIndex: draggingId === blockId ? 60 : 10,
|
|
52
|
+
pointerEvents: draggingId === blockId ? 'none' : undefined,
|
|
53
|
+
}"
|
|
54
|
+
>
|
|
55
|
+
<div
|
|
56
|
+
class="nodrag nopan flex cursor-grab touch-none items-center justify-center rounded-t-lg border border-b-0 border-indigo-800/60 bg-indigo-950/60 py-px active:cursor-grabbing pointer-coarse:py-2"
|
|
57
|
+
:title="t('board.frame.dragTask')"
|
|
58
|
+
@pointerdown="onHandle"
|
|
59
|
+
>
|
|
60
|
+
<UIcon
|
|
61
|
+
name="i-lucide-grip-horizontal"
|
|
62
|
+
class="h-3 w-3 text-indigo-400/60 pointer-coarse:h-5 pointer-coarse:w-5"
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
<div
|
|
66
|
+
data-testid="initiative-card"
|
|
67
|
+
class="cursor-pointer rounded-b-lg border border-indigo-800/60 bg-indigo-950/40 p-3 transition hover:border-indigo-600"
|
|
68
|
+
:class="selected ? 'ring-2 ring-indigo-400/60' : ''"
|
|
69
|
+
@click.stop="select"
|
|
70
|
+
>
|
|
71
|
+
<div class="flex items-start justify-between gap-2">
|
|
72
|
+
<div class="flex items-center gap-2">
|
|
73
|
+
<UIcon name="i-lucide-milestone" class="h-4 w-4 shrink-0 text-indigo-400" />
|
|
74
|
+
<div class="text-xs font-semibold text-white">{{ block.title }}</div>
|
|
75
|
+
</div>
|
|
76
|
+
<UBadge :color="INITIATIVE_STATUS_CHIPS[status] as any" variant="subtle" size="sm">
|
|
77
|
+
{{ statusLabel }}
|
|
78
|
+
</UBadge>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="mt-1 text-[10px] uppercase tracking-wide text-indigo-300/70">
|
|
81
|
+
{{ t('initiative.card.kind') }}
|
|
82
|
+
</div>
|
|
83
|
+
<div v-if="progress" class="mt-2 space-y-1">
|
|
84
|
+
<div class="h-1.5 overflow-hidden rounded bg-slate-800">
|
|
85
|
+
<div
|
|
86
|
+
class="h-full rounded bg-indigo-400"
|
|
87
|
+
:style="{ width: `${Math.round((progress.settled / progress.total) * 100)}%` }"
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="text-[10px] text-slate-400">
|
|
91
|
+
{{ t('initiative.card.progress', { done: progress.settled, total: progress.total }) }}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
<UButton
|
|
95
|
+
class="nodrag mt-2"
|
|
96
|
+
data-testid="initiative-open-tracker"
|
|
97
|
+
size="xs"
|
|
98
|
+
variant="soft"
|
|
99
|
+
color="primary"
|
|
100
|
+
icon="i-lucide-list-checks"
|
|
101
|
+
@click.stop="openTracker"
|
|
102
|
+
>
|
|
103
|
+
{{ t('initiative.card.openTracker') }}
|
|
104
|
+
</UButton>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</template>
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The initiative tracker window — the dedicated read-only view of an initiative's
|
|
3
|
+
// plan/tracker entity: goal + constraints, the phases with their per-item status +
|
|
4
|
+
// PR links, the execution policy, and the decisions / deviations / follow-ups /
|
|
5
|
+
// caveats logs. Renders the DB entity (the source of truth) — never the in-repo
|
|
6
|
+
// mirror, which may not exist (GitHub-unwired workspaces). Opened via the universal
|
|
7
|
+
// result-view host: from the board card / inspector (`ui.openInitiativeTracker`) or
|
|
8
|
+
// as the planner step's result view. Live `initiative` stream events patch the
|
|
9
|
+
// store, so an open window follows the plan as it is ingested and later executed.
|
|
10
|
+
import { computed } from 'vue'
|
|
11
|
+
import type { InitiativeItem } from '~/types/domain'
|
|
12
|
+
import {
|
|
13
|
+
INITIATIVE_ITEM_STATUS_CHIPS,
|
|
14
|
+
INITIATIVE_ITEM_STATUS_LABEL_KEYS,
|
|
15
|
+
INITIATIVE_STATUS_LABEL_KEYS,
|
|
16
|
+
} from '~/utils/initiative'
|
|
17
|
+
|
|
18
|
+
const board = useBoardStore()
|
|
19
|
+
const initiatives = useInitiativesStore()
|
|
20
|
+
const { t } = useI18n()
|
|
21
|
+
|
|
22
|
+
const { open, blockId, close } = useResultView('initiative-tracker', {
|
|
23
|
+
onOpen: (id) => void initiatives.load(id),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
27
|
+
const initiative = computed(() => (blockId.value ? initiatives.forBlock(blockId.value) : null))
|
|
28
|
+
|
|
29
|
+
const phases = computed(() => initiative.value?.phases ?? [])
|
|
30
|
+
function itemsOf(phaseId: string): InitiativeItem[] {
|
|
31
|
+
return (initiative.value?.items ?? []).filter((i) => i.phaseId === phaseId)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const policyRules = computed(() => initiative.value?.policy?.rules ?? [])
|
|
35
|
+
function ruleAxes(rule: { minComplexity?: number; minRisk?: number; minImpact?: number }): string {
|
|
36
|
+
const axes = [
|
|
37
|
+
rule.minComplexity !== undefined
|
|
38
|
+
? t('initiative.tracker.axisComplexity', { value: rule.minComplexity })
|
|
39
|
+
: null,
|
|
40
|
+
rule.minRisk !== undefined ? t('initiative.tracker.axisRisk', { value: rule.minRisk }) : null,
|
|
41
|
+
rule.minImpact !== undefined
|
|
42
|
+
? t('initiative.tracker.axisImpact', { value: rule.minImpact })
|
|
43
|
+
: null,
|
|
44
|
+
].filter((a): a is string => a !== null)
|
|
45
|
+
return axes.length ? axes.join(' · ') : t('initiative.tracker.axisNever')
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<template>
|
|
50
|
+
<Teleport to="body">
|
|
51
|
+
<div
|
|
52
|
+
v-if="open"
|
|
53
|
+
class="fixed inset-0 z-50 flex max-h-[100dvh] items-stretch justify-center bg-slate-950/70 backdrop-blur-sm"
|
|
54
|
+
@click.self="close"
|
|
55
|
+
>
|
|
56
|
+
<div
|
|
57
|
+
class="m-4 flex w-full max-w-4xl flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl"
|
|
58
|
+
role="dialog"
|
|
59
|
+
aria-modal="true"
|
|
60
|
+
data-testid="initiative-tracker-window"
|
|
61
|
+
>
|
|
62
|
+
<!-- Header -->
|
|
63
|
+
<header class="flex items-center gap-3 border-b border-slate-800 px-5 py-3">
|
|
64
|
+
<span
|
|
65
|
+
class="flex h-8 w-8 items-center justify-center rounded-lg bg-indigo-500/15 text-indigo-300"
|
|
66
|
+
>
|
|
67
|
+
<UIcon name="i-lucide-milestone" class="h-4 w-4" />
|
|
68
|
+
</span>
|
|
69
|
+
<div class="min-w-0 flex-1">
|
|
70
|
+
<h2 class="truncate text-sm font-semibold text-slate-100">
|
|
71
|
+
{{ initiative?.title ?? block?.title ?? t('initiative.tracker.title') }}
|
|
72
|
+
</h2>
|
|
73
|
+
<p class="truncate text-[11px] text-slate-400">
|
|
74
|
+
{{ t('initiative.tracker.subtitle') }}
|
|
75
|
+
</p>
|
|
76
|
+
</div>
|
|
77
|
+
<UBadge v-if="initiative" color="primary" variant="subtle" size="sm">
|
|
78
|
+
{{ t(INITIATIVE_STATUS_LABEL_KEYS[initiative.status]) }}
|
|
79
|
+
</UBadge>
|
|
80
|
+
<button
|
|
81
|
+
class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
|
|
82
|
+
@click="close"
|
|
83
|
+
>
|
|
84
|
+
<UIcon name="i-lucide-x" class="h-4 w-4" />
|
|
85
|
+
</button>
|
|
86
|
+
</header>
|
|
87
|
+
|
|
88
|
+
<div class="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
|
89
|
+
<!-- No entity yet (module unwired / still creating) -->
|
|
90
|
+
<div
|
|
91
|
+
v-if="!initiative"
|
|
92
|
+
class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
|
|
93
|
+
>
|
|
94
|
+
<UIcon name="i-lucide-milestone" class="h-8 w-8 opacity-40" />
|
|
95
|
+
<p class="text-sm">{{ t('initiative.tracker.empty') }}</p>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<template v-else>
|
|
99
|
+
<!-- Goal & constraints -->
|
|
100
|
+
<section v-if="initiative.goal" class="mb-4">
|
|
101
|
+
<h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
102
|
+
{{ t('initiative.tracker.goal') }}
|
|
103
|
+
</h3>
|
|
104
|
+
<p class="whitespace-pre-wrap text-[13px] leading-relaxed text-slate-300">
|
|
105
|
+
{{ initiative.goal }}
|
|
106
|
+
</p>
|
|
107
|
+
</section>
|
|
108
|
+
<section v-if="initiative.constraints?.length" class="mb-4">
|
|
109
|
+
<h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
110
|
+
{{ t('initiative.tracker.constraints') }}
|
|
111
|
+
</h3>
|
|
112
|
+
<ul class="list-inside list-disc text-[13px] text-slate-300">
|
|
113
|
+
<li v-for="(c, i) in initiative.constraints" :key="i">{{ c }}</li>
|
|
114
|
+
</ul>
|
|
115
|
+
</section>
|
|
116
|
+
<section v-if="initiative.nonGoals?.length" class="mb-4">
|
|
117
|
+
<h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
118
|
+
{{ t('initiative.tracker.nonGoals') }}
|
|
119
|
+
</h3>
|
|
120
|
+
<ul class="list-inside list-disc text-[13px] text-slate-300">
|
|
121
|
+
<li v-for="(g, i) in initiative.nonGoals" :key="i">{{ g }}</li>
|
|
122
|
+
</ul>
|
|
123
|
+
</section>
|
|
124
|
+
<section v-if="initiative.analysisSummary" class="mb-4">
|
|
125
|
+
<h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
126
|
+
{{ t('initiative.tracker.analysis') }}
|
|
127
|
+
</h3>
|
|
128
|
+
<p class="whitespace-pre-wrap text-[13px] leading-relaxed text-slate-300">
|
|
129
|
+
{{ initiative.analysisSummary }}
|
|
130
|
+
</p>
|
|
131
|
+
</section>
|
|
132
|
+
|
|
133
|
+
<!-- Awaiting planning -->
|
|
134
|
+
<div
|
|
135
|
+
v-if="phases.length === 0"
|
|
136
|
+
class="mb-4 rounded-lg border border-dashed border-slate-700 p-4 text-center text-[12px] text-slate-400"
|
|
137
|
+
>
|
|
138
|
+
{{ t('initiative.tracker.noPlan') }}
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<!-- Phases + items -->
|
|
142
|
+
<section v-for="phase in phases" :key="phase.id" class="mb-5">
|
|
143
|
+
<h3 class="mb-1 text-sm font-semibold text-slate-200">
|
|
144
|
+
{{ t('initiative.tracker.phase', { title: phase.title }) }}
|
|
145
|
+
</h3>
|
|
146
|
+
<p v-if="phase.goal" class="mb-2 text-[12px] text-slate-400">{{ phase.goal }}</p>
|
|
147
|
+
<div class="overflow-x-auto rounded-lg border border-slate-800">
|
|
148
|
+
<table class="w-full text-[12px]">
|
|
149
|
+
<thead>
|
|
150
|
+
<tr class="border-b border-slate-800 text-left text-slate-500">
|
|
151
|
+
<th class="px-3 py-2 font-medium">{{ t('initiative.tracker.colItem') }}</th>
|
|
152
|
+
<th class="px-3 py-2 font-medium">{{ t('initiative.tracker.colStatus') }}</th>
|
|
153
|
+
<th class="px-3 py-2 font-medium">{{ t('initiative.tracker.colPr') }}</th>
|
|
154
|
+
</tr>
|
|
155
|
+
</thead>
|
|
156
|
+
<tbody>
|
|
157
|
+
<tr
|
|
158
|
+
v-for="item in itemsOf(phase.id)"
|
|
159
|
+
:key="item.id"
|
|
160
|
+
class="border-b border-slate-800/60 last:border-0"
|
|
161
|
+
>
|
|
162
|
+
<td class="px-3 py-2 align-top">
|
|
163
|
+
<div class="font-medium text-slate-200">{{ item.title }}</div>
|
|
164
|
+
<div
|
|
165
|
+
v-if="item.dependsOn?.length"
|
|
166
|
+
class="mt-0.5 text-[10px] text-slate-500"
|
|
167
|
+
>
|
|
168
|
+
{{
|
|
169
|
+
t('initiative.tracker.dependsOn', {
|
|
170
|
+
items: item.dependsOn.join(', '),
|
|
171
|
+
})
|
|
172
|
+
}}
|
|
173
|
+
</div>
|
|
174
|
+
<div v-if="item.note" class="mt-0.5 text-[10px] text-amber-300/80">
|
|
175
|
+
{{ item.note }}
|
|
176
|
+
</div>
|
|
177
|
+
</td>
|
|
178
|
+
<td class="px-3 py-2 align-top">
|
|
179
|
+
<UBadge
|
|
180
|
+
:color="INITIATIVE_ITEM_STATUS_CHIPS[item.status] as any"
|
|
181
|
+
variant="subtle"
|
|
182
|
+
size="sm"
|
|
183
|
+
>
|
|
184
|
+
{{ t(INITIATIVE_ITEM_STATUS_LABEL_KEYS[item.status]) }}
|
|
185
|
+
</UBadge>
|
|
186
|
+
</td>
|
|
187
|
+
<td class="px-3 py-2 align-top">
|
|
188
|
+
<a
|
|
189
|
+
v-if="item.pr"
|
|
190
|
+
:href="item.pr.url"
|
|
191
|
+
target="_blank"
|
|
192
|
+
rel="noopener"
|
|
193
|
+
class="text-sky-400 hover:underline"
|
|
194
|
+
>
|
|
195
|
+
{{
|
|
196
|
+
item.pr.number ? `#${item.pr.number}` : t('initiative.tracker.prLink')
|
|
197
|
+
}}
|
|
198
|
+
</a>
|
|
199
|
+
<span v-else class="text-slate-600">—</span>
|
|
200
|
+
</td>
|
|
201
|
+
</tr>
|
|
202
|
+
</tbody>
|
|
203
|
+
</table>
|
|
204
|
+
</div>
|
|
205
|
+
</section>
|
|
206
|
+
|
|
207
|
+
<!-- Execution policy -->
|
|
208
|
+
<section v-if="initiative.policy" class="mb-4">
|
|
209
|
+
<h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
210
|
+
{{ t('initiative.tracker.policy') }}
|
|
211
|
+
</h3>
|
|
212
|
+
<ul class="text-[12px] text-slate-300">
|
|
213
|
+
<li>
|
|
214
|
+
{{
|
|
215
|
+
t('initiative.tracker.maxConcurrent', {
|
|
216
|
+
count: initiative.policy.maxConcurrent,
|
|
217
|
+
})
|
|
218
|
+
}}
|
|
219
|
+
</li>
|
|
220
|
+
<li v-for="(rule, i) in policyRules" :key="i">
|
|
221
|
+
<code class="text-sky-300">{{ rule.pipelineId }}</code>
|
|
222
|
+
· {{ ruleAxes(rule) }}
|
|
223
|
+
</li>
|
|
224
|
+
<li>
|
|
225
|
+
{{ t('initiative.tracker.defaultPipeline') }}
|
|
226
|
+
<code class="text-sky-300">{{ initiative.policy.defaultPipelineId }}</code>
|
|
227
|
+
</li>
|
|
228
|
+
</ul>
|
|
229
|
+
</section>
|
|
230
|
+
|
|
231
|
+
<!-- Logs -->
|
|
232
|
+
<section v-if="initiative.decisions?.length" class="mb-4">
|
|
233
|
+
<h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
234
|
+
{{ t('initiative.tracker.decisions') }}
|
|
235
|
+
</h3>
|
|
236
|
+
<ul class="list-inside list-disc text-[13px] text-slate-300">
|
|
237
|
+
<li v-for="d in initiative.decisions" :key="d.id">
|
|
238
|
+
<span class="font-medium">{{ d.title }}</span>
|
|
239
|
+
<span v-if="d.detail" class="text-slate-400"> — {{ d.detail }}</span>
|
|
240
|
+
</li>
|
|
241
|
+
</ul>
|
|
242
|
+
</section>
|
|
243
|
+
<section v-if="initiative.deviations?.length" class="mb-4">
|
|
244
|
+
<h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
245
|
+
{{ t('initiative.tracker.deviations') }}
|
|
246
|
+
</h3>
|
|
247
|
+
<ul class="list-inside list-disc text-[13px] text-slate-300">
|
|
248
|
+
<li v-for="d in initiative.deviations" :key="d.id">
|
|
249
|
+
<code v-if="d.itemId" class="text-slate-400">{{ d.itemId }}</code>
|
|
250
|
+
{{ d.description }}
|
|
251
|
+
<span v-if="d.resolution" class="text-slate-400"> → {{ d.resolution }}</span>
|
|
252
|
+
</li>
|
|
253
|
+
</ul>
|
|
254
|
+
</section>
|
|
255
|
+
<section v-if="initiative.followUps?.length" class="mb-4">
|
|
256
|
+
<h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
257
|
+
{{ t('initiative.tracker.followUps') }}
|
|
258
|
+
</h3>
|
|
259
|
+
<ul class="list-inside list-disc text-[13px] text-slate-300">
|
|
260
|
+
<li v-for="f in initiative.followUps" :key="f.id">
|
|
261
|
+
<span class="font-medium">{{ f.title }}</span>
|
|
262
|
+
<span v-if="f.detail" class="text-slate-400"> — {{ f.detail }}</span>
|
|
263
|
+
</li>
|
|
264
|
+
</ul>
|
|
265
|
+
</section>
|
|
266
|
+
<section v-if="initiative.caveats?.length" class="mb-4">
|
|
267
|
+
<h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
268
|
+
{{ t('initiative.tracker.caveats') }}
|
|
269
|
+
</h3>
|
|
270
|
+
<ul class="list-inside list-disc text-[13px] text-slate-300">
|
|
271
|
+
<li v-for="(c, i) in initiative.caveats" :key="i">{{ c }}</li>
|
|
272
|
+
</ul>
|
|
273
|
+
</section>
|
|
274
|
+
</template>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
</Teleport>
|
|
279
|
+
</template>
|
|
@@ -17,6 +17,7 @@ import TaskRunSettings from '~/components/panels/inspector/TaskRunSettings.vue'
|
|
|
17
17
|
import TaskExecution from '~/components/panels/inspector/TaskExecution.vue'
|
|
18
18
|
import TaskEstimateBadge from '~/components/panels/inspector/TaskEstimateBadge.vue'
|
|
19
19
|
import EpicChildren from '~/components/panels/inspector/EpicChildren.vue'
|
|
20
|
+
import InitiativeInspector from '~/components/panels/inspector/InitiativeInspector.vue'
|
|
20
21
|
import RecurringScheduleSettings from '~/components/panels/inspector/RecurringScheduleSettings.vue'
|
|
21
22
|
import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
|
|
22
23
|
import AgentStopButton from '~/components/board/AgentStopButton.vue'
|
|
@@ -73,6 +74,7 @@ watch(
|
|
|
73
74
|
const isContainer = computed(() => level.value === 'frame' || level.value === 'module')
|
|
74
75
|
const isTask = computed(() => level.value === 'task')
|
|
75
76
|
const isEpic = computed(() => level.value === 'epic')
|
|
77
|
+
const isInitiative = computed(() => level.value === 'initiative')
|
|
76
78
|
|
|
77
79
|
const instance = computed(() => execution.getInstance(block.value?.executionId))
|
|
78
80
|
const typeMeta = computed(() => (block.value ? blockTypeMeta(block.value.type) : null))
|
|
@@ -509,6 +511,9 @@ const showOriginalDescription = ref(false)
|
|
|
509
511
|
<!-- epic: the full tree of member tasks, grouped by service → module -->
|
|
510
512
|
<EpicChildren v-else-if="isEpic" :key="`epic-${block.id}`" :block="block" />
|
|
511
513
|
|
|
514
|
+
<!-- initiative: status + goal, run-planning + tracker controls -->
|
|
515
|
+
<InitiativeInspector v-else-if="isInitiative" :block="block" />
|
|
516
|
+
|
|
512
517
|
<!-- actions -->
|
|
513
518
|
<div class="flex items-center gap-2">
|
|
514
519
|
<UDropdownMenu v-if="isTask" :items="runMenu">
|
|
@@ -23,6 +23,7 @@ import GenericStructuredResultView from '~/components/panels/GenericStructuredRe
|
|
|
23
23
|
import ServiceSpecWindow from '~/components/spec/ServiceSpecWindow.vue'
|
|
24
24
|
import FollowUpWindow from '~/components/followUp/FollowUpWindow.vue'
|
|
25
25
|
import MergerResultView from '~/components/panels/MergerResultView.vue'
|
|
26
|
+
import InitiativeTrackerWindow from '~/components/initiative/InitiativeTrackerWindow.vue'
|
|
26
27
|
|
|
27
28
|
const ui = useUiStore()
|
|
28
29
|
|
|
@@ -52,6 +53,10 @@ const STEP_RESULT_VIEWS: Record<string, Component> = {
|
|
|
52
53
|
// The merger's verdict: the PR's complexity/risk/impact scores + the engine's auto-merge
|
|
53
54
|
// or awaiting-review decision (and why), instead of the agent's raw JSON.
|
|
54
55
|
merger: MergerResultView,
|
|
56
|
+
// The initiative tracker: phases, per-item status + PR links, decisions, deviations,
|
|
57
|
+
// caveats. Opened from the initiative card / inspector (`ui.openInitiativeTracker`) and
|
|
58
|
+
// as the planner step's result view.
|
|
59
|
+
'initiative-tracker': InitiativeTrackerWindow,
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
const active = computed<Component | null>(() => {
|