@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,71 @@
1
+ <script setup lang="ts">
2
+ import { agentKindMeta } from '~/utils/catalog'
3
+
4
+ const execution = useExecutionStore()
5
+ const board = useBoardStore()
6
+ const ui = useUiStore()
7
+
8
+ const ctx = computed(() => ui.decisionContext)
9
+
10
+ const instance = computed(() => execution.getInstance(ctx.value?.instanceId))
11
+ const step = computed(() =>
12
+ instance.value?.steps.find((s) => s.decision?.id === ctx.value?.decisionId),
13
+ )
14
+ const decision = computed(() => step.value?.decision ?? null)
15
+ const block = computed(() => (instance.value ? board.getBlock(instance.value.blockId) : undefined))
16
+ const agent = computed(() => (step.value ? agentKindMeta(step.value.agentKind) : null))
17
+
18
+ const open = computed({
19
+ get: () => !!ctx.value && !!decision.value,
20
+ set: (v: boolean) => {
21
+ if (!v) ui.closeDecision()
22
+ },
23
+ })
24
+
25
+ function choose(option: string) {
26
+ if (!ctx.value) return
27
+ execution.resolveDecision(ctx.value.instanceId, ctx.value.decisionId, option)
28
+ ui.closeDecision()
29
+ }
30
+ </script>
31
+
32
+ <template>
33
+ <UModal v-model:open="open" title="Decision required">
34
+ <template #body>
35
+ <div v-if="decision && agent" class="space-y-4">
36
+ <div class="flex items-center gap-2 text-sm text-slate-400">
37
+ <div
38
+ class="flex h-8 w-8 items-center justify-center rounded-lg"
39
+ :style="{ backgroundColor: agent.color + '22' }"
40
+ >
41
+ <UIcon :name="agent.icon" class="h-4 w-4" :style="{ color: agent.color }" />
42
+ </div>
43
+ <div>
44
+ <span class="font-medium text-slate-200">{{ agent.label }}</span>
45
+ <span v-if="block"> on </span>
46
+ <span v-if="block" class="font-medium text-slate-200">{{ block.title }}</span>
47
+ </div>
48
+ </div>
49
+
50
+ <p class="text-base font-medium text-white">{{ decision.question }}</p>
51
+
52
+ <div class="grid gap-2">
53
+ <UButton
54
+ v-for="opt in decision.options"
55
+ :key="opt"
56
+ color="primary"
57
+ variant="soft"
58
+ block
59
+ class="justify-start"
60
+ @click="choose(opt)"
61
+ >
62
+ {{ opt }}
63
+ </UButton>
64
+ </div>
65
+ <p class="text-[11px] text-slate-500">
66
+ This is a visualization — any choice simply resumes the pipeline.
67
+ </p>
68
+ </div>
69
+ </template>
70
+ </UModal>
71
+ </template>
@@ -0,0 +1,465 @@
1
+ <script setup lang="ts">
2
+ import type { Block, BlockStatus } from '~/types/domain'
3
+ import { BLOCK_TYPE_META, STATUS_META } from '~/utils/catalog'
4
+ import TaskContextDocs from '~/components/documents/TaskContextDocs.vue'
5
+ import TaskContextIssues from '~/components/tasks/TaskContextIssues.vue'
6
+ import TaskAgentConfig from '~/components/panels/inspector/TaskAgentConfig.vue'
7
+ import ServiceTestConfig from '~/components/panels/inspector/ServiceTestConfig.vue'
8
+ import ServiceFragments from '~/components/panels/inspector/ServiceFragments.vue'
9
+ import ContainerSummary from '~/components/panels/inspector/ContainerSummary.vue'
10
+ import TaskDependencies from '~/components/panels/inspector/TaskDependencies.vue'
11
+ import TaskStructure from '~/components/panels/inspector/TaskStructure.vue'
12
+ import TaskRunSettings from '~/components/panels/inspector/TaskRunSettings.vue'
13
+ import TaskExecution from '~/components/panels/inspector/TaskExecution.vue'
14
+ import RecurringScheduleSettings from '~/components/panels/inspector/RecurringScheduleSettings.vue'
15
+ import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
16
+ import AgentStopButton from '~/components/board/AgentStopButton.vue'
17
+
18
+ const board = useBoardStore()
19
+ const pipelines = usePipelinesStore()
20
+ const execution = useExecutionStore()
21
+ const ui = useUiStore()
22
+ const documents = useDocumentsStore()
23
+ const tasks = useTasksStore()
24
+ const fragments = useFragmentsStore()
25
+ const agentRuns = useAgentRunsStore()
26
+ const github = useGitHubStore()
27
+ const recurring = useRecurringPipelinesStore()
28
+ const requirements = useRequirementsStore()
29
+
30
+ // When the selected task block backs a recurring pipeline, the inspector shows the
31
+ // schedule controls + history, and "Delete" removes the schedule (block + history).
32
+ const schedule = computed(() => (block.value ? recurring.byBlock(block.value.id) : undefined))
33
+
34
+ onMounted(() => {
35
+ fragments.ensureLoaded()
36
+ github.ensureLoaded()
37
+ })
38
+
39
+ /** Open the document import/spawn flow, targeting this container's frame. */
40
+ function spawnFromDocument() {
41
+ if (!block.value) return
42
+ const frameId = isFrame.value ? block.value.id : (board.serviceOf(block.value)?.id ?? null)
43
+ ui.openDocumentImport(frameId)
44
+ }
45
+
46
+ const block = computed<Block | undefined>(() =>
47
+ ui.selectedBlockId ? board.getBlock(ui.selectedBlockId) : undefined,
48
+ )
49
+ const level = computed(() => block.value?.level ?? 'frame')
50
+ const isFrame = computed(() => level.value === 'frame')
51
+ const isContainer = computed(() => level.value === 'frame' || level.value === 'module')
52
+ const isTask = computed(() => level.value === 'task')
53
+
54
+ const instance = computed(() => execution.getInstance(block.value?.executionId))
55
+ const typeMeta = computed(() => (block.value ? BLOCK_TYPE_META[block.value.type] : null))
56
+
57
+ // Containers show a derived activity status (never "done"); tasks use their own.
58
+ const FRAME_LABEL: Record<BlockStatus, string> = {
59
+ planned: 'No tasks',
60
+ ready: 'Live',
61
+ in_progress: 'Active',
62
+ blocked: 'Needs attention',
63
+ pr_ready: 'Active',
64
+ done: 'Live',
65
+ }
66
+ const effectiveStatus = computed<BlockStatus>(() =>
67
+ isContainer.value ? board.frameStatus(block.value!.id) : block.value!.status,
68
+ )
69
+ const statusMeta = computed(() => (block.value ? STATUS_META[effectiveStatus.value] : null))
70
+ const statusLabel = computed(() =>
71
+ isContainer.value ? FRAME_LABEL[effectiveStatus.value] : statusMeta.value!.label,
72
+ )
73
+
74
+ const runnable = computed(() => (block.value ? board.isRunnable(block.value.id) : false))
75
+
76
+ // The delete control names what it removes, so selecting a task and deleting it
77
+ // reads as "Delete task" rather than ambiguously removing the whole service.
78
+ const deleteLabel = computed(() =>
79
+ schedule.value
80
+ ? 'Delete recurring pipeline'
81
+ : isTask.value
82
+ ? 'Delete task'
83
+ : level.value === 'module'
84
+ ? 'Delete module'
85
+ : 'Delete service',
86
+ )
87
+
88
+ // A task is "started" once a pipeline has been launched on it (it has an
89
+ // execution, or has moved past the pre-run states). Until then the user can keep
90
+ // editing its title + description; afterwards those details are locked. Non-task
91
+ // containers (frames / modules) are always editable.
92
+ const started = computed(
93
+ () =>
94
+ isTask.value &&
95
+ (!!block.value?.executionId || !['planned', 'ready'].includes(block.value!.status)),
96
+ )
97
+ const editable = computed(() => !started.value)
98
+
99
+ function saveTitle() {
100
+ const b = block.value
101
+ if (!b) return
102
+ const next = b.title.trim()
103
+ if (next) board.updateBlock(b.id, { title: next })
104
+ }
105
+ function saveDescription() {
106
+ const b = block.value
107
+ if (!b) return
108
+ board.updateBlock(b.id, { description: b.description ?? '' })
109
+ }
110
+
111
+ // The GitHub repo backing this service (a frame), if one is linked. Linkage lives
112
+ // on the github_repos projection (its `blockId`), not on the block itself.
113
+ const serviceRepo = computed(() =>
114
+ isFrame.value && block.value ? github.repoForBlock(block.value.id) : undefined,
115
+ )
116
+ const serviceRepoUrl = computed(() =>
117
+ serviceRepo.value ? github.repoUrl(serviceRepo.value.githubId) : null,
118
+ )
119
+
120
+ // A task's work branch on GitHub, once the agent has pushed one (a PR branch is
121
+ // recorded on the block). Repo linkage lives on the owning service frame, not the
122
+ // task, so resolve the repo by walking up to the frame; fall back to deriving the
123
+ // repo base from the PR url when the projection hasn't loaded. Null until a branch
124
+ // exists, so the link only appears after one is created.
125
+ const taskBranchUrl = computed(() => {
126
+ const pr = isTask.value ? block.value?.pullRequest : undefined
127
+ if (!pr?.branch || !block.value) return null
128
+ const frame = board.serviceOf(block.value)
129
+ const repo = frame ? github.repoForBlock(frame.id) : undefined
130
+ const base = repo ? github.repoUrl(repo.githubId) : pr.url.replace(/\/pull\/\d+$/, '')
131
+ return base ? `${base}/tree/${pr.branch}` : null
132
+ })
133
+
134
+ const runMenu = computed(() =>
135
+ pipelines.pipelines.map((p) => ({
136
+ label: p.name,
137
+ icon: 'i-lucide-play',
138
+ onSelect: () => block.value && execution.start(block.value.id, p),
139
+ })),
140
+ )
141
+
142
+ function remove() {
143
+ if (!block.value) return
144
+ const id = block.value.id
145
+ // Close the inspector right away; the stores hide the block optimistically and
146
+ // restore it (with a toast) only if the backend delete fails.
147
+ ui.select(null)
148
+ // A schedule owns its reused block + run history — removing the schedule deletes
149
+ // them server-side, so delete the schedule rather than orphaning it.
150
+ if (schedule.value) {
151
+ void recurring.remove(schedule.value.id)
152
+ return
153
+ }
154
+ execution.cancel(id)
155
+ void board.removeBlock(id)
156
+ }
157
+
158
+ // ---- failed agent run (bootstrap or execution) ------------------------------
159
+ // A block whose current run failed surfaces the shared failure banner + retry,
160
+ // keyed by block id — covering a failed "bootstrap repo" frame and (for tasks) a
161
+ // failed pipeline execution alike.
162
+ const failedRun = computed(() => {
163
+ const run = block.value ? agentRuns.byBlock[block.value.id] : undefined
164
+ return run && run.status === 'failed' ? run : null
165
+ })
166
+
167
+ // A running run on a container frame (a "bootstrapping…" service). Tasks surface
168
+ // their own Stop in TaskExecution, so this covers the bootstrap case the board was
169
+ // previously unable to stop. Drives the inspector's Stop control.
170
+ const runningRun = computed(() => {
171
+ if (!isContainer.value) return null
172
+ const run = block.value ? agentRuns.byBlock[block.value.id] : undefined
173
+ return run && run.status === 'running' ? run : null
174
+ })
175
+
176
+ // ---- requirements review (collected requirements → react → rework) ----------
177
+ // The reviewer runs automatically as the first pipeline gate step (no manual entry
178
+ // point), but the inspector still probes + caches the block's review so the
179
+ // description can freeze in favour of the reworked requirements document once it
180
+ // exists (and so a prior incorporated doc can surface as a base after a reset).
181
+ watch(
182
+ () => (isTask.value ? block.value?.id : undefined),
183
+ (id) => {
184
+ if (id) void requirements.load(id)
185
+ },
186
+ { immediate: true },
187
+ )
188
+ const reqReview = computed(() => (block.value ? requirements.reviewFor(block.value.id) : null))
189
+ const reqReworked = computed(() => reqReview.value?.status === 'incorporated')
190
+ const reqReworkedText = computed(() => reqReview.value?.incorporatedRequirements ?? '')
191
+ // Once a task's requirements have been reworked, the standardized document is what
192
+ // every agent step consumes — so the raw description is frozen (read-only) and hidden
193
+ // behind an expander, with the reworked requirements taking focus instead.
194
+ const frozenByRework = computed(() => isTask.value && reqReworked.value)
195
+ // After a "stop & reset" the task is editable again (phase zero) but the LAST incorporated
196
+ // requirements survive on the review as a base to rework from — surfaced read-only here.
197
+ const reqHasPriorDoc = computed(() => isTask.value && !reqReworked.value && !!reqReworkedText.value)
198
+ const showOriginalDescription = ref(false)
199
+ </script>
200
+
201
+ <template>
202
+ <div
203
+ v-if="block && statusMeta && typeMeta"
204
+ class="absolute right-4 top-4 z-20 w-80 overflow-hidden rounded-2xl border border-slate-700 bg-slate-900/95 shadow-2xl backdrop-blur"
205
+ >
206
+ <div class="h-1.5 w-full" :style="{ backgroundColor: statusMeta.color }" />
207
+ <!-- A tall task (execution steps + scenarios + docs) can overflow the
208
+ viewport; cap the body height and let it scroll so the lower controls
209
+ (Run / Focus / Delete) stay reachable. The status bar above stays put. -->
210
+ <div class="max-h-[calc(100vh-5rem)] space-y-4 overflow-y-auto p-4">
211
+ <!-- header -->
212
+ <div class="flex items-start justify-between gap-2">
213
+ <div class="flex items-center gap-2">
214
+ <div
215
+ class="flex h-9 w-9 items-center justify-center rounded-lg"
216
+ :style="{ backgroundColor: typeMeta.accent + '22' }"
217
+ >
218
+ <UIcon :name="typeMeta.icon" class="h-5 w-5" :style="{ color: typeMeta.accent }" />
219
+ </div>
220
+ <div>
221
+ <div class="text-sm font-semibold text-white">{{ block.title }}</div>
222
+ <div class="mt-0.5 flex items-center gap-1.5">
223
+ <UBadge :color="statusMeta.chip as any" variant="subtle" size="sm">
224
+ {{ statusLabel }}
225
+ </UBadge>
226
+ <span class="text-[10px] uppercase tracking-wide text-slate-500">{{ level }}</span>
227
+ </div>
228
+ </div>
229
+ </div>
230
+ <UButton
231
+ icon="i-lucide-x"
232
+ color="neutral"
233
+ variant="ghost"
234
+ size="xs"
235
+ @click="ui.select(null)"
236
+ />
237
+ </div>
238
+
239
+ <!-- editable identity: title + description. Edits persist to the backend.
240
+ A task's details lock once it has been started (a pipeline launched), and
241
+ once its requirements have been reworked the description is frozen in favour
242
+ of the standardized requirements document. -->
243
+ <div class="space-y-2">
244
+ <UInput
245
+ v-if="editable && !frozenByRework"
246
+ v-model="block.title"
247
+ size="sm"
248
+ class="w-full"
249
+ placeholder="Title…"
250
+ @change="saveTitle"
251
+ @blur="saveTitle"
252
+ />
253
+
254
+ <!-- reworked: the standardized requirements document takes focus; the raw
255
+ description is frozen and tucked behind an expander. -->
256
+ <template v-if="frozenByRework">
257
+ <div class="rounded-lg border border-emerald-900/60 bg-emerald-950/20 p-3">
258
+ <div
259
+ class="mb-1.5 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-emerald-400"
260
+ >
261
+ <UIcon name="i-lucide-file-check-2" class="h-3.5 w-3.5" />
262
+ Reworked requirements
263
+ </div>
264
+ <p class="line-clamp-5 whitespace-pre-line text-[13px] leading-relaxed text-slate-300">
265
+ {{ reqReworkedText }}
266
+ </p>
267
+ <div class="mt-2 flex items-center justify-between gap-2">
268
+ <p class="text-[11px] text-slate-500">Agent steps use this document.</p>
269
+ <UButton
270
+ color="neutral"
271
+ variant="link"
272
+ size="xs"
273
+ icon="i-lucide-maximize-2"
274
+ @click="ui.openRequirementReview(block.id)"
275
+ >
276
+ Open
277
+ </UButton>
278
+ </div>
279
+ </div>
280
+ <UButton
281
+ color="neutral"
282
+ variant="ghost"
283
+ size="xs"
284
+ :icon="showOriginalDescription ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
285
+ @click="showOriginalDescription = !showOriginalDescription"
286
+ >
287
+ Original description (frozen)
288
+ </UButton>
289
+ <UTextarea
290
+ v-if="showOriginalDescription"
291
+ :model-value="block.description"
292
+ :rows="2"
293
+ autoresize
294
+ size="sm"
295
+ class="w-full"
296
+ disabled
297
+ placeholder="No description"
298
+ />
299
+ </template>
300
+
301
+ <!-- normal: editable (or started-locked) description -->
302
+ <template v-else>
303
+ <UTextarea
304
+ v-model="block.description"
305
+ :rows="2"
306
+ autoresize
307
+ size="sm"
308
+ class="w-full"
309
+ :disabled="!editable"
310
+ placeholder="Describe this block…"
311
+ @change="saveDescription"
312
+ @blur="saveDescription"
313
+ />
314
+ <p v-if="isTask && !editable" class="flex items-center gap-1 text-[11px] text-slate-500">
315
+ <UIcon name="i-lucide-lock" class="h-3 w-3" />
316
+ This task has started — its details are locked.
317
+ </p>
318
+
319
+ <!-- prior incorporated requirements kept as a base after a review-driven reset -->
320
+ <div v-if="reqHasPriorDoc" class="rounded-lg border border-slate-700 bg-slate-800/40 p-3">
321
+ <div
322
+ class="mb-1.5 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-400"
323
+ >
324
+ <UIcon name="i-lucide-history" class="h-3.5 w-3.5" />
325
+ Last incorporated requirements
326
+ </div>
327
+ <p class="line-clamp-5 whitespace-pre-line text-[13px] leading-relaxed text-slate-300">
328
+ {{ reqReworkedText }}
329
+ </p>
330
+ <p class="mt-2 text-[11px] text-slate-500">
331
+ From the previous review. Use it as a base — edit the description above and resubmit.
332
+ </p>
333
+ </div>
334
+ </template>
335
+ </div>
336
+
337
+ <!-- failed run (bootstrap or execution): shared failure banner + retry -->
338
+ <AgentFailureCard v-if="failedRun" :run="failedRun" />
339
+
340
+ <!-- running bootstrap: let the user stop it (kills the container) -->
341
+ <div
342
+ v-else-if="runningRun"
343
+ class="flex items-center justify-between gap-2 rounded-lg border border-amber-900/60 bg-amber-950/30 px-3 py-2"
344
+ >
345
+ <span class="flex items-center gap-1.5 text-xs text-amber-300">
346
+ <UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 animate-spin" />
347
+ Bootstrapping…
348
+ </span>
349
+ <AgentStopButton :run-id="runningRun.runId" :kind="runningRun.kind" size="xs" />
350
+ </div>
351
+
352
+ <!-- external links -->
353
+ <div class="flex flex-wrap gap-2">
354
+ <UButton
355
+ v-if="serviceRepoUrl"
356
+ :to="serviceRepoUrl"
357
+ target="_blank"
358
+ rel="noopener"
359
+ color="neutral"
360
+ variant="soft"
361
+ size="xs"
362
+ icon="i-lucide-github"
363
+ trailing-icon="i-lucide-external-link"
364
+ >
365
+ {{ serviceRepo!.owner }}/{{ serviceRepo!.name }}
366
+ </UButton>
367
+ <UButton
368
+ v-if="taskBranchUrl"
369
+ :to="taskBranchUrl"
370
+ target="_blank"
371
+ rel="noopener"
372
+ color="neutral"
373
+ variant="soft"
374
+ size="xs"
375
+ icon="i-lucide-git-branch"
376
+ trailing-icon="i-lucide-external-link"
377
+ >
378
+ {{ block!.pullRequest!.branch }}
379
+ </UButton>
380
+ <UButton
381
+ v-if="tasks.available"
382
+ color="neutral"
383
+ variant="soft"
384
+ size="xs"
385
+ icon="i-lucide-ticket"
386
+ @click="ui.openTaskImport()"
387
+ >
388
+ {{ tasks.anyConnected ? 'Import Jira issue' : 'Connect Jira' }}
389
+ </UButton>
390
+ <UButton
391
+ v-if="isContainer && documents.available && documents.anyConnected"
392
+ color="neutral"
393
+ variant="soft"
394
+ size="xs"
395
+ icon="i-lucide-wand-sparkles"
396
+ @click="spawnFromDocument"
397
+ >
398
+ Spawn from document
399
+ </UButton>
400
+ </div>
401
+
402
+ <!-- task: context documents -->
403
+ <TaskContextDocs v-if="isTask" :block="block" />
404
+
405
+ <!-- task: context issues (tracker) -->
406
+ <TaskContextIssues v-if="isTask" :block="block" />
407
+
408
+ <!-- service / module: tasks summary -->
409
+ <ContainerSummary v-if="isContainer" :block="block" />
410
+ <!-- service (frame): test infra + provisioning configuration -->
411
+ <ServiceTestConfig v-if="isFrame" :block="block" />
412
+
413
+ <!-- service (frame): best-practice fragments for code-aware agents -->
414
+ <ServiceFragments v-if="isFrame" :block="block" />
415
+
416
+ <!-- task: dependencies, structure, agent config, run settings, execution -->
417
+ <template v-else-if="isTask">
418
+ <RecurringScheduleSettings :block="block" />
419
+ <TaskDependencies :block="block" />
420
+ <TaskStructure :block="block" />
421
+ <TaskAgentConfig :block="block" />
422
+ <TaskEstimateBadge :block="block" />
423
+ <TaskRunSettings :block="block" />
424
+ <TaskExecution :block="block" />
425
+ </template>
426
+
427
+ <!-- actions -->
428
+ <div class="flex items-center gap-2">
429
+ <UDropdownMenu v-if="isTask" :items="runMenu">
430
+ <UButton
431
+ :color="runnable ? 'primary' : 'neutral'"
432
+ variant="soft"
433
+ size="sm"
434
+ :icon="runnable ? 'i-lucide-play' : 'i-lucide-lock'"
435
+ trailing-icon="i-lucide-chevron-down"
436
+ :disabled="!runnable"
437
+ >
438
+ {{ instance ? 'Re-run' : 'Run' }}
439
+ </UButton>
440
+ </UDropdownMenu>
441
+ <UButton
442
+ v-if="isTask"
443
+ color="neutral"
444
+ variant="soft"
445
+ size="sm"
446
+ icon="i-lucide-maximize-2"
447
+ @click="ui.focus(block.id)"
448
+ >
449
+ Focus
450
+ </UButton>
451
+ <UButton
452
+ color="error"
453
+ variant="ghost"
454
+ size="sm"
455
+ icon="i-lucide-trash-2"
456
+ class="ml-auto"
457
+ :title="deleteLabel"
458
+ @click="remove"
459
+ >
460
+ {{ deleteLabel }}
461
+ </UButton>
462
+ </div>
463
+ </div>
464
+ </div>
465
+ </template>