@cat-factory/app 0.311.2 → 0.312.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 (210) hide show
  1. package/README.md +92 -4
  2. package/app/app.config.ts +7 -0
  3. package/app/components/assistant/AssistantModal.vue +2 -5
  4. package/app/components/auth/AuthGate.vue +2 -1
  5. package/app/components/binaryCandidates/BinaryCandidatesWindow.vue +14 -15
  6. package/app/components/binaryOutput/StoredAssetView.vue +1 -3
  7. package/app/components/board/AddTaskModal.vue +3 -10
  8. package/app/components/board/AgentFailureCard.vue +4 -5
  9. package/app/components/board/AgentStopButton.vue +5 -7
  10. package/app/components/board/BoardCanvas.vue +31 -30
  11. package/app/components/board/CreateInitiativeModal.vue +2 -4
  12. package/app/components/board/RecurringPipelineModal.vue +2 -7
  13. package/app/components/board/nodes/AgentChip.vue +3 -2
  14. package/app/components/board/nodes/BlockNode.vue +14 -11
  15. package/app/components/board/nodes/InitiativeCard.vue +6 -3
  16. package/app/components/board/nodes/TaskCard.vue +45 -212
  17. package/app/components/bootstrap/AdoptionReviewModal.vue +2 -5
  18. package/app/components/bootstrap/BootstrapModal.vue +6 -15
  19. package/app/components/bootstrap/BootstrapRunSteps.vue +3 -1
  20. package/app/components/brainstorm/BrainstormWindow.vue +32 -26
  21. package/app/components/bugFishing/BugFishingWindow.vue +15 -13
  22. package/app/components/clarity/ClarityReviewWindow.vue +31 -26
  23. package/app/components/common/ClarificationItem.vue +3 -1
  24. package/app/components/common/EmptyState.vue +41 -19
  25. package/app/components/common/InterviewGateNotice.vue +3 -1
  26. package/app/components/common/Spinner.vue +28 -0
  27. package/app/components/consensus/ConsensusSessionWindow.vue +5 -5
  28. package/app/components/docs/DocInterviewWindow.vue +6 -6
  29. package/app/components/documents/ContextDocumentPicker.vue +2 -2
  30. package/app/components/documents/DocumentImportModal.vue +9 -8
  31. package/app/components/documents/DocumentSourceConnectModal.vue +10 -10
  32. package/app/components/documents/DocumentTemplatesModal.vue +13 -7
  33. package/app/components/documents/RepoContextDocPicker.vue +2 -2
  34. package/app/components/documents/SpawnPreviewModal.vue +4 -6
  35. package/app/components/documents/StartFromDesignModal.vue +2 -1
  36. package/app/components/documents/TaskContextDocs.vue +4 -5
  37. package/app/components/environments/EnvironmentStatusPanel.vue +11 -8
  38. package/app/components/environments/steps/EnvPickStep.vue +7 -3
  39. package/app/components/environments/steps/EnvPreflightStep.vue +6 -5
  40. package/app/components/focus/BlockFocusView.vue +6 -5
  41. package/app/components/followUp/FollowUpWindow.vue +7 -9
  42. package/app/components/forkDecision/ForkDecisionWindow.vue +9 -8
  43. package/app/components/foundational/FoundationalServiceCatalogList.vue +9 -5
  44. package/app/components/foundational/FoundationalServiceRegistry.vue +11 -7
  45. package/app/components/foundational/FoundationalServiceSources.vue +20 -22
  46. package/app/components/foundational/FoundationalSuppressions.vue +2 -2
  47. package/app/components/foundational/ServiceCatalogConnection.vue +15 -15
  48. package/app/components/fragments/FragmentLibraryManager.vue +36 -34
  49. package/app/components/fragments/FragmentSelector.vue +7 -3
  50. package/app/components/gates/GateResultView.vue +6 -9
  51. package/app/components/github/AddServiceFromRepoModal.vue +6 -10
  52. package/app/components/github/GitHubConnect.vue +10 -17
  53. package/app/components/github/GitHubPanel.vue +44 -47
  54. package/app/components/github/RepoTreeBrowser.vue +9 -5
  55. package/app/components/guidedReview/GuidedReviewModal.vue +16 -6
  56. package/app/components/guidedReview/GuidedReviewOverview.vue +2 -1
  57. package/app/components/guidedReview/GuidedReviewPicker.vue +10 -7
  58. package/app/components/guidedReview/GuidedReviewThread.vue +9 -4
  59. package/app/components/humanTest/HumanTestWindow.vue +2 -1
  60. package/app/components/initiative/InitiativePlanningWindow.vue +6 -6
  61. package/app/components/initiative/InitiativeTrackerWindow.vue +11 -11
  62. package/app/components/judge/JudgeResultView.vue +2 -7
  63. package/app/components/kaizen/KaizenPanel.vue +20 -5
  64. package/app/components/kaizen/KaizenStepStatus.vue +2 -1
  65. package/app/components/layout/AccountAuditLog.vue +11 -3
  66. package/app/components/layout/AccountDeploymentSettings.vue +17 -62
  67. package/app/components/layout/AccountFailureKindRules.vue +6 -5
  68. package/app/components/layout/AccountModelPolicySettings.vue +2 -6
  69. package/app/components/layout/AccountPlatformAlertSettings.vue +5 -13
  70. package/app/components/layout/AccountRiskPolicySettings.vue +13 -14
  71. package/app/components/layout/AccountRunCredentialSettings.vue +2 -2
  72. package/app/components/layout/AccountTeamSettings.vue +12 -9
  73. package/app/components/layout/BoardSwitcher.vue +2 -2
  74. package/app/components/layout/BoardToolbar.vue +27 -37
  75. package/app/components/layout/BoardTopOverlays.vue +8 -2
  76. package/app/components/layout/CommandBar.vue +7 -3
  77. package/app/components/layout/ConnectionStatusBanner.vue +2 -1
  78. package/app/components/layout/IntegrationsHub.vue +7 -3
  79. package/app/components/layout/ModelProvidersHub.vue +7 -3
  80. package/app/components/layout/NotificationsInbox.vue +3 -11
  81. package/app/components/layout/SideBar.vue +5 -0
  82. package/app/components/layout/WorkViewSwitcher.vue +64 -0
  83. package/app/components/layout/WorkspaceMembersSettings.vue +12 -5
  84. package/app/components/media/ArtifactLightbox.vue +4 -3
  85. package/app/components/notifications/NotificationSettingsPanel.vue +4 -11
  86. package/app/components/observability/ToolCallList.vue +12 -17
  87. package/app/components/panels/AgentStepDetail.vue +5 -5
  88. package/app/components/panels/GenericStructuredResultView.vue +6 -9
  89. package/app/components/panels/InspectorPanel.vue +2 -1
  90. package/app/components/panels/MergerResultView.vue +5 -6
  91. package/app/components/panels/ObservabilityPanel.vue +26 -41
  92. package/app/components/panels/OperatorDashboardPanel.vue +29 -17
  93. package/app/components/panels/ReportsPanel.vue +17 -11
  94. package/app/components/panels/ReportsSpendBreakdown.vue +7 -3
  95. package/app/components/panels/RunDetailLoadState.vue +4 -3
  96. package/app/components/panels/StepContainerStatus.vue +4 -3
  97. package/app/components/panels/StepDelegatedStatus.vue +3 -2
  98. package/app/components/panels/StepMetadataCard.vue +6 -8
  99. package/app/components/panels/StepRunMeta.vue +2 -5
  100. package/app/components/panels/inspector/ContainerSummary.vue +7 -1
  101. package/app/components/panels/inspector/EpicChildren.vue +7 -3
  102. package/app/components/panels/inspector/FrontendConfig.vue +7 -3
  103. package/app/components/panels/inspector/InitiativeInspector.vue +5 -2
  104. package/app/components/panels/inspector/ServiceConnections.vue +7 -3
  105. package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +2 -6
  106. package/app/components/panels/inspector/ServiceTestSecrets.vue +2 -6
  107. package/app/components/panels/inspector/ServiceTestingContext.vue +2 -6
  108. package/app/components/panels/inspector/ServiceValidationConfig.vue +38 -39
  109. package/app/components/panels/inspector/TaskExecution.vue +5 -6
  110. package/app/components/panels/inspector/TaskRunSettings.vue +7 -3
  111. package/app/components/panels/inspector/TaskStructure.vue +0 -1
  112. package/app/components/pipeline/AgentPromptEditor.vue +25 -18
  113. package/app/components/pipeline/PipelineBuilder.vue +14 -24
  114. package/app/components/pipeline/PipelineProgress.vue +16 -17
  115. package/app/components/prReview/PrReviewPhaseBadge.vue +3 -2
  116. package/app/components/prReview/PrReviewWindow.vue +16 -20
  117. package/app/components/providers/ApiKeysSection.vue +2 -6
  118. package/app/components/providers/PersonalCredentialModal.vue +2 -6
  119. package/app/components/providers/PersonalSubscriptionSection.vue +3 -7
  120. package/app/components/providers/VendorCredentialsModal.vue +2 -6
  121. package/app/components/provisioning/ProvisioningLogsDrawer.vue +10 -6
  122. package/app/components/queue/QueueCard.vue +269 -0
  123. package/app/components/queue/QueueView.vue +192 -0
  124. package/app/components/ralph/RalphLoopResultView.vue +2 -7
  125. package/app/components/requirements/RequirementsReviewWindow.vue +44 -39
  126. package/app/components/sandbox/SandboxPanel.vue +37 -25
  127. package/app/components/settings/AccountSettingsPanel.vue +6 -3
  128. package/app/components/settings/ApiTokensPanel.vue +2 -6
  129. package/app/components/settings/BudgetSettings.vue +2 -2
  130. package/app/components/settings/CapabilityCredentialsPanel.vue +2 -6
  131. package/app/components/settings/CloudflareHandlerSection.vue +3 -7
  132. package/app/components/settings/ConsensusGroupsSection.vue +7 -3
  133. package/app/components/settings/CustomManifestTypeEditor.vue +7 -3
  134. package/app/components/settings/DefaultProvisionTypeSection.vue +2 -2
  135. package/app/components/settings/InfraHandlersConfigurator.vue +8 -11
  136. package/app/components/settings/IssueTrackerPanel.vue +9 -9
  137. package/app/components/settings/LocalContainerPoolSettings.vue +2 -6
  138. package/app/components/settings/LocalModelEndpointsPanel.vue +5 -7
  139. package/app/components/settings/McpAuthorizeScreen.vue +8 -3
  140. package/app/components/settings/McpOAuthCallbackScreen.vue +2 -1
  141. package/app/components/settings/ModelConfigurationPanel.vue +45 -17
  142. package/app/components/settings/ObservabilityConnectionPanel.vue +4 -15
  143. package/app/components/settings/OpenRouterCatalogPanel.vue +8 -24
  144. package/app/components/settings/PackageRegistriesPanel.vue +2 -6
  145. package/app/components/settings/ProviderConnectionTab.vue +3 -7
  146. package/app/components/settings/RiskPolicyPanel.vue +4 -16
  147. package/app/components/settings/SharedStacksPanel.vue +8 -18
  148. package/app/components/settings/TaskTypeSuppressionsPanel.vue +10 -6
  149. package/app/components/settings/UsageSettings.vue +11 -2
  150. package/app/components/settings/UserSecretsSection.vue +5 -7
  151. package/app/components/settings/WorkspaceMetadataSettings.vue +9 -9
  152. package/app/components/settings/WorkspaceSettingsPanel.vue +3 -11
  153. package/app/components/skills/ReviewSkillQueue.vue +7 -3
  154. package/app/components/skills/SkillLibraryManager.vue +20 -18
  155. package/app/components/slack/SlackPanel.vue +5 -8
  156. package/app/components/spec/ServiceSpecWindow.vue +32 -29
  157. package/app/components/tasks/BugHuntModal.vue +23 -19
  158. package/app/components/tasks/ContextIssuePicker.vue +2 -2
  159. package/app/components/tasks/TaskContextIssues.vue +4 -5
  160. package/app/components/tasks/TaskImportModal.vue +10 -8
  161. package/app/components/tasks/TaskSourceConnectModal.vue +11 -11
  162. package/app/components/testing/TestReportWindow.vue +6 -12
  163. package/app/components/tutorial/TutorialCatalogue.vue +7 -3
  164. package/app/components/tutorial/TutorialOverlay.vue +2 -1
  165. package/app/components/tutorial/TutorialPrompt.vue +7 -3
  166. package/app/components/vcs/GitLabConnect.vue +2 -6
  167. package/app/components/visualConfirm/VisualConfirmationWindow.vue +9 -4
  168. package/app/composables/toastTitleKeys.spec.ts +143 -0
  169. package/app/composables/useActionToast.spec.ts +71 -0
  170. package/app/composables/useActionToast.ts +101 -0
  171. package/app/composables/useBlockDeletion.ts +3 -6
  172. package/app/composables/useConfirmAction.ts +5 -3
  173. package/app/composables/useContextLinking.spec.ts +3 -0
  174. package/app/composables/useContextLinking.ts +11 -16
  175. package/app/composables/useCopyToClipboard.ts +4 -3
  176. package/app/composables/useFrameLanes.ts +6 -82
  177. package/app/composables/useNavContributions.ts +6 -5
  178. package/app/composables/usePipelineErrorToast.spec.ts +24 -0
  179. package/app/composables/usePipelineErrorToast.ts +26 -2
  180. package/app/composables/usePipelineLibraryActions.ts +8 -9
  181. package/app/composables/useTaskActions.ts +254 -0
  182. package/app/composables/useTaskLaneClassifier.ts +88 -0
  183. package/app/modular/nav-contributions.spec.ts +4 -0
  184. package/app/modular/nav-contributions.ts +31 -0
  185. package/app/modular/tutorial-tours.ts +10 -0
  186. package/app/pages/index.vue +42 -8
  187. package/app/stores/board/context.ts +4 -3
  188. package/app/stores/board/placement.ts +8 -20
  189. package/app/stores/board/removal.ts +4 -13
  190. package/app/stores/board.spec.ts +10 -1
  191. package/app/stores/board.ts +3 -2
  192. package/app/stores/workView.ts +51 -0
  193. package/app/stores/workspaceQueue.ts +148 -0
  194. package/app/utils/bootstrapSteps.ts +11 -2
  195. package/app/utils/pipelineRender.ts +21 -8
  196. package/app/utils/queueSections.spec.ts +111 -0
  197. package/app/utils/queueSections.ts +117 -0
  198. package/app/utils/workView.spec.ts +16 -0
  199. package/app/utils/workView.ts +25 -0
  200. package/i18n/locales/de.json +73 -32
  201. package/i18n/locales/en.json +73 -32
  202. package/i18n/locales/es.json +73 -32
  203. package/i18n/locales/fr.json +73 -32
  204. package/i18n/locales/he.json +73 -32
  205. package/i18n/locales/it.json +73 -32
  206. package/i18n/locales/ja.json +73 -32
  207. package/i18n/locales/pl.json +73 -32
  208. package/i18n/locales/tr.json +73 -32
  209. package/i18n/locales/uk.json +73 -32
  210. package/package.json +1 -1
@@ -0,0 +1,269 @@
1
+ <script setup lang="ts">
2
+ import { agentKindMeta } from '~/utils/catalog'
3
+ import { formatDuration, useNowTick } from '~/composables/useStepTimer'
4
+ import { useFramePlacement } from '~/composables/useFramePlacement'
5
+ import { LANE_REASON_LABEL_KEYS } from '~/utils/swimlanes'
6
+ import type { QueueEntry } from '~/utils/queueSections'
7
+ import IconButton from '~/components/common/IconButton.vue'
8
+ import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
9
+
10
+ /**
11
+ * One task in the queue: why it is here, where it lives, and the ONE thing to do about it.
12
+ *
13
+ * The board's card answers "what is this task's state"; this card answers "what do I do next",
14
+ * so it carries a single primary action derived from the lane REASON, and the inspector one
15
+ * click away for everything else. The actions themselves are the board card's
16
+ * (`useTaskActions`), so Start, Resolve and Merge behave identically on both surfaces, and a failed
17
+ * run shows the same `AgentFailureCard` the board card shows, with its Retry.
18
+ *
19
+ * Every action is `soft`, as on the board card: the queue lists many cards at once, and a solid
20
+ * button on each would put many primaries in one view.
21
+ */
22
+ const props = defineProps<{ entry: QueueEntry }>()
23
+
24
+ const { t } = useI18n()
25
+ const ui = useUiStore()
26
+ const execution = useExecutionStore()
27
+ const agentRuns = useAgentRunsStore()
28
+ const views = useWorkViewStore()
29
+ const { focusFrame } = useFramePlacement()
30
+ // A shared 30s clock, so "waited 4m" advances on a quiet board instead of freezing until some
31
+ // unrelated event re-evaluates the card.
32
+ const nowTick = useNowTick(30_000)
33
+
34
+ const taskId = () => props.entry.task.id
35
+ const {
36
+ task,
37
+ runnable,
38
+ canExecute,
39
+ executeDeniedTitle,
40
+ unmet,
41
+ sandboxed,
42
+ outcomeReadable,
43
+ openOutcome,
44
+ starting,
45
+ start,
46
+ merge,
47
+ pendingDecision,
48
+ reviewStageLabel,
49
+ attention,
50
+ } = useTaskActions(taskId)
51
+
52
+ const reason = computed(() => props.entry.reason)
53
+ const run = computed(() => execution.getByBlock(props.entry.task.id) ?? null)
54
+ const agentRun = computed(() => agentRuns.byBlock[props.entry.task.id])
55
+ const runFailed = computed(() => agentRun.value?.status === 'failed')
56
+
57
+ /** The eyebrow's time fact: how long a human has kept it waiting, or how fresh the run is. */
58
+ const timeFact = computed(() => {
59
+ const now = nowTick.value
60
+ // The clock reads 0 until the card has mounted and subscribed.
61
+ if (!now) return null
62
+ const e = props.entry
63
+ if (reason.value === 'merged' && e.task.completedAt != null)
64
+ return t('queue.card.merged', { duration: formatDuration(now - e.task.completedAt) })
65
+ if (e.waitingSince != null)
66
+ return t('queue.card.waited', { duration: formatDuration(now - e.waitingSince) })
67
+ if ((reason.value === 'running' || reason.value === 'background_review') && e.activityAt != null)
68
+ return t('queue.card.active', { duration: formatDuration(now - e.activityAt) })
69
+ return null
70
+ })
71
+
72
+ /** The step a live run is on, as "step 5 of 9 · Coder". Null when no run is in flight. */
73
+ const stepFact = computed(() => {
74
+ const r = run.value
75
+ if (!r || r.steps.length === 0) return null
76
+ const step = r.steps[r.currentStep]
77
+ const position = t('queue.card.step', {
78
+ current: Math.min(r.currentStep + 1, r.steps.length),
79
+ total: r.steps.length,
80
+ })
81
+ return step ? `${position} · ${agentKindMeta(step.agentKind).label}` : position
82
+ })
83
+
84
+ /** One line of context: what the human would want to know before pressing the button. */
85
+ const context = computed<string | null>(() => {
86
+ switch (reason.value) {
87
+ case 'decision':
88
+ return pendingDecision.value?.decision.question ?? null
89
+ case 'budget_paused':
90
+ return t('queue.card.budgetPaused')
91
+ case 'parked':
92
+ case 'unclassified':
93
+ return t('queue.card.parked')
94
+ case 'dependencies':
95
+ return t('queue.card.waitingOn', { deps: unmet.value.map((d) => d.title).join(', ') })
96
+ case 'background_review':
97
+ return reviewStageLabel.value
98
+ default:
99
+ return null
100
+ }
101
+ })
102
+
103
+ /**
104
+ * The eyebrow's first word. The lane reason, except in the merge column, whose header already says
105
+ * "ready to merge": there the pull request's own number is the more useful fact.
106
+ */
107
+ const eyebrow = computed(() => {
108
+ const pr = props.entry.task.pullRequest
109
+ if (reason.value === 'pr_awaiting_merge' && pr?.number)
110
+ return t('queue.card.pr', { number: pr.number })
111
+ return t(LANE_REASON_LABEL_KEYS[reason.value])
112
+ })
113
+
114
+ const showProgress = computed(
115
+ () => reason.value === 'running' || reason.value === 'background_review',
116
+ )
117
+
118
+ /** The inspector is showing this card's task. Its border wins over the hover one. */
119
+ const selected = computed(() => ui.selectedBlockId === props.entry.task.id)
120
+
121
+ function openTask() {
122
+ ui.select(props.entry.task.id)
123
+ }
124
+
125
+ /**
126
+ * Leave the queue for the canvas, with this task selected and its service in view. The canvas
127
+ * kept its own camera while the queue covered it, so without the pan the task is usually off
128
+ * screen. A task has no position of its own (it lives in its frame's swimlanes), so the camera
129
+ * centres on the frame.
130
+ */
131
+ function showOnBoard() {
132
+ views.show('board')
133
+ ui.select(props.entry.task.id)
134
+ void focusFrame(props.entry.service?.id ?? props.entry.task.id)
135
+ }
136
+ </script>
137
+
138
+ <template>
139
+ <!-- The whole card opens the task, as the board's card does: a click anywhere on it selects
140
+ the task, and each control inside stops its own click. The title stays a button, so the
141
+ card is reachable by keyboard. -->
142
+ <article
143
+ v-if="task"
144
+ class="rounded-lg border bg-default p-3 transition-colors"
145
+ :class="selected ? 'border-inverted' : 'border-muted hover:border-accented'"
146
+ @click="openTask"
147
+ data-testid="queue-card"
148
+ :data-queue-task="task.id"
149
+ :data-reason="reason"
150
+ >
151
+ <div class="flex items-center gap-1.5 text-3xs uppercase tracking-wide text-dimmed">
152
+ <span
153
+ class="shrink-0 font-semibold whitespace-nowrap"
154
+ :class="
155
+ reason === 'failed'
156
+ ? 'text-error'
157
+ : entry.reason === 'pr_awaiting_merge'
158
+ ? 'text-success'
159
+ : 'text-highlighted'
160
+ "
161
+ >
162
+ {{ eyebrow }}
163
+ </span>
164
+ <span v-if="entry.service" class="min-w-0 truncate">· {{ entry.service.title }}</span>
165
+ <span v-if="timeFact" class="shrink-0 whitespace-nowrap">· {{ timeFact }}</span>
166
+ <IconButton
167
+ class="ms-auto shrink-0"
168
+ color="neutral"
169
+ variant="ghost"
170
+ size="xs"
171
+ icon="i-lucide-map"
172
+ :label="t('queue.card.showOnBoard')"
173
+ @click.stop="showOnBoard"
174
+ />
175
+ </div>
176
+
177
+ <!-- The card's keyboard target: Enter on it opens the task, as a click on the card does. -->
178
+ <UButton
179
+ color="neutral"
180
+ variant="link"
181
+ class="mt-1 w-full p-0 text-start text-sm font-semibold leading-snug text-highlighted"
182
+ data-testid="queue-card-title"
183
+ @click.stop="openTask"
184
+ >
185
+ {{ task.title }}
186
+ </UButton>
187
+
188
+ <p v-if="context" class="mt-1 line-clamp-2 text-xs text-muted" :title="context">
189
+ {{ context }}
190
+ </p>
191
+
192
+ <!-- A failed run: the same failure banner and Retry the board card shows. -->
193
+ <AgentFailureCard
194
+ v-if="reason === 'failed' && runFailed && agentRun"
195
+ :run="agentRun"
196
+ variant="compact"
197
+ class="mt-2"
198
+ />
199
+
200
+ <div v-if="showProgress" class="mt-2 space-y-1">
201
+ <UProgress :model-value="Math.round(task.progress * 100)" size="xs" />
202
+ <p v-if="stepFact" class="text-2xs text-dimmed">{{ stepFact }}</p>
203
+ </div>
204
+
205
+ <div class="mt-3 flex flex-wrap items-center gap-2 empty:hidden">
206
+ <!-- Parked on a decision or an approval: the gate itself, one click away. -->
207
+ <UButton
208
+ v-if="attention"
209
+ color="warning"
210
+ variant="soft"
211
+ size="xs"
212
+ :icon="attention.icon"
213
+ data-testid="queue-card-primary"
214
+ @click.stop="attention.open()"
215
+ >
216
+ {{ reason === 'decision' ? t('queue.card.answer') : attention.action }}
217
+ </UButton>
218
+
219
+ <template v-else-if="reason === 'pr_awaiting_merge'">
220
+ <UButton
221
+ v-if="outcomeReadable"
222
+ variant="soft"
223
+ size="xs"
224
+ icon="i-lucide-clipboard-check"
225
+ data-testid="queue-card-primary"
226
+ @click.stop="openOutcome"
227
+ >
228
+ {{ t('board.task.readOutcome') }}
229
+ </UButton>
230
+ <UButton
231
+ color="success"
232
+ :variant="outcomeReadable ? 'outline' : 'soft'"
233
+ size="xs"
234
+ icon="i-lucide-git-merge"
235
+ :disabled="!canExecute"
236
+ :title="executeDeniedTitle"
237
+ @click.stop="merge"
238
+ >
239
+ {{ t('board.task.merge') }}
240
+ </UButton>
241
+ </template>
242
+
243
+ <UButton
244
+ v-else-if="reason === 'unstarted'"
245
+ variant="soft"
246
+ size="xs"
247
+ :icon="sandboxed ? 'i-lucide-shield' : 'i-lucide-play'"
248
+ :loading="starting"
249
+ :disabled="!runnable || starting || !canExecute"
250
+ :title="executeDeniedTitle"
251
+ data-testid="queue-card-primary"
252
+ @click.stop="start"
253
+ >
254
+ {{ starting ? t('board.task.starting') : t('board.task.start') }}
255
+ </UButton>
256
+
257
+ <UButton
258
+ v-else-if="reason === 'merged' && outcomeReadable"
259
+ size="xs"
260
+ variant="soft"
261
+ icon="i-lucide-clipboard-check"
262
+ data-testid="queue-card-primary"
263
+ @click.stop="openOutcome"
264
+ >
265
+ {{ t('board.task.readOutcome') }}
266
+ </UButton>
267
+ </div>
268
+ </article>
269
+ </template>
@@ -0,0 +1,192 @@
1
+ <script setup lang="ts">
2
+ import type { QueueEntry } from '~/utils/queueSections'
3
+ import QueueCard from '~/components/queue/QueueCard.vue'
4
+ import NotificationsInbox from '~/components/layout/NotificationsInbox.vue'
5
+ import SectionLabel from '~/components/common/SectionLabel.vue'
6
+
7
+ /**
8
+ * The queue: every task across every service, in the column that says what it needs. One of the
9
+ * board's two views (issue #2258); the canvas is the other.
10
+ *
11
+ * Three columns, read left to right in order of urgency: what is stalled on you, what agents are
12
+ * doing, what is finished and waiting to land. Work nobody has started sits under "In flight" as
13
+ * a short list, and recent merges sit under "Ready to merge" behind a count, because neither is
14
+ * what a person opening the app came to act on first. The canvas stays one click away as the
15
+ * planning view.
16
+ */
17
+ const { t, d } = useI18n()
18
+ const workspaceSettings = useWorkspaceSettingsStore()
19
+ const ui = useUiStore()
20
+ const board = useBoardStore()
21
+
22
+ /**
23
+ * Where the Done window starts: the workspace's Done-lane retention, the same one each frame's
24
+ * Done lane applies. Stated as a date, so no sentence has to agree with a number of days. Null
25
+ * when the workspace keeps no age cap.
26
+ */
27
+ const doneSince = computed(() => {
28
+ const days = workspaceSettings.settings.doneLaneRetentionDays
29
+ return days == null ? null : new Date(Date.now() - days * 86_400_000)
30
+ })
31
+ const queueStore = useWorkspaceQueueStore()
32
+
33
+ const sections = computed(() => queueStore.queue.sections)
34
+ const doneOpen = ref(false)
35
+
36
+ const serviceItems = computed(() => [
37
+ { label: t('queue.filter.allServices'), value: '__all__' },
38
+ ...queueStore.services.map((s) => ({ label: s.title, value: s.id })),
39
+ ])
40
+ const serviceValue = computed({
41
+ get: () => queueStore.serviceFilter ?? '__all__',
42
+ set: (v: string) => (queueStore.serviceFilter = v === '__all__' ? null : v),
43
+ })
44
+
45
+ interface Column {
46
+ id: 'needs_you' | 'in_flight' | 'ready_to_merge'
47
+ icon: string
48
+ entries: QueueEntry[]
49
+ }
50
+ const columns = computed<Column[]>(() => [
51
+ { id: 'needs_you', icon: 'i-lucide-hand', entries: sections.value.needs_you },
52
+ { id: 'in_flight', icon: 'i-lucide-loader', entries: sections.value.in_flight },
53
+ { id: 'ready_to_merge', icon: 'i-lucide-git-merge', entries: sections.value.ready_to_merge },
54
+ ])
55
+
56
+ const COLUMN_COPY: Record<Column['id'], { label: string; empty: string }> = {
57
+ needs_you: { label: 'queue.columns.needsYou.label', empty: 'queue.columns.needsYou.empty' },
58
+ in_flight: { label: 'queue.columns.inFlight.label', empty: 'queue.columns.inFlight.empty' },
59
+ ready_to_merge: {
60
+ label: 'queue.columns.readyToMerge.label',
61
+ empty: 'queue.columns.readyToMerge.empty',
62
+ },
63
+ }
64
+
65
+ const COLUMN_ACCENT: Record<Column['id'], string> = {
66
+ needs_you: 'text-warning',
67
+ in_flight: 'text-primary',
68
+ ready_to_merge: 'text-success',
69
+ }
70
+
71
+ /**
72
+ * Whether the inspector is on screen beside the queue. On a wide screen it floats over the
73
+ * board's right edge (`InspectorPanel`, `lg:w-80` at `lg:end-4`), which is where the queue's
74
+ * last column sits, so the columns give it that width (20rem, its 1rem offset and a 1rem gap)
75
+ * rather than letting it cover them. Below `lg` it is a bottom sheet the reader opens and closes.
76
+ */
77
+ const inspectorOpen = computed(() => !!ui.selectedBlockId && !!board.getBlock(ui.selectedBlockId))
78
+ </script>
79
+
80
+ <template>
81
+ <div class="flex h-full flex-col overflow-hidden" data-testid="queue-view">
82
+ <!-- `ps-14` below lg leaves room for the drawer's menu button in the corner. -->
83
+ <header
84
+ class="flex flex-wrap items-center gap-x-4 gap-y-2 ps-14 pe-4 pt-4 pb-3 sm:pe-6 lg:ps-6"
85
+ >
86
+ <div class="flex items-baseline gap-2">
87
+ <h1 class="text-xl font-semibold text-highlighted">{{ t('queue.title') }}</h1>
88
+ <span class="text-sm text-muted" data-testid="queue-summary">
89
+ {{
90
+ t('queue.summary', {
91
+ needsYou: sections.needs_you.length,
92
+ ready: sections.ready_to_merge.length,
93
+ })
94
+ }}
95
+ </span>
96
+ </div>
97
+ <div class="ms-auto flex flex-wrap items-center gap-2">
98
+ <USelect
99
+ v-model="serviceValue"
100
+ :items="serviceItems"
101
+ size="sm"
102
+ class="w-48"
103
+ :aria-label="t('queue.filter.service')"
104
+ data-testid="queue-service-filter"
105
+ />
106
+ <!-- The inbox still holds what is NOT a task: platform alerts, budget thresholds, key
107
+ drift. The canvas shows it in its toolbar, which this view does not have. -->
108
+ <NotificationsInbox />
109
+ </div>
110
+ </header>
111
+
112
+ <div
113
+ class="grid min-h-0 flex-1 grid-cols-1 gap-4 overflow-y-auto px-4 pb-6 transition-[padding] sm:px-6 md:grid-cols-3 md:overflow-hidden"
114
+ :class="inspectorOpen ? 'lg:pe-[22rem]' : ''"
115
+ >
116
+ <section
117
+ v-for="column in columns"
118
+ :key="column.id"
119
+ class="flex min-h-0 flex-col rounded-xl border border-muted bg-elevated/40"
120
+ data-testid="queue-column"
121
+ :data-column="column.id"
122
+ >
123
+ <div class="flex items-center gap-2 px-3 pt-3 pb-2">
124
+ <UIcon :name="column.icon" class="h-4 w-4" :class="COLUMN_ACCENT[column.id]" />
125
+ <SectionLabel as="h2">{{ t(COLUMN_COPY[column.id].label) }}</SectionLabel>
126
+ <UBadge class="ms-auto" color="neutral" variant="subtle" size="sm">
127
+ {{ column.entries.length }}
128
+ </UBadge>
129
+ </div>
130
+
131
+ <div class="min-h-0 flex-1 space-y-2 overflow-y-auto px-3 pb-3">
132
+ <QueueCard v-for="entry in column.entries" :key="entry.task.id" :entry="entry" />
133
+ <p v-if="column.entries.length === 0" class="px-1 py-6 text-center text-xs text-dimmed">
134
+ {{ t(COLUMN_COPY[column.id].empty) }}
135
+ </p>
136
+
137
+ <!-- Not started: under In flight, because it is the next thing that column will hold. -->
138
+ <template v-if="column.id === 'in_flight'">
139
+ <USeparator class="my-3" />
140
+ <SectionLabel as="h3" class="px-1">
141
+ {{ t('queue.waiting.label', { count: sections.waiting.length }) }}
142
+ </SectionLabel>
143
+ <QueueCard v-for="entry in sections.waiting" :key="entry.task.id" :entry="entry" />
144
+ <p v-if="sections.waiting.length === 0" class="px-1 text-xs text-dimmed">
145
+ {{ t('queue.waiting.empty') }}
146
+ </p>
147
+ </template>
148
+
149
+ <!-- Recent merges: a count, and the list on request. The queue is about now. -->
150
+ <template v-if="column.id === 'ready_to_merge'">
151
+ <USeparator class="my-3" />
152
+ <div class="flex items-center gap-2 px-1 text-xs text-muted">
153
+ <span data-testid="queue-done-count">
154
+ {{
155
+ doneSince
156
+ ? t('queue.done.count', {
157
+ count: queueStore.queue.doneInWindow,
158
+ date: d(doneSince, 'short'),
159
+ })
160
+ : t('queue.done.countAll', { count: queueStore.queue.doneInWindow })
161
+ }}
162
+ </span>
163
+ <UButton
164
+ v-if="queueStore.queue.doneInWindow > 0"
165
+ variant="link"
166
+ size="xs"
167
+ class="p-0"
168
+ @click="doneOpen = !doneOpen"
169
+ >
170
+ {{ doneOpen ? t('queue.done.hide') : t('queue.done.show') }}
171
+ </UButton>
172
+ </div>
173
+ <template v-if="doneOpen">
174
+ <QueueCard v-for="entry in sections.done" :key="entry.task.id" :entry="entry" />
175
+ <p
176
+ v-if="queueStore.queue.doneInWindow > sections.done.length"
177
+ class="px-1 text-2xs text-dimmed"
178
+ >
179
+ {{
180
+ t('queue.done.capped', {
181
+ shown: sections.done.length,
182
+ total: queueStore.queue.doneInWindow,
183
+ })
184
+ }}
185
+ </p>
186
+ </template>
187
+ </template>
188
+ </div>
189
+ </section>
190
+ </div>
191
+ </div>
192
+ </template>
@@ -11,6 +11,7 @@ import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
11
11
  import StepRunMeta from '~/components/panels/StepRunMeta.vue'
12
12
  import CopyButton from '~/components/common/CopyButton.vue'
13
13
  import SectionLabel from '~/components/common/SectionLabel.vue'
14
+ import EmptyState from '~/components/common/EmptyState.vue'
14
15
 
15
16
  const board = useBoardStore()
16
17
  const execution = useExecutionStore()
@@ -136,13 +137,7 @@ const STATUS_META = computed<
136
137
  </template>
137
138
  <div class="flex min-h-0 flex-1">
138
139
  <div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
139
- <div
140
- v-if="!ralph"
141
- class="flex h-full flex-col items-center justify-center gap-2 text-center text-muted"
142
- >
143
- <UIcon :name="meta.icon" class="h-8 w-8 opacity-40" />
144
- <p class="text-sm">{{ t('ralph.noActivity') }}</p>
145
- </div>
140
+ <EmptyState v-if="!ralph" class="h-full" :icon="meta.icon" :title="t('ralph.noActivity')" />
146
141
 
147
142
  <template v-else>
148
143
  <!-- The completion criterion. -->
@@ -29,11 +29,13 @@ import type {
29
29
  ReviewItemStatus,
30
30
  } from '~/types/requirements'
31
31
  import SectionLabel from '~/components/common/SectionLabel.vue'
32
+ import Spinner from '~/components/common/Spinner.vue'
33
+ import EmptyState from '~/components/common/EmptyState.vue'
32
34
 
33
35
  const board = useBoardStore()
34
36
  const requirements = useRequirementsStore()
35
37
  const models = useModelsStore()
36
- const toast = useToast()
38
+ const actionToast = useActionToast()
37
39
  const { present } = usePipelineErrorToast()
38
40
  const { t } = useI18n()
39
41
  const access = useWorkspaceAccess()
@@ -574,18 +576,18 @@ async function requestRecommendations() {
574
576
  // ran inline and the recommendations are already settled. Tell the human which actually
575
577
  // happened rather than always promising a background callback.
576
578
  const stillGenerating = (updated?.recommendations ?? []).some((r) => r.status === 'pending')
577
- toast.add(
578
- stillGenerating
579
- ? {
580
- title: t('requirements.toast.preparingRecommendations', { count: n }, n),
581
- description: t('requirements.toast.preparingRecommendationsDescription'),
582
- icon: 'i-lucide-sparkles',
583
- }
584
- : {
585
- title: t('requirements.toast.recommendationsReady', { count: n }, n),
586
- icon: 'i-lucide-sparkles',
587
- },
588
- )
579
+ if (stillGenerating) {
580
+ actionToast.info('requirements.toast.preparingRecommendations', {
581
+ params: { count: n },
582
+ plural: n,
583
+ description: t('requirements.toast.preparingRecommendationsDescription'),
584
+ })
585
+ } else {
586
+ actionToast.success('requirements.toast.recommendationsReady', {
587
+ params: { count: n },
588
+ plural: n,
589
+ })
590
+ }
589
591
  } catch (e) {
590
592
  present(e, 'requirements.errors.requestRecommendations')
591
593
  }
@@ -634,10 +636,8 @@ async function incorporate(feedback?: string) {
634
636
  showRedo.value = false
635
637
  // The fold + re-review now run in the durable driver. Hand the user back to the board;
636
638
  // a notification calls them back only if the re-review needs more input.
637
- toast.add({
638
- title: t('requirements.toast.incorporating'),
639
+ actionToast.info('requirements.toast.incorporating', {
639
640
  description: t('requirements.toast.incorporatingDescription'),
640
- icon: 'i-lucide-wand-sparkles',
641
641
  })
642
642
  close()
643
643
  }
@@ -647,15 +647,16 @@ async function reReview() {
647
647
  try {
648
648
  const updated = await requirements.reReview(blockId.value)
649
649
  const newFindings = requirements.openCount(updated)
650
- toast.add({
651
- title:
652
- updated.status === 'incorporated'
653
- ? t('requirements.toast.reviewerSatisfied')
654
- : updated.status === 'exceeded'
655
- ? t('requirements.toast.iterationLimitReached')
656
- : t('requirements.toast.newFindings', { count: newFindings }, newFindings),
657
- icon: 'i-lucide-sparkles',
658
- })
650
+ if (updated.status === 'incorporated') {
651
+ actionToast.success('requirements.toast.reviewerSatisfied')
652
+ } else if (updated.status === 'exceeded') {
653
+ actionToast.warning('requirements.toast.iterationLimitReached')
654
+ } else {
655
+ actionToast.info('requirements.toast.newFindings', {
656
+ params: { count: newFindings },
657
+ plural: newFindings,
658
+ })
659
+ }
659
660
  } catch (e) {
660
661
  present(e, 'requirements.errors.reReview')
661
662
  }
@@ -667,7 +668,7 @@ async function proceed() {
667
668
  try {
668
669
  await flushDrafts()
669
670
  await requirements.proceed(blockId.value)
670
- toast.add({ title: t('requirements.toast.proceeding'), icon: 'i-lucide-arrow-right' })
671
+ actionToast.info('requirements.toast.proceeding')
671
672
  } catch (e) {
672
673
  present(e, 'requirements.errors.proceed')
673
674
  } finally {
@@ -681,12 +682,12 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
681
682
  try {
682
683
  await requirements.resolveExceeded(blockId.value, choice)
683
684
  if (choice === 'stop-reset') {
684
- toast.add({ title: t('requirements.toast.taskReset'), icon: 'i-lucide-undo' })
685
+ actionToast.info('requirements.toast.taskReset')
685
686
  close()
686
687
  } else if (choice === 'proceed') {
687
- toast.add({ title: t('requirements.toast.proceeding'), icon: 'i-lucide-arrow-right' })
688
+ actionToast.info('requirements.toast.proceeding')
688
689
  } else {
689
- toast.add({ title: t('requirements.toast.extraRoundGranted'), icon: 'i-lucide-rotate-cw' })
690
+ actionToast.info('requirements.toast.extraRoundGranted')
690
691
  }
691
692
  } catch (e) {
692
693
  present(e, 'requirements.errors.resolveReview')
@@ -734,20 +735,24 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
734
735
 
735
736
  <!-- empty state — the reviewer runs automatically as the first pipeline
736
737
  gate step, so there's nothing to do here until then -->
737
- <div
738
+ <EmptyState
738
739
  v-if="!review && !busy && !loading"
739
- class="rounded-lg border border-dashed border-muted p-8 text-center text-sm text-dimmed"
740
- >
741
- {{ t('requirements.empty') }}
740
+ icon="i-lucide-list-checks"
741
+ :title="t('requirements.empty')"
742
+ />
743
+
744
+ <!-- initial fetch on open -->
745
+ <div v-else-if="loading && !busy && !review" class="flex flex-col gap-3">
746
+ <USkeleton v-for="n in 3" :key="n" class="h-20 w-full rounded-lg" />
742
747
  </div>
743
748
 
744
- <!-- working state (initial fetch on open, or a reviewer pass running) -->
749
+ <!-- a reviewer pass running -->
745
750
  <div
746
- v-else-if="(busy || loading) && !review"
751
+ v-else-if="busy && !review"
747
752
  class="flex items-center justify-center gap-2 p-8 text-sm text-muted"
748
753
  >
749
- <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
750
- {{ loading && !busy ? t('requirements.loadingReview') : t('requirements.reviewing') }}
754
+ <Spinner class="h-4 w-4" />
755
+ {{ t('requirements.reviewing') }}
751
756
  </div>
752
757
 
753
758
  <template v-else-if="review">
@@ -777,7 +782,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
777
782
  v-else-if="working"
778
783
  class="mb-4 flex items-center gap-2 rounded-lg border border-primary/60 bg-primary/10 p-4 text-sm text-primary"
779
784
  >
780
- <UIcon name="i-lucide-loader-circle" class="h-5 w-5 shrink-0 animate-spin" />
785
+ <Spinner class="h-5 w-5 shrink-0" />
781
786
  <span v-if="incorporating">
782
787
  {{ t('requirements.working.incorporating') }}
783
788
  </span>
@@ -978,7 +983,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
978
983
  v-if="pendingRecFor(item)"
979
984
  class="mt-2 flex items-center gap-1.5 text-xs text-primary"
980
985
  >
981
- <UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 animate-spin" />
986
+ <Spinner class="h-3.5 w-3.5" />
982
987
  {{ t('requirements.generatingSuggestion') }}
983
988
  </div>
984
989
  <template v-else-if="readyRecFor(item)">