@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.
Files changed (189) 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 +143 -0
  8. package/app/components/auth/UserMenu.vue +39 -0
  9. package/app/components/board/AddTaskModal.vue +444 -0
  10. package/app/components/board/AgentFailureCard.vue +97 -0
  11. package/app/components/board/AgentStopButton.vue +61 -0
  12. package/app/components/board/BoardCanvas.vue +183 -0
  13. package/app/components/board/ContextPicker.vue +367 -0
  14. package/app/components/board/RecurringPipelineModal.vue +219 -0
  15. package/app/components/board/TaskDependencyEdges.vue +132 -0
  16. package/app/components/board/nodes/AgentChip.vue +59 -0
  17. package/app/components/board/nodes/BlockNode.vue +433 -0
  18. package/app/components/board/nodes/DecisionBadge.vue +27 -0
  19. package/app/components/board/nodes/DraggableTask.vue +48 -0
  20. package/app/components/board/nodes/ModuleFrame.vue +97 -0
  21. package/app/components/board/nodes/TaskCard.vue +359 -0
  22. package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
  23. package/app/components/bootstrap/BootstrapModal.vue +665 -0
  24. package/app/components/clarity/ClarityReviewWindow.vue +611 -0
  25. package/app/components/consensus/ConsensusSessionWindow.vue +210 -0
  26. package/app/components/documents/DocumentImportModal.vue +161 -0
  27. package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
  28. package/app/components/documents/SpawnPreviewModal.vue +161 -0
  29. package/app/components/documents/TaskContextDocs.vue +83 -0
  30. package/app/components/focus/BlockFocusView.vue +171 -0
  31. package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
  32. package/app/components/gates/GateResultView.vue +282 -0
  33. package/app/components/github/AddServiceFromRepoModal.vue +354 -0
  34. package/app/components/github/GitHubConnect.vue +183 -0
  35. package/app/components/github/GitHubOnboarding.vue +45 -0
  36. package/app/components/github/GitHubPanel.vue +584 -0
  37. package/app/components/github/RepoTreeBrowser.vue +171 -0
  38. package/app/components/layout/AccountTeamSettings.vue +237 -0
  39. package/app/components/layout/BoardSwitcher.vue +280 -0
  40. package/app/components/layout/BoardToolbar.vue +156 -0
  41. package/app/components/layout/CommandBar.vue +336 -0
  42. package/app/components/layout/GitHubPatBanner.vue +73 -0
  43. package/app/components/layout/NotificationsInbox.vue +175 -0
  44. package/app/components/layout/SideBar.vue +314 -0
  45. package/app/components/layout/SpendWarningBanner.vue +107 -0
  46. package/app/components/observability/StepMetricsBar.vue +102 -0
  47. package/app/components/palettes/AgentPalette.vue +86 -0
  48. package/app/components/panels/AgentStepDetail.vue +737 -0
  49. package/app/components/panels/DecisionModal.vue +71 -0
  50. package/app/components/panels/InspectorPanel.vue +465 -0
  51. package/app/components/panels/ObservabilityPanel.vue +351 -0
  52. package/app/components/panels/StepMetadataCard.vue +253 -0
  53. package/app/components/panels/StepRestartControl.vue +90 -0
  54. package/app/components/panels/StepResultViewHost.vue +40 -0
  55. package/app/components/panels/StepTestReport.vue +84 -0
  56. package/app/components/panels/inspector/ContainerSummary.vue +74 -0
  57. package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
  58. package/app/components/panels/inspector/ServiceFragments.vue +82 -0
  59. package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
  60. package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
  61. package/app/components/panels/inspector/TaskDependencies.vue +70 -0
  62. package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
  63. package/app/components/panels/inspector/TaskExecution.vue +364 -0
  64. package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
  65. package/app/components/panels/inspector/TaskStructure.vue +96 -0
  66. package/app/components/pipeline/AgentKindIcon.vue +30 -0
  67. package/app/components/pipeline/IterationCapPrompt.vue +70 -0
  68. package/app/components/pipeline/PipelineBuilder.vue +817 -0
  69. package/app/components/pipeline/PipelineProgress.vue +484 -0
  70. package/app/components/providers/ApiKeysSection.vue +273 -0
  71. package/app/components/providers/PersonalCredentialModal.vue +128 -0
  72. package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
  73. package/app/components/providers/VendorCredentialsModal.vue +197 -0
  74. package/app/components/recurring/RecurrenceEditor.vue +124 -0
  75. package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
  76. package/app/components/settings/DatadogPanel.vue +213 -0
  77. package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
  78. package/app/components/settings/MergeThresholdsPanel.vue +378 -0
  79. package/app/components/settings/ModelDefaultsPanel.vue +250 -0
  80. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
  81. package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
  82. package/app/components/slack/SlackPanel.vue +299 -0
  83. package/app/components/tasks/TaskContextIssues.vue +88 -0
  84. package/app/components/tasks/TaskImportModal.vue +207 -0
  85. package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
  86. package/app/components/testing/TestReportWindow.vue +404 -0
  87. package/app/composables/api/accounts.ts +81 -0
  88. package/app/composables/api/auth.ts +45 -0
  89. package/app/composables/api/board.ts +101 -0
  90. package/app/composables/api/bootstrap.ts +62 -0
  91. package/app/composables/api/context.ts +25 -0
  92. package/app/composables/api/documents.ts +74 -0
  93. package/app/composables/api/execution.ts +127 -0
  94. package/app/composables/api/fragments.ts +71 -0
  95. package/app/composables/api/github.ts +131 -0
  96. package/app/composables/api/models.ts +127 -0
  97. package/app/composables/api/notifications.ts +23 -0
  98. package/app/composables/api/presets.ts +29 -0
  99. package/app/composables/api/recurring.ts +68 -0
  100. package/app/composables/api/releaseHealth.ts +43 -0
  101. package/app/composables/api/reviews.ts +146 -0
  102. package/app/composables/api/slack.ts +54 -0
  103. package/app/composables/api/tasks.ts +72 -0
  104. package/app/composables/api/workspaces.ts +36 -0
  105. package/app/composables/useApi.ts +89 -0
  106. package/app/composables/useBlockDrag.ts +90 -0
  107. package/app/composables/useBlockQueries.ts +154 -0
  108. package/app/composables/useBoardFlow.ts +11 -0
  109. package/app/composables/useContextLinking.ts +65 -0
  110. package/app/composables/useDepLabels.ts +26 -0
  111. package/app/composables/useFrameResize.ts +54 -0
  112. package/app/composables/useResultView.ts +48 -0
  113. package/app/composables/useReviewStage.ts +40 -0
  114. package/app/composables/useSemanticZoom.ts +31 -0
  115. package/app/composables/useStepApproval.ts +233 -0
  116. package/app/composables/useStepProse.ts +78 -0
  117. package/app/composables/useStepTimer.ts +63 -0
  118. package/app/composables/useTaskExpansion.ts +92 -0
  119. package/app/composables/useWorkspaceStream.ts +155 -0
  120. package/app/docs/architecture.md +31 -0
  121. package/app/pages/index.vue +141 -0
  122. package/app/stores/accounts.ts +152 -0
  123. package/app/stores/agentConfig.ts +35 -0
  124. package/app/stores/agentRuns.ts +122 -0
  125. package/app/stores/agents.ts +40 -0
  126. package/app/stores/apiKeys.ts +108 -0
  127. package/app/stores/auth.ts +166 -0
  128. package/app/stores/board.spec.ts +205 -0
  129. package/app/stores/board.ts +286 -0
  130. package/app/stores/bootstrap.ts +97 -0
  131. package/app/stores/clarity.ts +196 -0
  132. package/app/stores/consensus.ts +60 -0
  133. package/app/stores/documents.ts +176 -0
  134. package/app/stores/execution.ts +273 -0
  135. package/app/stores/fragmentLibrary.ts +147 -0
  136. package/app/stores/fragments.ts +40 -0
  137. package/app/stores/github.ts +305 -0
  138. package/app/stores/localModels.ts +51 -0
  139. package/app/stores/mergePresets.ts +58 -0
  140. package/app/stores/modelDefaults.ts +76 -0
  141. package/app/stores/models.ts +134 -0
  142. package/app/stores/notifications.ts +70 -0
  143. package/app/stores/observability.ts +144 -0
  144. package/app/stores/personalSubscriptions.ts +215 -0
  145. package/app/stores/pipelines.ts +327 -0
  146. package/app/stores/recurringPipelines.ts +112 -0
  147. package/app/stores/releaseHealth.ts +75 -0
  148. package/app/stores/requirements.spec.ts +94 -0
  149. package/app/stores/requirements.ts +208 -0
  150. package/app/stores/serviceFragmentDefaults.ts +29 -0
  151. package/app/stores/services.ts +87 -0
  152. package/app/stores/slack.ts +142 -0
  153. package/app/stores/taskExpansion.ts +36 -0
  154. package/app/stores/tasks.spec.ts +71 -0
  155. package/app/stores/tasks.ts +176 -0
  156. package/app/stores/tracker.ts +27 -0
  157. package/app/stores/ui.ts +434 -0
  158. package/app/stores/vendorCredentials.ts +54 -0
  159. package/app/stores/workspace.ts +215 -0
  160. package/app/stores/workspaceSettings.ts +36 -0
  161. package/app/types/accounts.ts +77 -0
  162. package/app/types/bootstrap.ts +83 -0
  163. package/app/types/clarity.ts +59 -0
  164. package/app/types/consensus.ts +91 -0
  165. package/app/types/documents.ts +104 -0
  166. package/app/types/domain.ts +495 -0
  167. package/app/types/execution.ts +383 -0
  168. package/app/types/fragments.ts +72 -0
  169. package/app/types/github.ts +173 -0
  170. package/app/types/localModels.ts +73 -0
  171. package/app/types/merge.ts +71 -0
  172. package/app/types/models.ts +157 -0
  173. package/app/types/notifications.ts +74 -0
  174. package/app/types/recurring.ts +69 -0
  175. package/app/types/releaseHealth.ts +31 -0
  176. package/app/types/requirements.ts +61 -0
  177. package/app/types/services.ts +27 -0
  178. package/app/types/slack.ts +57 -0
  179. package/app/types/tasks.ts +82 -0
  180. package/app/types/tracker.ts +18 -0
  181. package/app/utils/agentOutput.spec.ts +128 -0
  182. package/app/utils/agentOutput.ts +173 -0
  183. package/app/utils/catalog.spec.ts +112 -0
  184. package/app/utils/catalog.ts +455 -0
  185. package/app/utils/dnd.ts +29 -0
  186. package/app/utils/observability.ts +52 -0
  187. package/app/utils/pipelineRender.ts +151 -0
  188. package/nuxt.config.ts +55 -0
  189. 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 &amp; 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>