@cat-factory/app 0.6.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 +143 -0
- package/app/components/auth/UserMenu.vue +39 -0
- package/app/components/board/AddTaskModal.vue +444 -0
- package/app/components/board/AgentFailureCard.vue +97 -0
- package/app/components/board/AgentStopButton.vue +61 -0
- package/app/components/board/BoardCanvas.vue +183 -0
- package/app/components/board/ContextPicker.vue +367 -0
- package/app/components/board/RecurringPipelineModal.vue +219 -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 +433 -0
- package/app/components/board/nodes/DecisionBadge.vue +27 -0
- package/app/components/board/nodes/DraggableTask.vue +48 -0
- package/app/components/board/nodes/ModuleFrame.vue +97 -0
- package/app/components/board/nodes/TaskCard.vue +359 -0
- package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
- package/app/components/bootstrap/BootstrapModal.vue +665 -0
- package/app/components/clarity/ClarityReviewWindow.vue +611 -0
- package/app/components/consensus/ConsensusSessionWindow.vue +210 -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 +171 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
- package/app/components/gates/GateResultView.vue +282 -0
- package/app/components/github/AddServiceFromRepoModal.vue +354 -0
- package/app/components/github/GitHubConnect.vue +183 -0
- package/app/components/github/GitHubOnboarding.vue +45 -0
- package/app/components/github/GitHubPanel.vue +584 -0
- package/app/components/github/RepoTreeBrowser.vue +171 -0
- package/app/components/layout/AccountTeamSettings.vue +237 -0
- package/app/components/layout/BoardSwitcher.vue +280 -0
- package/app/components/layout/BoardToolbar.vue +156 -0
- package/app/components/layout/CommandBar.vue +336 -0
- package/app/components/layout/GitHubPatBanner.vue +73 -0
- package/app/components/layout/NotificationsInbox.vue +175 -0
- package/app/components/layout/SideBar.vue +314 -0
- package/app/components/layout/SpendWarningBanner.vue +107 -0
- package/app/components/observability/StepMetricsBar.vue +102 -0
- package/app/components/palettes/AgentPalette.vue +86 -0
- package/app/components/panels/AgentStepDetail.vue +737 -0
- package/app/components/panels/DecisionModal.vue +71 -0
- package/app/components/panels/InspectorPanel.vue +465 -0
- package/app/components/panels/ObservabilityPanel.vue +351 -0
- package/app/components/panels/StepMetadataCard.vue +253 -0
- package/app/components/panels/StepRestartControl.vue +90 -0
- package/app/components/panels/StepResultViewHost.vue +40 -0
- package/app/components/panels/StepTestReport.vue +84 -0
- package/app/components/panels/inspector/ContainerSummary.vue +74 -0
- package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
- package/app/components/panels/inspector/ServiceFragments.vue +82 -0
- package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
- package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
- package/app/components/panels/inspector/TaskDependencies.vue +70 -0
- package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
- package/app/components/panels/inspector/TaskExecution.vue +364 -0
- package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
- package/app/components/panels/inspector/TaskStructure.vue +96 -0
- package/app/components/pipeline/AgentKindIcon.vue +30 -0
- package/app/components/pipeline/IterationCapPrompt.vue +70 -0
- package/app/components/pipeline/PipelineBuilder.vue +817 -0
- package/app/components/pipeline/PipelineProgress.vue +484 -0
- package/app/components/providers/ApiKeysSection.vue +273 -0
- package/app/components/providers/PersonalCredentialModal.vue +128 -0
- package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
- package/app/components/providers/VendorCredentialsModal.vue +197 -0
- package/app/components/recurring/RecurrenceEditor.vue +124 -0
- package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
- package/app/components/settings/DatadogPanel.vue +213 -0
- package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
- package/app/components/settings/MergeThresholdsPanel.vue +378 -0
- package/app/components/settings/ModelDefaultsPanel.vue +250 -0
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
- package/app/components/slack/SlackPanel.vue +299 -0
- package/app/components/tasks/TaskContextIssues.vue +88 -0
- package/app/components/tasks/TaskImportModal.vue +207 -0
- package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
- package/app/components/testing/TestReportWindow.vue +404 -0
- package/app/composables/api/accounts.ts +81 -0
- package/app/composables/api/auth.ts +45 -0
- package/app/composables/api/board.ts +101 -0
- package/app/composables/api/bootstrap.ts +62 -0
- package/app/composables/api/context.ts +25 -0
- package/app/composables/api/documents.ts +74 -0
- package/app/composables/api/execution.ts +127 -0
- package/app/composables/api/fragments.ts +71 -0
- package/app/composables/api/github.ts +131 -0
- package/app/composables/api/models.ts +127 -0
- package/app/composables/api/notifications.ts +23 -0
- package/app/composables/api/presets.ts +29 -0
- package/app/composables/api/recurring.ts +68 -0
- package/app/composables/api/releaseHealth.ts +43 -0
- package/app/composables/api/reviews.ts +146 -0
- package/app/composables/api/slack.ts +54 -0
- package/app/composables/api/tasks.ts +72 -0
- package/app/composables/api/workspaces.ts +36 -0
- package/app/composables/useApi.ts +89 -0
- package/app/composables/useBlockDrag.ts +90 -0
- package/app/composables/useBlockQueries.ts +154 -0
- package/app/composables/useBoardFlow.ts +11 -0
- package/app/composables/useContextLinking.ts +65 -0
- package/app/composables/useDepLabels.ts +26 -0
- package/app/composables/useFrameResize.ts +54 -0
- package/app/composables/useResultView.ts +48 -0
- package/app/composables/useReviewStage.ts +40 -0
- package/app/composables/useSemanticZoom.ts +31 -0
- package/app/composables/useStepApproval.ts +233 -0
- package/app/composables/useStepProse.ts +78 -0
- package/app/composables/useStepTimer.ts +63 -0
- package/app/composables/useTaskExpansion.ts +92 -0
- package/app/composables/useWorkspaceStream.ts +155 -0
- package/app/docs/architecture.md +31 -0
- package/app/pages/index.vue +141 -0
- package/app/stores/accounts.ts +152 -0
- package/app/stores/agentConfig.ts +35 -0
- package/app/stores/agentRuns.ts +122 -0
- package/app/stores/agents.ts +40 -0
- package/app/stores/apiKeys.ts +108 -0
- package/app/stores/auth.ts +166 -0
- package/app/stores/board.spec.ts +205 -0
- package/app/stores/board.ts +286 -0
- package/app/stores/bootstrap.ts +97 -0
- package/app/stores/clarity.ts +196 -0
- package/app/stores/consensus.ts +60 -0
- package/app/stores/documents.ts +176 -0
- package/app/stores/execution.ts +273 -0
- package/app/stores/fragmentLibrary.ts +147 -0
- package/app/stores/fragments.ts +40 -0
- package/app/stores/github.ts +305 -0
- package/app/stores/localModels.ts +51 -0
- package/app/stores/mergePresets.ts +58 -0
- package/app/stores/modelDefaults.ts +76 -0
- package/app/stores/models.ts +134 -0
- package/app/stores/notifications.ts +70 -0
- package/app/stores/observability.ts +144 -0
- package/app/stores/personalSubscriptions.ts +215 -0
- package/app/stores/pipelines.ts +327 -0
- package/app/stores/recurringPipelines.ts +112 -0
- package/app/stores/releaseHealth.ts +75 -0
- package/app/stores/requirements.spec.ts +94 -0
- package/app/stores/requirements.ts +208 -0
- package/app/stores/serviceFragmentDefaults.ts +29 -0
- package/app/stores/services.ts +87 -0
- package/app/stores/slack.ts +142 -0
- package/app/stores/taskExpansion.ts +36 -0
- package/app/stores/tasks.spec.ts +71 -0
- package/app/stores/tasks.ts +176 -0
- package/app/stores/tracker.ts +27 -0
- package/app/stores/ui.ts +434 -0
- package/app/stores/vendorCredentials.ts +54 -0
- package/app/stores/workspace.ts +215 -0
- package/app/stores/workspaceSettings.ts +36 -0
- package/app/types/accounts.ts +77 -0
- package/app/types/bootstrap.ts +83 -0
- package/app/types/clarity.ts +59 -0
- package/app/types/consensus.ts +91 -0
- package/app/types/documents.ts +104 -0
- package/app/types/domain.ts +495 -0
- package/app/types/execution.ts +383 -0
- package/app/types/fragments.ts +72 -0
- package/app/types/github.ts +173 -0
- package/app/types/localModels.ts +73 -0
- package/app/types/merge.ts +71 -0
- package/app/types/models.ts +157 -0
- package/app/types/notifications.ts +74 -0
- package/app/types/recurring.ts +69 -0
- package/app/types/releaseHealth.ts +31 -0
- package/app/types/requirements.ts +61 -0
- package/app/types/services.ts +27 -0
- package/app/types/slack.ts +57 -0
- package/app/types/tasks.ts +82 -0
- package/app/types/tracker.ts +18 -0
- package/app/utils/agentOutput.spec.ts +128 -0
- package/app/utils/agentOutput.ts +173 -0
- package/app/utils/catalog.spec.ts +112 -0
- package/app/utils/catalog.ts +455 -0
- package/app/utils/dnd.ts +29 -0
- package/app/utils/observability.ts +52 -0
- package/app/utils/pipelineRender.ts +151 -0
- package/nuxt.config.ts +55 -0
- package/package.json +45 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
// Shared "Restart pipeline from this step" control.
|
|
5
|
+
//
|
|
6
|
+
// The restart action is step-generic — it lives on the execution engine, not on any one
|
|
7
|
+
// agent kind — but a pipeline step opens in several different windows: the generic prose
|
|
8
|
+
// panel (AgentStepDetail) AND the dedicated result views (the tester report, the CI /
|
|
9
|
+
// conflicts gate, the requirements review). The restart affordance was originally bolted
|
|
10
|
+
// onto the prose panel alone, so clicking a step that routes to a dedicated window showed
|
|
11
|
+
// no way to restart. Centralising it here keeps the two-click confirm + the gating
|
|
12
|
+
// identical across every window, so the control is reachable from every step a human can
|
|
13
|
+
// click into.
|
|
14
|
+
//
|
|
15
|
+
// Re-runs the pipeline from this step onward: the server resets this step + every later
|
|
16
|
+
// step's iteration counters and re-drives a fresh run, preserving the earlier steps'
|
|
17
|
+
// outputs as handoff context. Destructive (later steps' results are dropped), so it's a
|
|
18
|
+
// two-click confirm. Renders nothing when there's no run behind the view (an off-path
|
|
19
|
+
// open, e.g. the inspector's pre-start requirements review) or while THIS step is parked
|
|
20
|
+
// on an unresolved approval gate (the approval rail owns that interaction).
|
|
21
|
+
const props = defineProps<{
|
|
22
|
+
instanceId: string | null
|
|
23
|
+
stepIndex: number | null
|
|
24
|
+
}>()
|
|
25
|
+
const emit = defineEmits<{ restarted: [] }>()
|
|
26
|
+
|
|
27
|
+
const execution = useExecutionStore()
|
|
28
|
+
|
|
29
|
+
const instance = computed(() =>
|
|
30
|
+
props.instanceId ? execution.getInstance(props.instanceId) : undefined,
|
|
31
|
+
)
|
|
32
|
+
const step = computed(() =>
|
|
33
|
+
instance.value && props.stepIndex !== null
|
|
34
|
+
? (instance.value.steps[props.stepIndex] ?? null)
|
|
35
|
+
: null,
|
|
36
|
+
)
|
|
37
|
+
const approvalPending = computed(() => step.value?.approval?.status === 'pending')
|
|
38
|
+
const canRestart = computed(
|
|
39
|
+
() => !!instance.value && props.stepIndex !== null && !approvalPending.value,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const armed = ref(false)
|
|
43
|
+
const restarting = ref(false)
|
|
44
|
+
async function restart() {
|
|
45
|
+
if (!instance.value || props.stepIndex === null || restarting.value) return
|
|
46
|
+
restarting.value = true
|
|
47
|
+
try {
|
|
48
|
+
await execution.restartFromStep(instance.value.id, props.stepIndex)
|
|
49
|
+
emit('restarted')
|
|
50
|
+
} finally {
|
|
51
|
+
restarting.value = false
|
|
52
|
+
armed.value = false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<template>
|
|
58
|
+
<template v-if="canRestart">
|
|
59
|
+
<UButton
|
|
60
|
+
v-if="!armed"
|
|
61
|
+
icon="i-lucide-rotate-ccw"
|
|
62
|
+
color="neutral"
|
|
63
|
+
variant="ghost"
|
|
64
|
+
size="sm"
|
|
65
|
+
title="Restart pipeline from this step"
|
|
66
|
+
@click="armed = true"
|
|
67
|
+
/>
|
|
68
|
+
<template v-else>
|
|
69
|
+
<UButton
|
|
70
|
+
color="warning"
|
|
71
|
+
variant="soft"
|
|
72
|
+
size="sm"
|
|
73
|
+
icon="i-lucide-rotate-ccw"
|
|
74
|
+
:loading="restarting"
|
|
75
|
+
@click="restart"
|
|
76
|
+
>
|
|
77
|
+
Restart from here
|
|
78
|
+
</UButton>
|
|
79
|
+
<UButton
|
|
80
|
+
color="neutral"
|
|
81
|
+
variant="ghost"
|
|
82
|
+
size="sm"
|
|
83
|
+
:disabled="restarting"
|
|
84
|
+
@click="armed = false"
|
|
85
|
+
>
|
|
86
|
+
Cancel
|
|
87
|
+
</UButton>
|
|
88
|
+
</template>
|
|
89
|
+
</template>
|
|
90
|
+
</template>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Universal dedicated-result-view host. An agent archetype can declare a `resultView`
|
|
3
|
+
// id (see `~/utils/catalog`); when a step of that kind is opened, `ui.resultView` is set
|
|
4
|
+
// and this host renders the matching registered window instead of the generic
|
|
5
|
+
// `AgentStepDetail` prose panel. Adding a bespoke visualization for a new agent is just:
|
|
6
|
+
// 1. declare `resultView: '<id>'` on its archetype, and
|
|
7
|
+
// 2. register `'<id>': <Component>` below.
|
|
8
|
+
// No caller changes — every board/inspector entry point already routes through
|
|
9
|
+
// `ui` dispatch. Each registered window builds on `useResultView(viewId, { onOpen })`,
|
|
10
|
+
// which owns the seam contract (open/blockId/close + Escape + load-on-open) so a new
|
|
11
|
+
// window can't reintroduce the route-dependent empty-state bug by forgetting to fetch
|
|
12
|
+
// on mount — declare an `onOpen` loader and it fires on every open.
|
|
13
|
+
import { computed, type Component } from 'vue'
|
|
14
|
+
import RequirementsReviewWindow from '~/components/requirements/RequirementsReviewWindow.vue'
|
|
15
|
+
import ClarityReviewWindow from '~/components/clarity/ClarityReviewWindow.vue'
|
|
16
|
+
import TestReportWindow from '~/components/testing/TestReportWindow.vue'
|
|
17
|
+
import GateResultView from '~/components/gates/GateResultView.vue'
|
|
18
|
+
import ConsensusSessionWindow from '~/components/consensus/ConsensusSessionWindow.vue'
|
|
19
|
+
|
|
20
|
+
const ui = useUiStore()
|
|
21
|
+
|
|
22
|
+
const STEP_RESULT_VIEWS: Record<string, Component> = {
|
|
23
|
+
'requirements-review': RequirementsReviewWindow,
|
|
24
|
+
'clarity-review': ClarityReviewWindow,
|
|
25
|
+
tester: TestReportWindow,
|
|
26
|
+
// Shared by both polling gates (`ci` + `conflicts`); the window branches on agentKind.
|
|
27
|
+
gate: GateResultView,
|
|
28
|
+
// Opened for any step that ran the consensus mechanism (routed in `ui.dispatchStepView`).
|
|
29
|
+
'consensus-session': ConsensusSessionWindow,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const active = computed<Component | null>(() => {
|
|
33
|
+
const view = ui.resultView?.view
|
|
34
|
+
return view ? (STEP_RESULT_VIEWS[view] ?? null) : null
|
|
35
|
+
})
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template>
|
|
39
|
+
<component :is="active" v-if="active" />
|
|
40
|
+
</template>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { TestReport } from '~/types/domain'
|
|
3
|
+
import type { TesterStepState } from '~/types/execution'
|
|
4
|
+
|
|
5
|
+
// A tester step's latest structured report (what was tested, the per-area outcomes,
|
|
6
|
+
// the concerns it raised and the greenlight verdict) plus the fixer-loop phase.
|
|
7
|
+
defineProps<{
|
|
8
|
+
report: TestReport
|
|
9
|
+
phase: TesterStepState | null
|
|
10
|
+
}>()
|
|
11
|
+
|
|
12
|
+
const SEVERITY_COLOR: Record<string, string> = {
|
|
13
|
+
low: '#64748b',
|
|
14
|
+
medium: '#f59e0b',
|
|
15
|
+
high: '#f97316',
|
|
16
|
+
critical: '#ef4444',
|
|
17
|
+
}
|
|
18
|
+
const OUTCOME_COLOR: Record<string, string> = {
|
|
19
|
+
passed: '#22c55e',
|
|
20
|
+
failed: '#ef4444',
|
|
21
|
+
skipped: '#64748b',
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<section class="mt-4 scroll-mt-4">
|
|
27
|
+
<div class="mb-2 flex items-center gap-1.5 text-[11px]">
|
|
28
|
+
<UIcon name="i-lucide-flask-conical" class="h-3.5 w-3.5 text-slate-400" />
|
|
29
|
+
<span class="font-semibold uppercase tracking-wide text-slate-400"> Test report </span>
|
|
30
|
+
<UBadge :color="report.greenlight ? 'success' : 'warning'" variant="subtle" size="sm">
|
|
31
|
+
{{ report.greenlight ? 'Greenlit' : 'Needs fixes' }}
|
|
32
|
+
</UBadge>
|
|
33
|
+
<span v-if="phase && phase.attempts > 0" class="text-[11px] text-slate-500">
|
|
34
|
+
{{ phase.attempts }}/{{ phase.maxAttempts }} fix attempt(s)<span
|
|
35
|
+
v-if="phase.phase === 'fixing'"
|
|
36
|
+
>
|
|
37
|
+
· fixing…</span
|
|
38
|
+
>
|
|
39
|
+
</span>
|
|
40
|
+
</div>
|
|
41
|
+
<p v-if="report.summary" class="mb-3 text-[13px] leading-relaxed text-slate-300">
|
|
42
|
+
{{ report.summary }}
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
<div v-if="report.tested.length" class="mb-3">
|
|
46
|
+
<div class="mb-1 text-[11px] text-slate-500">Tested</div>
|
|
47
|
+
<ul class="space-y-0.5 text-[12px] text-slate-300">
|
|
48
|
+
<li v-for="(t, i) in report.tested" :key="i">• {{ t }}</li>
|
|
49
|
+
</ul>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div v-if="report.outcomes.length" class="mb-3 space-y-1">
|
|
53
|
+
<div class="text-[11px] text-slate-500">Outcomes</div>
|
|
54
|
+
<div v-for="(o, i) in report.outcomes" :key="i" class="flex items-start gap-2 text-[12px]">
|
|
55
|
+
<span
|
|
56
|
+
class="mt-1 h-2 w-2 shrink-0 rounded-full"
|
|
57
|
+
:style="{ backgroundColor: OUTCOME_COLOR[o.status] ?? '#64748b' }"
|
|
58
|
+
/>
|
|
59
|
+
<span class="text-slate-300"
|
|
60
|
+
>{{ o.name }}<span v-if="o.detail" class="text-slate-500"> — {{ o.detail }}</span></span
|
|
61
|
+
>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div v-if="report.concerns.length" class="space-y-1">
|
|
66
|
+
<div class="text-[11px] text-slate-500">Concerns</div>
|
|
67
|
+
<div
|
|
68
|
+
v-for="(c, i) in report.concerns"
|
|
69
|
+
:key="i"
|
|
70
|
+
class="rounded border border-slate-700/60 p-2 text-[12px]"
|
|
71
|
+
>
|
|
72
|
+
<div class="flex items-center gap-1.5">
|
|
73
|
+
<span
|
|
74
|
+
class="rounded px-1 text-[10px] font-semibold uppercase text-white"
|
|
75
|
+
:style="{ backgroundColor: SEVERITY_COLOR[c.severity] ?? '#64748b' }"
|
|
76
|
+
>{{ c.severity }}</span
|
|
77
|
+
>
|
|
78
|
+
<span class="font-medium text-slate-200">{{ c.title }}</span>
|
|
79
|
+
</div>
|
|
80
|
+
<p v-if="c.detail" class="mt-1 text-slate-400">{{ c.detail }}</p>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</section>
|
|
84
|
+
</template>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Block } from '~/types/domain'
|
|
3
|
+
import { STATUS_META } from '~/utils/catalog'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{ block: Block }>()
|
|
6
|
+
|
|
7
|
+
const board = useBoardStore()
|
|
8
|
+
const ui = useUiStore()
|
|
9
|
+
|
|
10
|
+
const isFrame = computed(() => (props.block.level ?? 'frame') === 'frame')
|
|
11
|
+
const modules = computed(() => (isFrame.value ? board.modulesOf(props.block.id) : []))
|
|
12
|
+
const tasks = computed(() =>
|
|
13
|
+
isFrame.value ? board.allTasksUnder(props.block.id) : board.tasksOf(props.block.id),
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
function addTask() {
|
|
17
|
+
ui.openAddTask(props.block.id)
|
|
18
|
+
}
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<div class="space-y-4">
|
|
23
|
+
<!-- modules (services only) -->
|
|
24
|
+
<div v-if="modules.length">
|
|
25
|
+
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
26
|
+
Modules ({{ modules.length }})
|
|
27
|
+
</div>
|
|
28
|
+
<ul class="space-y-1">
|
|
29
|
+
<li
|
|
30
|
+
v-for="m in modules"
|
|
31
|
+
:key="m.id"
|
|
32
|
+
class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1 hover:bg-slate-800/60"
|
|
33
|
+
@click="ui.select(m.id)"
|
|
34
|
+
>
|
|
35
|
+
<UIcon name="i-lucide-package" class="h-3.5 w-3.5 text-violet-400" />
|
|
36
|
+
<span class="truncate text-xs text-slate-200">{{ m.title }}</span>
|
|
37
|
+
<span class="ml-auto text-[10px] text-slate-500"
|
|
38
|
+
>{{ board.tasksOf(m.id).length }} task(s)</span
|
|
39
|
+
>
|
|
40
|
+
</li>
|
|
41
|
+
</ul>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div>
|
|
45
|
+
<div class="mb-1 flex items-center justify-between">
|
|
46
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
47
|
+
{{ isFrame ? 'All tasks' : 'Tasks' }} ({{ tasks.length }})
|
|
48
|
+
</span>
|
|
49
|
+
<UButton size="xs" variant="soft" color="primary" icon="i-lucide-plus" @click="addTask">
|
|
50
|
+
Add task
|
|
51
|
+
</UButton>
|
|
52
|
+
</div>
|
|
53
|
+
<ul v-if="tasks.length" class="space-y-1">
|
|
54
|
+
<li
|
|
55
|
+
v-for="t in tasks"
|
|
56
|
+
:key="t.id"
|
|
57
|
+
class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1 hover:bg-slate-800/60"
|
|
58
|
+
@click="ui.select(t.id)"
|
|
59
|
+
>
|
|
60
|
+
<span
|
|
61
|
+
class="h-2 w-2 shrink-0 rounded-full"
|
|
62
|
+
:style="{ backgroundColor: STATUS_META[t.status].color }"
|
|
63
|
+
/>
|
|
64
|
+
<span class="truncate text-xs text-slate-200">{{ t.title }}</span>
|
|
65
|
+
<span class="ml-auto text-[10px] text-slate-500">{{ STATUS_META[t.status].label }}</span>
|
|
66
|
+
</li>
|
|
67
|
+
</ul>
|
|
68
|
+
<div v-else class="text-[11px] text-slate-500">No tasks yet — add one to start work.</div>
|
|
69
|
+
</div>
|
|
70
|
+
<p v-if="isFrame" class="text-[11px] text-slate-500">
|
|
71
|
+
Services are long-lived — they don't "complete". Work happens in their tasks & modules.
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
</template>
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Inspector section shown when the selected task block backs a recurring pipeline.
|
|
3
|
+
// Lets the user edit the cadence, pause/resume, run now, and review the run history
|
|
4
|
+
// (lazily loaded; retained ~1 week on the backend).
|
|
5
|
+
import type { Block } from '~/types/domain'
|
|
6
|
+
import type { Recurrence } from '~/types/recurring'
|
|
7
|
+
|
|
8
|
+
const props = defineProps<{ block: Block }>()
|
|
9
|
+
const recurring = useRecurringPipelinesStore()
|
|
10
|
+
const pipelines = usePipelinesStore()
|
|
11
|
+
const toast = useToast()
|
|
12
|
+
|
|
13
|
+
const schedule = computed(() => recurring.byBlock(props.block.id))
|
|
14
|
+
const runs = computed(() =>
|
|
15
|
+
schedule.value ? (recurring.runsBySchedule[schedule.value.id] ?? []) : [],
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
const editing = ref(false)
|
|
19
|
+
const draft = ref<Recurrence | null>(null)
|
|
20
|
+
const busy = ref(false)
|
|
21
|
+
|
|
22
|
+
// Load history whenever a schedule is shown.
|
|
23
|
+
watch(
|
|
24
|
+
() => schedule.value?.id,
|
|
25
|
+
(id) => {
|
|
26
|
+
if (id) recurring.loadRuns(id).catch(() => {})
|
|
27
|
+
},
|
|
28
|
+
{ immediate: true },
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const pipelineName = computed(
|
|
32
|
+
() => pipelines.getPipeline(schedule.value?.pipelineId ?? '')?.name ?? schedule.value?.pipelineId,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
function describeCadence(r: Recurrence): string {
|
|
36
|
+
const every = r.intervalHours % 24 === 0 ? `${r.intervalHours / 24}d` : `${r.intervalHours}h`
|
|
37
|
+
const days =
|
|
38
|
+
r.weekdays.length === 0
|
|
39
|
+
? 'any day'
|
|
40
|
+
: r.weekdays.map((d) => ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][d]).join(' ')
|
|
41
|
+
const window =
|
|
42
|
+
r.windowStartHour === null && r.windowEndHour === null
|
|
43
|
+
? ''
|
|
44
|
+
: ` · ${String(r.windowStartHour ?? 0).padStart(2, '0')}:00–${String(r.windowEndHour ?? 24).padStart(2, '0')}:00`
|
|
45
|
+
return `Every ${every} · ${days}${window} · ${r.timezone}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function startEdit() {
|
|
49
|
+
if (!schedule.value) return
|
|
50
|
+
draft.value = { ...schedule.value.recurrence }
|
|
51
|
+
editing.value = true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function saveEdit() {
|
|
55
|
+
if (!schedule.value || !draft.value) return
|
|
56
|
+
busy.value = true
|
|
57
|
+
try {
|
|
58
|
+
await recurring.update(schedule.value.id, { recurrence: draft.value })
|
|
59
|
+
editing.value = false
|
|
60
|
+
} catch (e) {
|
|
61
|
+
toast.add({ title: 'Could not update schedule', description: errMsg(e), color: 'error' })
|
|
62
|
+
} finally {
|
|
63
|
+
busy.value = false
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function toggleEnabled() {
|
|
68
|
+
if (!schedule.value) return
|
|
69
|
+
busy.value = true
|
|
70
|
+
try {
|
|
71
|
+
await recurring.update(schedule.value.id, { enabled: !schedule.value.enabled })
|
|
72
|
+
} catch (e) {
|
|
73
|
+
toast.add({ title: 'Could not update schedule', description: errMsg(e), color: 'error' })
|
|
74
|
+
} finally {
|
|
75
|
+
busy.value = false
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function runNow() {
|
|
80
|
+
if (!schedule.value) return
|
|
81
|
+
busy.value = true
|
|
82
|
+
try {
|
|
83
|
+
await recurring.runNow(schedule.value.id)
|
|
84
|
+
} catch (e) {
|
|
85
|
+
toast.add({ title: 'Could not run now', description: errMsg(e), color: 'error' })
|
|
86
|
+
} finally {
|
|
87
|
+
busy.value = false
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function errMsg(e: unknown) {
|
|
92
|
+
return e instanceof Error ? e.message : String(e)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const RUN_COLOR: Record<string, string> = {
|
|
96
|
+
running: 'text-amber-400',
|
|
97
|
+
done: 'text-emerald-400',
|
|
98
|
+
failed: 'text-rose-400',
|
|
99
|
+
skipped: 'text-slate-500',
|
|
100
|
+
}
|
|
101
|
+
function fmtTime(ms: number) {
|
|
102
|
+
return new Date(ms).toLocaleString()
|
|
103
|
+
}
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<template>
|
|
107
|
+
<div
|
|
108
|
+
v-if="schedule"
|
|
109
|
+
class="space-y-2 rounded-lg border border-indigo-900/50 bg-indigo-950/20 p-3"
|
|
110
|
+
>
|
|
111
|
+
<div class="flex items-center justify-between">
|
|
112
|
+
<span
|
|
113
|
+
class="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-indigo-300"
|
|
114
|
+
>
|
|
115
|
+
<UIcon name="i-lucide-repeat" class="h-3.5 w-3.5" />
|
|
116
|
+
Recurring pipeline
|
|
117
|
+
</span>
|
|
118
|
+
<UBadge :color="schedule.enabled ? 'primary' : 'neutral'" variant="subtle" size="xs">
|
|
119
|
+
{{ schedule.enabled ? 'Active' : 'Paused' }}
|
|
120
|
+
</UBadge>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<p class="text-[11px] text-slate-400">
|
|
124
|
+
<span class="text-slate-300">{{ pipelineName }}</span>
|
|
125
|
+
</p>
|
|
126
|
+
|
|
127
|
+
<template v-if="!editing">
|
|
128
|
+
<p class="text-[11px] text-slate-400">{{ describeCadence(schedule.recurrence) }}</p>
|
|
129
|
+
<p class="text-[11px] text-slate-500">Next run: {{ fmtTime(schedule.nextRunAt) }}</p>
|
|
130
|
+
<div class="flex flex-wrap gap-1.5 pt-1">
|
|
131
|
+
<UButton size="xs" variant="soft" icon="i-lucide-play" :loading="busy" @click="runNow">
|
|
132
|
+
Run now
|
|
133
|
+
</UButton>
|
|
134
|
+
<UButton
|
|
135
|
+
size="xs"
|
|
136
|
+
variant="soft"
|
|
137
|
+
color="neutral"
|
|
138
|
+
:icon="schedule.enabled ? 'i-lucide-pause' : 'i-lucide-play'"
|
|
139
|
+
:loading="busy"
|
|
140
|
+
@click="toggleEnabled"
|
|
141
|
+
>
|
|
142
|
+
{{ schedule.enabled ? 'Pause' : 'Resume' }}
|
|
143
|
+
</UButton>
|
|
144
|
+
<UButton
|
|
145
|
+
size="xs"
|
|
146
|
+
variant="ghost"
|
|
147
|
+
color="neutral"
|
|
148
|
+
icon="i-lucide-pencil"
|
|
149
|
+
@click="startEdit"
|
|
150
|
+
>
|
|
151
|
+
Edit cadence
|
|
152
|
+
</UButton>
|
|
153
|
+
</div>
|
|
154
|
+
</template>
|
|
155
|
+
|
|
156
|
+
<template v-else-if="draft">
|
|
157
|
+
<RecurringRecurrenceEditor v-model="draft" />
|
|
158
|
+
<div class="flex justify-end gap-1.5 pt-1">
|
|
159
|
+
<UButton size="xs" variant="ghost" color="neutral" @click="editing = false">Cancel</UButton>
|
|
160
|
+
<UButton size="xs" color="primary" :loading="busy" @click="saveEdit">Save</UButton>
|
|
161
|
+
</div>
|
|
162
|
+
</template>
|
|
163
|
+
|
|
164
|
+
<!-- run history -->
|
|
165
|
+
<div v-if="runs.length" class="space-y-1 border-t border-slate-800 pt-2">
|
|
166
|
+
<span class="text-[10px] font-semibold uppercase tracking-wide text-slate-500">
|
|
167
|
+
Recent runs
|
|
168
|
+
</span>
|
|
169
|
+
<div v-for="run in runs" :key="run.id" class="flex items-center gap-2 text-[11px]">
|
|
170
|
+
<span :class="RUN_COLOR[run.status] ?? 'text-slate-400'" class="w-14 shrink-0 capitalize">
|
|
171
|
+
{{ run.status }}
|
|
172
|
+
</span>
|
|
173
|
+
<span class="truncate text-slate-500">{{ fmtTime(run.startedAt) }}</span>
|
|
174
|
+
<span v-if="run.outcome" class="ml-auto truncate text-slate-500">{{ run.outcome }}</span>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</template>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Block } from '~/types/domain'
|
|
3
|
+
|
|
4
|
+
// Service-level best-practice fragments (frame blocks). These are the programming
|
|
5
|
+
// standards/guidelines for the whole service; at run time their bodies are folded
|
|
6
|
+
// into the prompt of every `code-aware` agent on tasks under this service. Drawn from
|
|
7
|
+
// the universal fragment pool (built-in + deployment-registered), grouped by category.
|
|
8
|
+
const props = defineProps<{ block: Block }>()
|
|
9
|
+
|
|
10
|
+
const board = useBoardStore()
|
|
11
|
+
const fragments = useFragmentsStore()
|
|
12
|
+
|
|
13
|
+
onMounted(() => fragments.ensureLoaded())
|
|
14
|
+
|
|
15
|
+
const selectedFragments = computed(() =>
|
|
16
|
+
(props.block.serviceFragmentIds ?? [])
|
|
17
|
+
.map((id) => fragments.getFragment(id))
|
|
18
|
+
.filter((f): f is NonNullable<typeof f> => !!f),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// Picker menu: every pool fragment not already selected, grouped by category.
|
|
22
|
+
const fragmentMenu = computed(() => {
|
|
23
|
+
const selected = new Set(props.block.serviceFragmentIds ?? [])
|
|
24
|
+
const groups = new Map<string, { label: string; onSelect: () => void }[]>()
|
|
25
|
+
for (const f of fragments.fragments) {
|
|
26
|
+
if (selected.has(f.id)) continue
|
|
27
|
+
const items = groups.get(f.category) ?? []
|
|
28
|
+
items.push({ label: f.title, onSelect: () => addFragment(f.id) })
|
|
29
|
+
groups.set(f.category, items)
|
|
30
|
+
}
|
|
31
|
+
return [...groups.values()]
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
function addFragment(id: string) {
|
|
35
|
+
const list = props.block.serviceFragmentIds ? [...props.block.serviceFragmentIds] : []
|
|
36
|
+
if (!list.includes(id)) list.push(id)
|
|
37
|
+
board.updateBlock(props.block.id, { serviceFragmentIds: list })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function removeFragment(id: string) {
|
|
41
|
+
if (!props.block.serviceFragmentIds) return
|
|
42
|
+
board.updateBlock(props.block.id, {
|
|
43
|
+
serviceFragmentIds: props.block.serviceFragmentIds.filter((x) => x !== id),
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<div>
|
|
50
|
+
<div class="mb-1 flex items-center justify-between">
|
|
51
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
52
|
+
Service best practices
|
|
53
|
+
</span>
|
|
54
|
+
<UDropdownMenu v-if="fragmentMenu.length" :items="fragmentMenu">
|
|
55
|
+
<UButton
|
|
56
|
+
size="xs"
|
|
57
|
+
variant="ghost"
|
|
58
|
+
color="neutral"
|
|
59
|
+
icon="i-lucide-plus"
|
|
60
|
+
trailing-icon="i-lucide-chevron-down"
|
|
61
|
+
/>
|
|
62
|
+
</UDropdownMenu>
|
|
63
|
+
</div>
|
|
64
|
+
<div v-if="selectedFragments.length" class="mb-1 flex flex-wrap gap-1">
|
|
65
|
+
<UBadge
|
|
66
|
+
v-for="f in selectedFragments"
|
|
67
|
+
:key="f.id"
|
|
68
|
+
color="primary"
|
|
69
|
+
variant="subtle"
|
|
70
|
+
size="sm"
|
|
71
|
+
class="cursor-pointer"
|
|
72
|
+
:title="f.summary"
|
|
73
|
+
@click="removeFragment(f.id)"
|
|
74
|
+
>
|
|
75
|
+
{{ f.title }}<UIcon name="i-lucide-x" class="ml-0.5 h-3 w-3" />
|
|
76
|
+
</UBadge>
|
|
77
|
+
</div>
|
|
78
|
+
<div v-else class="text-[11px] text-slate-500">
|
|
79
|
+
None — code-aware agents on this service follow their default guidance.
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</template>
|