@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.
- package/README.md +92 -4
- package/app/app.config.ts +7 -0
- package/app/components/assistant/AssistantModal.vue +2 -5
- package/app/components/auth/AuthGate.vue +2 -1
- package/app/components/binaryCandidates/BinaryCandidatesWindow.vue +14 -15
- package/app/components/binaryOutput/StoredAssetView.vue +1 -3
- package/app/components/board/AddTaskModal.vue +3 -10
- package/app/components/board/AgentFailureCard.vue +4 -5
- package/app/components/board/AgentStopButton.vue +5 -7
- package/app/components/board/BoardCanvas.vue +31 -30
- package/app/components/board/CreateInitiativeModal.vue +2 -4
- package/app/components/board/RecurringPipelineModal.vue +2 -7
- package/app/components/board/nodes/AgentChip.vue +3 -2
- package/app/components/board/nodes/BlockNode.vue +14 -11
- package/app/components/board/nodes/InitiativeCard.vue +6 -3
- package/app/components/board/nodes/TaskCard.vue +45 -212
- package/app/components/bootstrap/AdoptionReviewModal.vue +2 -5
- package/app/components/bootstrap/BootstrapModal.vue +6 -15
- package/app/components/bootstrap/BootstrapRunSteps.vue +3 -1
- package/app/components/brainstorm/BrainstormWindow.vue +32 -26
- package/app/components/bugFishing/BugFishingWindow.vue +15 -13
- package/app/components/clarity/ClarityReviewWindow.vue +31 -26
- package/app/components/common/ClarificationItem.vue +3 -1
- package/app/components/common/EmptyState.vue +41 -19
- package/app/components/common/InterviewGateNotice.vue +3 -1
- package/app/components/common/Spinner.vue +28 -0
- package/app/components/consensus/ConsensusSessionWindow.vue +5 -5
- package/app/components/docs/DocInterviewWindow.vue +6 -6
- package/app/components/documents/ContextDocumentPicker.vue +2 -2
- package/app/components/documents/DocumentImportModal.vue +9 -8
- package/app/components/documents/DocumentSourceConnectModal.vue +10 -10
- package/app/components/documents/DocumentTemplatesModal.vue +13 -7
- package/app/components/documents/RepoContextDocPicker.vue +2 -2
- package/app/components/documents/SpawnPreviewModal.vue +4 -6
- package/app/components/documents/StartFromDesignModal.vue +2 -1
- package/app/components/documents/TaskContextDocs.vue +4 -5
- package/app/components/environments/EnvironmentStatusPanel.vue +11 -8
- package/app/components/environments/steps/EnvPickStep.vue +7 -3
- package/app/components/environments/steps/EnvPreflightStep.vue +6 -5
- package/app/components/focus/BlockFocusView.vue +6 -5
- package/app/components/followUp/FollowUpWindow.vue +7 -9
- package/app/components/forkDecision/ForkDecisionWindow.vue +9 -8
- package/app/components/foundational/FoundationalServiceCatalogList.vue +9 -5
- package/app/components/foundational/FoundationalServiceRegistry.vue +11 -7
- package/app/components/foundational/FoundationalServiceSources.vue +20 -22
- package/app/components/foundational/FoundationalSuppressions.vue +2 -2
- package/app/components/foundational/ServiceCatalogConnection.vue +15 -15
- package/app/components/fragments/FragmentLibraryManager.vue +36 -34
- package/app/components/fragments/FragmentSelector.vue +7 -3
- package/app/components/gates/GateResultView.vue +6 -9
- package/app/components/github/AddServiceFromRepoModal.vue +6 -10
- package/app/components/github/GitHubConnect.vue +10 -17
- package/app/components/github/GitHubPanel.vue +44 -47
- package/app/components/github/RepoTreeBrowser.vue +9 -5
- package/app/components/guidedReview/GuidedReviewModal.vue +16 -6
- package/app/components/guidedReview/GuidedReviewOverview.vue +2 -1
- package/app/components/guidedReview/GuidedReviewPicker.vue +10 -7
- package/app/components/guidedReview/GuidedReviewThread.vue +9 -4
- package/app/components/humanTest/HumanTestWindow.vue +2 -1
- package/app/components/initiative/InitiativePlanningWindow.vue +6 -6
- package/app/components/initiative/InitiativeTrackerWindow.vue +11 -11
- package/app/components/judge/JudgeResultView.vue +2 -7
- package/app/components/kaizen/KaizenPanel.vue +20 -5
- package/app/components/kaizen/KaizenStepStatus.vue +2 -1
- package/app/components/layout/AccountAuditLog.vue +11 -3
- package/app/components/layout/AccountDeploymentSettings.vue +17 -62
- package/app/components/layout/AccountFailureKindRules.vue +6 -5
- package/app/components/layout/AccountModelPolicySettings.vue +2 -6
- package/app/components/layout/AccountPlatformAlertSettings.vue +5 -13
- package/app/components/layout/AccountRiskPolicySettings.vue +13 -14
- package/app/components/layout/AccountRunCredentialSettings.vue +2 -2
- package/app/components/layout/AccountTeamSettings.vue +12 -9
- package/app/components/layout/BoardSwitcher.vue +2 -2
- package/app/components/layout/BoardToolbar.vue +27 -37
- package/app/components/layout/BoardTopOverlays.vue +8 -2
- package/app/components/layout/CommandBar.vue +7 -3
- package/app/components/layout/ConnectionStatusBanner.vue +2 -1
- package/app/components/layout/IntegrationsHub.vue +7 -3
- package/app/components/layout/ModelProvidersHub.vue +7 -3
- package/app/components/layout/NotificationsInbox.vue +3 -11
- package/app/components/layout/SideBar.vue +5 -0
- package/app/components/layout/WorkViewSwitcher.vue +64 -0
- package/app/components/layout/WorkspaceMembersSettings.vue +12 -5
- package/app/components/media/ArtifactLightbox.vue +4 -3
- package/app/components/notifications/NotificationSettingsPanel.vue +4 -11
- package/app/components/observability/ToolCallList.vue +12 -17
- package/app/components/panels/AgentStepDetail.vue +5 -5
- package/app/components/panels/GenericStructuredResultView.vue +6 -9
- package/app/components/panels/InspectorPanel.vue +2 -1
- package/app/components/panels/MergerResultView.vue +5 -6
- package/app/components/panels/ObservabilityPanel.vue +26 -41
- package/app/components/panels/OperatorDashboardPanel.vue +29 -17
- package/app/components/panels/ReportsPanel.vue +17 -11
- package/app/components/panels/ReportsSpendBreakdown.vue +7 -3
- package/app/components/panels/RunDetailLoadState.vue +4 -3
- package/app/components/panels/StepContainerStatus.vue +4 -3
- package/app/components/panels/StepDelegatedStatus.vue +3 -2
- package/app/components/panels/StepMetadataCard.vue +6 -8
- package/app/components/panels/StepRunMeta.vue +2 -5
- package/app/components/panels/inspector/ContainerSummary.vue +7 -1
- package/app/components/panels/inspector/EpicChildren.vue +7 -3
- package/app/components/panels/inspector/FrontendConfig.vue +7 -3
- package/app/components/panels/inspector/InitiativeInspector.vue +5 -2
- package/app/components/panels/inspector/ServiceConnections.vue +7 -3
- package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +2 -6
- package/app/components/panels/inspector/ServiceTestSecrets.vue +2 -6
- package/app/components/panels/inspector/ServiceTestingContext.vue +2 -6
- package/app/components/panels/inspector/ServiceValidationConfig.vue +38 -39
- package/app/components/panels/inspector/TaskExecution.vue +5 -6
- package/app/components/panels/inspector/TaskRunSettings.vue +7 -3
- package/app/components/panels/inspector/TaskStructure.vue +0 -1
- package/app/components/pipeline/AgentPromptEditor.vue +25 -18
- package/app/components/pipeline/PipelineBuilder.vue +14 -24
- package/app/components/pipeline/PipelineProgress.vue +16 -17
- package/app/components/prReview/PrReviewPhaseBadge.vue +3 -2
- package/app/components/prReview/PrReviewWindow.vue +16 -20
- package/app/components/providers/ApiKeysSection.vue +2 -6
- package/app/components/providers/PersonalCredentialModal.vue +2 -6
- package/app/components/providers/PersonalSubscriptionSection.vue +3 -7
- package/app/components/providers/VendorCredentialsModal.vue +2 -6
- package/app/components/provisioning/ProvisioningLogsDrawer.vue +10 -6
- package/app/components/queue/QueueCard.vue +269 -0
- package/app/components/queue/QueueView.vue +192 -0
- package/app/components/ralph/RalphLoopResultView.vue +2 -7
- package/app/components/requirements/RequirementsReviewWindow.vue +44 -39
- package/app/components/sandbox/SandboxPanel.vue +37 -25
- package/app/components/settings/AccountSettingsPanel.vue +6 -3
- package/app/components/settings/ApiTokensPanel.vue +2 -6
- package/app/components/settings/BudgetSettings.vue +2 -2
- package/app/components/settings/CapabilityCredentialsPanel.vue +2 -6
- package/app/components/settings/CloudflareHandlerSection.vue +3 -7
- package/app/components/settings/ConsensusGroupsSection.vue +7 -3
- package/app/components/settings/CustomManifestTypeEditor.vue +7 -3
- package/app/components/settings/DefaultProvisionTypeSection.vue +2 -2
- package/app/components/settings/InfraHandlersConfigurator.vue +8 -11
- package/app/components/settings/IssueTrackerPanel.vue +9 -9
- package/app/components/settings/LocalContainerPoolSettings.vue +2 -6
- package/app/components/settings/LocalModelEndpointsPanel.vue +5 -7
- package/app/components/settings/McpAuthorizeScreen.vue +8 -3
- package/app/components/settings/McpOAuthCallbackScreen.vue +2 -1
- package/app/components/settings/ModelConfigurationPanel.vue +45 -17
- package/app/components/settings/ObservabilityConnectionPanel.vue +4 -15
- package/app/components/settings/OpenRouterCatalogPanel.vue +8 -24
- package/app/components/settings/PackageRegistriesPanel.vue +2 -6
- package/app/components/settings/ProviderConnectionTab.vue +3 -7
- package/app/components/settings/RiskPolicyPanel.vue +4 -16
- package/app/components/settings/SharedStacksPanel.vue +8 -18
- package/app/components/settings/TaskTypeSuppressionsPanel.vue +10 -6
- package/app/components/settings/UsageSettings.vue +11 -2
- package/app/components/settings/UserSecretsSection.vue +5 -7
- package/app/components/settings/WorkspaceMetadataSettings.vue +9 -9
- package/app/components/settings/WorkspaceSettingsPanel.vue +3 -11
- package/app/components/skills/ReviewSkillQueue.vue +7 -3
- package/app/components/skills/SkillLibraryManager.vue +20 -18
- package/app/components/slack/SlackPanel.vue +5 -8
- package/app/components/spec/ServiceSpecWindow.vue +32 -29
- package/app/components/tasks/BugHuntModal.vue +23 -19
- package/app/components/tasks/ContextIssuePicker.vue +2 -2
- package/app/components/tasks/TaskContextIssues.vue +4 -5
- package/app/components/tasks/TaskImportModal.vue +10 -8
- package/app/components/tasks/TaskSourceConnectModal.vue +11 -11
- package/app/components/testing/TestReportWindow.vue +6 -12
- package/app/components/tutorial/TutorialCatalogue.vue +7 -3
- package/app/components/tutorial/TutorialOverlay.vue +2 -1
- package/app/components/tutorial/TutorialPrompt.vue +7 -3
- package/app/components/vcs/GitLabConnect.vue +2 -6
- package/app/components/visualConfirm/VisualConfirmationWindow.vue +9 -4
- package/app/composables/toastTitleKeys.spec.ts +143 -0
- package/app/composables/useActionToast.spec.ts +71 -0
- package/app/composables/useActionToast.ts +101 -0
- package/app/composables/useBlockDeletion.ts +3 -6
- package/app/composables/useConfirmAction.ts +5 -3
- package/app/composables/useContextLinking.spec.ts +3 -0
- package/app/composables/useContextLinking.ts +11 -16
- package/app/composables/useCopyToClipboard.ts +4 -3
- package/app/composables/useFrameLanes.ts +6 -82
- package/app/composables/useNavContributions.ts +6 -5
- package/app/composables/usePipelineErrorToast.spec.ts +24 -0
- package/app/composables/usePipelineErrorToast.ts +26 -2
- package/app/composables/usePipelineLibraryActions.ts +8 -9
- package/app/composables/useTaskActions.ts +254 -0
- package/app/composables/useTaskLaneClassifier.ts +88 -0
- package/app/modular/nav-contributions.spec.ts +4 -0
- package/app/modular/nav-contributions.ts +31 -0
- package/app/modular/tutorial-tours.ts +10 -0
- package/app/pages/index.vue +42 -8
- package/app/stores/board/context.ts +4 -3
- package/app/stores/board/placement.ts +8 -20
- package/app/stores/board/removal.ts +4 -13
- package/app/stores/board.spec.ts +10 -1
- package/app/stores/board.ts +3 -2
- package/app/stores/workView.ts +51 -0
- package/app/stores/workspaceQueue.ts +148 -0
- package/app/utils/bootstrapSteps.ts +11 -2
- package/app/utils/pipelineRender.ts +21 -8
- package/app/utils/queueSections.spec.ts +111 -0
- package/app/utils/queueSections.ts +117 -0
- package/app/utils/workView.spec.ts +16 -0
- package/app/utils/workView.ts +25 -0
- package/i18n/locales/de.json +73 -32
- package/i18n/locales/en.json +73 -32
- package/i18n/locales/es.json +73 -32
- package/i18n/locales/fr.json +73 -32
- package/i18n/locales/he.json +73 -32
- package/i18n/locales/it.json +73 -32
- package/i18n/locales/ja.json +73 -32
- package/i18n/locales/pl.json +73 -32
- package/i18n/locales/tr.json +73 -32
- package/i18n/locales/uk.json +73 -32
- 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
|
-
<
|
|
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
|
|
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
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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.
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
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
|
-
|
|
685
|
+
actionToast.info('requirements.toast.taskReset')
|
|
685
686
|
close()
|
|
686
687
|
} else if (choice === 'proceed') {
|
|
687
|
-
|
|
688
|
+
actionToast.info('requirements.toast.proceeding')
|
|
688
689
|
} else {
|
|
689
|
-
|
|
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
|
-
<
|
|
738
|
+
<EmptyState
|
|
738
739
|
v-if="!review && !busy && !loading"
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
-
<!--
|
|
749
|
+
<!-- a reviewer pass running -->
|
|
745
750
|
<div
|
|
746
|
-
v-else-if="
|
|
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
|
-
<
|
|
750
|
-
{{
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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)">
|