@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,198 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from 'vue'
3
+ import type { Block, CloudProvider, InstanceSize } from '~/types/domain'
4
+ import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
5
+
6
+ // Service-level (frame) configuration: where the Tester's local-mode infra comes
7
+ // from (a docker-compose path, or an explicit "no infra dependencies" toggle — a
8
+ // Tester pipeline can't start until one is set), plus the cloud provider + instance
9
+ // size the service's container jobs run on. Autodiscovery suggests a compose path
10
+ // when the service is added; it can be set/changed here later — or browsed for in
11
+ // the backing repository.
12
+ const props = defineProps<{
13
+ block: Block
14
+ // Repo backing this service, supplied by the add-service modal when the block is
15
+ // too fresh to be resolvable from the stores yet. Otherwise resolved below.
16
+ repo?: { githubId: number; directory?: string | null }
17
+ }>()
18
+
19
+ const board = useBoardStore()
20
+ const accounts = useAccountsStore()
21
+ const github = useGitHubStore()
22
+ const services = useServicesStore()
23
+
24
+ const composePath = computed(() => props.block.testComposePath ?? '')
25
+ const noInfra = computed(() => props.block.noInfraDependencies === true)
26
+
27
+ // The repo + service subdirectory backing this frame, for the compose-file browser.
28
+ // A monorepo service isn't on the `github_repos` blockId link (that stays null), so
29
+ // fall back to the service catalog mapping, which carries the repo + directory.
30
+ const repoContext = computed<{ githubId: number; directory?: string | null } | undefined>(() => {
31
+ if (props.repo) return props.repo
32
+ const svc = services.serviceByFrameBlock[props.block.id]
33
+ if (svc?.repoGithubId != null) return { githubId: svc.repoGithubId, directory: svc.directory }
34
+ const r = github.repoForBlock(props.block.id)
35
+ return r ? { githubId: r.githubId } : undefined
36
+ })
37
+
38
+ // Compose-file picker: browse the repo and pin the compose file. The Tester runs
39
+ // `docker compose -f <path>` from the CLONE ROOT, so the stored path is relative to
40
+ // the repo root (the browser starts inside the service's subdirectory for convenience).
41
+ const browseOpen = ref(false)
42
+ const pickedPath = ref<string | undefined>(undefined)
43
+ function openBrowse() {
44
+ pickedPath.value = composePath.value || undefined
45
+ browseOpen.value = true
46
+ }
47
+ function applyPicked() {
48
+ if (pickedPath.value) setComposePath(pickedPath.value)
49
+ browseOpen.value = false
50
+ }
51
+
52
+ // A service with no explicit provider inherits the active account's default (else the
53
+ // built-in `cloudflare`); show that as the selected chip so the inherited value is visible.
54
+ const effectiveProvider = computed<CloudProvider>(
55
+ () => props.block.cloudProvider ?? accounts.activeAccount?.defaultCloudProvider ?? 'cloudflare',
56
+ )
57
+
58
+ function setComposePath(value: string) {
59
+ board.updateBlock(props.block.id, { testComposePath: value.trim() })
60
+ }
61
+ function toggleNoInfra(value: boolean) {
62
+ board.updateBlock(props.block.id, { noInfraDependencies: value })
63
+ }
64
+
65
+ const PROVIDERS: { value: CloudProvider; label: string }[] = [
66
+ { value: 'cloudflare', label: 'Cloudflare' },
67
+ { value: 'docker', label: 'Docker (local)' },
68
+ { value: 'aws', label: 'AWS' },
69
+ { value: 'gcp', label: 'GCP' },
70
+ { value: 'azure', label: 'Azure' },
71
+ { value: 'custom', label: 'Custom' },
72
+ ]
73
+ const SIZES: { value: InstanceSize; label: string }[] = [
74
+ { value: 'small', label: 'Small' },
75
+ { value: 'medium', label: 'Medium' },
76
+ { value: 'large', label: 'Large' },
77
+ { value: 'xlarge', label: 'XLarge' },
78
+ ]
79
+
80
+ function setProvider(value: CloudProvider) {
81
+ board.updateBlock(props.block.id, { cloudProvider: value })
82
+ }
83
+ function setSize(value: InstanceSize) {
84
+ board.updateBlock(props.block.id, { instanceSize: value })
85
+ }
86
+
87
+ const missingInfra = computed(() => !noInfra.value && composePath.value.trim() === '')
88
+ </script>
89
+
90
+ <template>
91
+ <div class="space-y-3">
92
+ <div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
93
+ Test infrastructure
94
+ </div>
95
+
96
+ <div class="space-y-1">
97
+ <label class="text-[11px] text-slate-400">docker-compose path</label>
98
+ <div class="flex items-center gap-1">
99
+ <UInput
100
+ :model-value="composePath"
101
+ size="xs"
102
+ class="flex-1"
103
+ placeholder="docker-compose.yml"
104
+ :disabled="noInfra"
105
+ @blur="(e: FocusEvent) => setComposePath((e.target as HTMLInputElement).value)"
106
+ @keydown.enter="
107
+ (e: KeyboardEvent) => setComposePath((e.target as HTMLInputElement).value)
108
+ "
109
+ />
110
+ <UButton
111
+ v-if="repoContext"
112
+ size="xs"
113
+ variant="soft"
114
+ color="neutral"
115
+ icon="i-lucide-folder-search"
116
+ :disabled="noInfra"
117
+ title="Browse the repository for the compose file"
118
+ @click="openBrowse"
119
+ />
120
+ </div>
121
+ <p class="text-[11px] leading-snug text-slate-500">
122
+ Used by the Tester to stand up the service's dependencies locally.
123
+ </p>
124
+ </div>
125
+
126
+ <UModal v-model:open="browseOpen" title="Select the docker-compose file">
127
+ <template #body>
128
+ <div v-if="repoContext" class="space-y-3">
129
+ <p class="text-xs text-slate-400">
130
+ Pick the compose file in the repository — its path is stored relative to the repo root.
131
+ </p>
132
+ <RepoTreeBrowser
133
+ v-model="pickedPath"
134
+ :repo-github-id="repoContext.githubId"
135
+ mode="file"
136
+ :start-path="repoContext.directory ?? ''"
137
+ />
138
+ <div class="flex items-center justify-between gap-2">
139
+ <p class="truncate text-xs text-slate-400">
140
+ <template v-if="pickedPath">
141
+ Selected: <code class="text-slate-200">{{ pickedPath }}</code>
142
+ </template>
143
+ <template v-else>No file selected.</template>
144
+ </p>
145
+ <UButton size="xs" color="primary" :disabled="!pickedPath" @click="applyPicked">
146
+ Use this file
147
+ </UButton>
148
+ </div>
149
+ </div>
150
+ </template>
151
+ </UModal>
152
+
153
+ <label class="flex items-center gap-2 text-[11px] text-slate-400">
154
+ <UCheckbox
155
+ :model-value="noInfra"
156
+ @update:model-value="(v: boolean | 'indeterminate') => toggleNoInfra(v === true)"
157
+ />
158
+ No infra dependencies (the Tester spins nothing up)
159
+ </label>
160
+
161
+ <p v-if="missingInfra" class="text-[11px] leading-snug text-amber-500">
162
+ Set a docker-compose path or enable “no infra dependencies”, otherwise a pipeline with a
163
+ Tester won't start.
164
+ </p>
165
+
166
+ <div class="space-y-1">
167
+ <span class="text-[11px] text-slate-400">Cloud provider</span>
168
+ <div class="flex flex-wrap gap-1">
169
+ <UButton
170
+ v-for="p in PROVIDERS"
171
+ :key="p.value"
172
+ :color="effectiveProvider === p.value ? 'primary' : 'neutral'"
173
+ :variant="effectiveProvider === p.value ? 'soft' : 'ghost'"
174
+ size="xs"
175
+ @click="setProvider(p.value)"
176
+ >
177
+ {{ p.label }}
178
+ </UButton>
179
+ </div>
180
+ </div>
181
+
182
+ <div class="space-y-1">
183
+ <span class="text-[11px] text-slate-400">Instance size</span>
184
+ <div class="flex flex-wrap gap-1">
185
+ <UButton
186
+ v-for="s in SIZES"
187
+ :key="s.value"
188
+ :color="(block.instanceSize ?? 'medium') === s.value ? 'primary' : 'neutral'"
189
+ :variant="(block.instanceSize ?? 'medium') === s.value ? 'soft' : 'ghost'"
190
+ size="xs"
191
+ @click="setSize(s.value)"
192
+ >
193
+ {{ s.label }}
194
+ </UButton>
195
+ </div>
196
+ </div>
197
+ </div>
198
+ </template>
@@ -0,0 +1,81 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import type { Block } from '~/types/domain'
4
+ import { useAgentConfigStore } from '~/stores/agentConfig'
5
+ import { useExecutionStore } from '~/stores/execution'
6
+
7
+ // Task-level configuration contributed by the agents in this task's selected
8
+ // pipeline (e.g. the Tester's environment: local vs ephemeral). Each value is
9
+ // editable until its contributing agent's step starts, then it freezes (the run is
10
+ // already consuming it). Persisted as a sparse id→value map on the block.
11
+ const props = defineProps<{ block: Block }>()
12
+
13
+ const board = useBoardStore()
14
+ const agentConfig = useAgentConfigStore()
15
+ const execution = useExecutionStore()
16
+
17
+ // The descriptors that apply: those contributed by the task's pinned pipeline, plus
18
+ // any whose value is already set (so an existing choice always stays visible/editable
19
+ // even if the pinned pipeline changed).
20
+ const descriptors = computed(() => {
21
+ const byPipeline = agentConfig.forPipeline(props.block.pipelineId)
22
+ const seen = new Set(byPipeline.map((d) => d.id))
23
+ const fromValues = Object.keys(props.block.agentConfig ?? {})
24
+ .filter((id) => !seen.has(id))
25
+ .map((id) => agentConfig.descriptors.find((d) => d.id === id))
26
+ .filter((d): d is NonNullable<typeof d> => Boolean(d))
27
+ return [...byPipeline, ...fromValues]
28
+ })
29
+
30
+ const run = computed(() => execution.getByBlock(props.block.id))
31
+
32
+ /** A descriptor freezes once its contributing agent's step has left `pending`. */
33
+ function isFrozen(agentKind: string): boolean {
34
+ const steps = run.value?.steps
35
+ if (!steps) return false
36
+ const step = steps.find((s) => s.agentKind === agentKind)
37
+ return Boolean(step && step.state !== 'pending')
38
+ }
39
+
40
+ function valueOf(id: string, fallback: string): string {
41
+ return props.block.agentConfig?.[id] ?? fallback
42
+ }
43
+
44
+ function setValue(id: string, value: string) {
45
+ const next = { ...props.block.agentConfig, [id]: value }
46
+ board.updateBlock(props.block.id, { agentConfig: next })
47
+ }
48
+ </script>
49
+
50
+ <template>
51
+ <div v-if="descriptors.length" class="space-y-3">
52
+ <div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
53
+ Agent configuration
54
+ </div>
55
+ <div v-for="d in descriptors" :key="d.id" class="space-y-1">
56
+ <div class="flex items-center justify-between">
57
+ <span class="text-[11px] text-slate-400">{{ d.label }}</span>
58
+ <UIcon
59
+ v-if="isFrozen(d.agentKind)"
60
+ name="i-lucide-lock"
61
+ class="h-3 w-3 text-slate-500"
62
+ title="Frozen — the agent has started"
63
+ />
64
+ </div>
65
+ <div class="flex flex-wrap gap-1">
66
+ <UButton
67
+ v-for="opt in d.options"
68
+ :key="opt.value"
69
+ :color="valueOf(d.id, d.default) === opt.value ? 'primary' : 'neutral'"
70
+ :variant="valueOf(d.id, d.default) === opt.value ? 'soft' : 'ghost'"
71
+ size="xs"
72
+ :disabled="isFrozen(d.agentKind)"
73
+ @click="setValue(d.id, opt.value)"
74
+ >
75
+ {{ opt.label }}
76
+ </UButton>
77
+ </div>
78
+ <p class="text-[11px] leading-snug text-slate-500">{{ d.description }}</p>
79
+ </div>
80
+ </div>
81
+ </template>
@@ -0,0 +1,70 @@
1
+ <script setup lang="ts">
2
+ import type { Block } from '~/types/domain'
3
+
4
+ const props = defineProps<{ block: Block }>()
5
+
6
+ const board = useBoardStore()
7
+ const { depLabel } = useDepLabels()
8
+
9
+ const deps = computed(() =>
10
+ (props.block.dependsOn ?? []).map((id) => board.getBlock(id)).filter((b): b is Block => !!b),
11
+ )
12
+ const runnable = computed(() => board.isRunnable(props.block.id))
13
+
14
+ /** Label a dependency relative to this task's container. */
15
+ const label = (dep: Block) => depLabel(dep, props.block.parentId)
16
+
17
+ // candidate tasks to depend on: any other task not already a dependency
18
+ const depMenu = computed(() => {
19
+ const current = new Set(props.block.dependsOn)
20
+ return board.allTasks
21
+ .filter((t) => t.id !== props.block.id && !current.has(t.id))
22
+ .map((t) => ({
23
+ label: label(t),
24
+ icon: 'i-lucide-plus',
25
+ onSelect: () => board.toggleDependency(props.block.id, t.id),
26
+ }))
27
+ })
28
+
29
+ function removeDep(depId: string) {
30
+ board.removeDependency(props.block.id, depId)
31
+ }
32
+ </script>
33
+
34
+ <template>
35
+ <div>
36
+ <div class="mb-1 flex items-center justify-between">
37
+ <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
38
+ Depends on
39
+ </span>
40
+ <UDropdownMenu v-if="depMenu.length" :items="depMenu">
41
+ <UButton
42
+ size="xs"
43
+ variant="ghost"
44
+ color="neutral"
45
+ icon="i-lucide-plus"
46
+ trailing-icon="i-lucide-chevron-down"
47
+ />
48
+ </UDropdownMenu>
49
+ </div>
50
+ <div v-if="deps.length" class="flex flex-wrap gap-1">
51
+ <UBadge
52
+ v-for="d in deps"
53
+ :key="d.id"
54
+ :color="d.status === 'done' ? 'neutral' : 'warning'"
55
+ variant="subtle"
56
+ size="sm"
57
+ class="cursor-pointer"
58
+ :title="d.status === 'done' ? 'Merged' : 'Not merged yet'"
59
+ @click="removeDep(d.id)"
60
+ >
61
+ {{ label(d) }}
62
+ <UIcon name="i-lucide-x" class="ml-0.5 h-3 w-3" />
63
+ </UBadge>
64
+ </div>
65
+ <div v-else class="text-[11px] text-slate-500">No dependencies — can run any time.</div>
66
+ <div v-if="!runnable" class="mt-1 text-[10px] text-amber-400">
67
+ Blocked until dependencies merge.
68
+ </div>
69
+ </div>
70
+ </template>
@@ -0,0 +1,56 @@
1
+ <script setup lang="ts">
2
+ // Compact display of a task's estimator triage (Complexity / Risk / Impact), shown on the
3
+ // inspector once a `task-estimator` step has run. Read-only — produced by the estimator,
4
+ // used to gate consensus steps. Hidden when no estimate exists.
5
+ import { computed } from 'vue'
6
+ import type { Block } from '~/types/domain'
7
+
8
+ const props = defineProps<{ block: Block }>()
9
+
10
+ const estimate = computed(() => props.block.estimate ?? null)
11
+
12
+ const AXES = [
13
+ { key: 'complexity', label: 'Complexity' },
14
+ { key: 'risk', label: 'Risk' },
15
+ { key: 'impact', label: 'Impact' },
16
+ ] as const
17
+
18
+ function pct(n: number): number {
19
+ return Math.round(n * 100)
20
+ }
21
+ /** Cool→hot bar colour by severity (low = sky, mid = amber, high = rose). */
22
+ function barClass(n: number): string {
23
+ if (n >= 0.66) return 'bg-rose-500'
24
+ if (n >= 0.33) return 'bg-amber-500'
25
+ return 'bg-sky-500'
26
+ }
27
+ </script>
28
+
29
+ <template>
30
+ <section v-if="estimate" class="space-y-2">
31
+ <div
32
+ class="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-slate-400"
33
+ >
34
+ <UIcon name="i-lucide-gauge" class="h-3.5 w-3.5" />
35
+ Estimate
36
+ </div>
37
+ <div class="space-y-1.5 rounded-lg border border-slate-800 bg-slate-900/40 p-2.5">
38
+ <div v-for="axis in AXES" :key="axis.key" class="flex items-center gap-2">
39
+ <span class="w-20 shrink-0 text-xs text-slate-400">{{ axis.label }}</span>
40
+ <div class="h-1.5 flex-1 overflow-hidden rounded-full bg-slate-800">
41
+ <div
42
+ class="h-full rounded-full"
43
+ :class="barClass(estimate[axis.key])"
44
+ :style="{ width: `${pct(estimate[axis.key])}%` }"
45
+ />
46
+ </div>
47
+ <span class="w-9 shrink-0 text-right text-xs tabular-nums text-slate-300"
48
+ >{{ pct(estimate[axis.key]) }}%</span
49
+ >
50
+ </div>
51
+ <p v-if="estimate.rationale" class="pt-1 text-xs leading-relaxed text-slate-500">
52
+ {{ estimate.rationale }}
53
+ </p>
54
+ </div>
55
+ </section>
56
+ </template>