@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
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { defaultBuildPipelineId } from '@cat-factory/contracts'
|
|
3
2
|
import type { Block } from '~/types/domain'
|
|
4
3
|
import { STATUS_META, MODULE_META, taskTypeMeta } from '~/utils/catalog'
|
|
5
|
-
import { composeRunOutcome, hasOutcomeToShow } from '~/utils/runOutcome'
|
|
6
4
|
import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
|
|
7
5
|
import TaskPipelineMini from './TaskPipelineMini.vue'
|
|
8
6
|
import IconButton from '~/components/common/IconButton.vue'
|
|
@@ -10,16 +8,28 @@ import IconButton from '~/components/common/IconButton.vue'
|
|
|
10
8
|
const props = defineProps<{ taskId: string }>()
|
|
11
9
|
|
|
12
10
|
const board = useBoardStore()
|
|
13
|
-
const execution = useExecutionStore()
|
|
14
|
-
const pipelines = usePipelinesStore()
|
|
15
11
|
const ui = useUiStore()
|
|
16
12
|
const agentRuns = useAgentRunsStore()
|
|
17
|
-
const reviews = useReviewStage()
|
|
18
|
-
const toast = useToast()
|
|
19
13
|
const { t } = useI18n()
|
|
20
|
-
const { confirm } = useConfirm()
|
|
21
14
|
|
|
22
|
-
|
|
15
|
+
// What the card offers a human to do, shared with the queue's card (see `useTaskActions`).
|
|
16
|
+
const {
|
|
17
|
+
task,
|
|
18
|
+
sandboxed,
|
|
19
|
+
unmet,
|
|
20
|
+
runnable,
|
|
21
|
+
canExecute,
|
|
22
|
+
executeDeniedTitle,
|
|
23
|
+
defaultPipeline,
|
|
24
|
+
outcomeReadable,
|
|
25
|
+
openOutcome,
|
|
26
|
+
starting,
|
|
27
|
+
start: run,
|
|
28
|
+
merge,
|
|
29
|
+
reviewStage,
|
|
30
|
+
reviewStageLabel,
|
|
31
|
+
attention,
|
|
32
|
+
} = useTaskActions(() => props.taskId)
|
|
23
33
|
const statusMeta = computed(() => (task.value ? STATUS_META[task.value.status] : null))
|
|
24
34
|
|
|
25
35
|
// A type badge for any NON-default task type — built-in (bug/document/spike/review/ralph) or a
|
|
@@ -39,13 +49,6 @@ const typeBadge = computed(() => {
|
|
|
39
49
|
})
|
|
40
50
|
const selected = computed(() => ui.selectedBlockId === props.taskId)
|
|
41
51
|
|
|
42
|
-
// This card's Start is a ONE-TAP live start, so it has nothing to ask for and offers no dry-run
|
|
43
|
-
// request. What it does owe the reader is the half that is not a choice: a preset that sandboxes
|
|
44
|
-
// their role means this button opens a pull request and merges nothing, and a card that said so
|
|
45
|
-
// only after the fact would leave that to be discovered from a run that stops at the merge.
|
|
46
|
-
const { forcedFor } = useDryRunPolicy()
|
|
47
|
-
const sandboxed = computed(() => forcedFor(props.taskId))
|
|
48
|
-
|
|
49
52
|
// Drag-to-connect: dragging from this card's handle onto another task makes THAT task
|
|
50
53
|
// depend on this one (this is the prerequisite). The composable tracks the gesture.
|
|
51
54
|
const { start: startConnect } = useDependencyConnect()
|
|
@@ -56,84 +59,28 @@ const deps = computed(() =>
|
|
|
56
59
|
)
|
|
57
60
|
const uiMode = useUiModeStore()
|
|
58
61
|
|
|
59
|
-
/** Deps that haven't merged yet — these block this task from running. */
|
|
60
|
-
const unmet = computed(() => board.unmetDeps(props.taskId))
|
|
61
|
-
const runnable = computed(() => board.isRunnable(props.taskId))
|
|
62
|
-
|
|
63
62
|
/** Label a dependency, noting its frame when it lives in another one. */
|
|
64
63
|
const { depLabel: labelDep } = useDepLabels()
|
|
65
64
|
const depLabel = (dep: Block) => labelDep(dep, task.value?.parentId)
|
|
66
65
|
|
|
67
|
-
/**
|
|
68
|
-
* The pipeline a plain "Start" will use: the task's pinned pipeline, else the build rung this
|
|
69
|
-
* INTERFACE MODE defaults to (`defaultBuildPipelineId` — the fixed Standard build in basic mode,
|
|
70
|
-
* the Adaptive one in advanced). The workspace's positional first pipeline remains the last
|
|
71
|
-
* resort, for a board whose catalog does not carry the rung (an older seed, or a deployment that
|
|
72
|
-
* retired it).
|
|
73
|
-
*
|
|
74
|
-
* A PIN is honoured even when the library holds no row for it, and that branch is the whole reason
|
|
75
|
-
* this returns a descriptor rather than a `Pipeline`. An INTERNAL pipeline is withheld from the
|
|
76
|
-
* library on purpose (the platform starts it on its own behalf, so no picker may offer it), and a
|
|
77
|
-
* task can legitimately be pinned to one — the docs-refresh preset spawns its tasks onto
|
|
78
|
-
* `pl_code_comments`. Resolving that pin through the library alone answers undefined, and the
|
|
79
|
-
* fallback below then starts a FULL BUILD on a comment-only task while the button still reads as
|
|
80
|
-
* an ordinary Start. The fallback chain exists for a task with NO pin; a pin the library cannot
|
|
81
|
-
* show is still the task's answer, and the backend resolves the id for the run.
|
|
82
|
-
*/
|
|
83
|
-
const defaultPipeline = computed<{ id: string; name: string } | undefined>(() => {
|
|
84
|
-
const pinnedId = task.value?.pipelineId
|
|
85
|
-
if (pinnedId) {
|
|
86
|
-
return (
|
|
87
|
-
pipelines.getPipeline(pinnedId) ?? {
|
|
88
|
-
id: pinnedId,
|
|
89
|
-
// The catalog NAME map spans the whole catalog (unlike the versions map), so an internal
|
|
90
|
-
// pin still names itself here; the generic label covers a pin to something this build's
|
|
91
|
-
// catalog does not know at all.
|
|
92
|
-
name: pipelines.catalogNames[pinnedId] ?? t('board.task.pipelineFallback'),
|
|
93
|
-
}
|
|
94
|
-
)
|
|
95
|
-
}
|
|
96
|
-
// No pin: the workspace's own DECLARED in-app default outranks the interface-mode rung, because
|
|
97
|
-
// an operator who named one said something a tier cannot overrule.
|
|
98
|
-
const declared = pipelines.declaredDefaultId('interactive')
|
|
99
|
-
return (
|
|
100
|
-
pipelines.getPipeline(declared ?? defaultBuildPipelineId(uiMode.isAdvanced)) ??
|
|
101
|
-
pipelines.pipelines[0]
|
|
102
|
-
)
|
|
103
|
-
})
|
|
104
|
-
|
|
105
66
|
/** The PR the implementer agent opened for this task, if any. */
|
|
106
67
|
const pr = computed(() => task.value?.pullRequest)
|
|
107
68
|
const prLabel = computed(() =>
|
|
108
69
|
pr.value?.number ? t('board.task.prNumber', { number: pr.value.number }) : t('board.task.pr'),
|
|
109
70
|
)
|
|
110
71
|
|
|
111
|
-
/**
|
|
112
|
-
* Reading the result starts at the OUTCOME summary (what changed in product terms, with the
|
|
113
|
-
* captured evidence), and the pull request is one click inside it. In BASIC mode that replaces
|
|
114
|
-
* the card's raw PR chip: the diff is still exactly as reachable, through a surface that says
|
|
115
|
-
* what the diff is about first. Advanced mode keeps both, since a reader who wants the diff
|
|
116
|
-
* directly is the reader that tier is for.
|
|
117
|
-
*
|
|
118
|
-
* Offered only where there is something to read, asked of the SAME reduction the window renders
|
|
119
|
-
* and the inspector's button gates on: a card that offered "read the result" on a task whose
|
|
120
|
-
* every section says "nothing here" would teach people the surface is empty. A task marked done
|
|
121
|
-
* by hand, with no pull request and no run, is that task.
|
|
122
|
-
*/
|
|
123
72
|
const laneView = useLaneViewStore()
|
|
124
|
-
const outcomeReadable = computed(() => {
|
|
125
|
-
const block = task.value
|
|
126
|
-
if (!block) return false
|
|
127
|
-
return hasOutcomeToShow(
|
|
128
|
-
composeRunOutcome({ block, instance: execution.getInstance(block.executionId) ?? null }),
|
|
129
|
-
)
|
|
130
|
-
})
|
|
131
73
|
/**
|
|
132
|
-
* The card's raw pull-request chip
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* the
|
|
74
|
+
* The card's raw pull-request chip. Reading the result starts at the OUTCOME summary (what changed
|
|
75
|
+
* in product terms, with the captured evidence), and the pull request is one click inside it, so
|
|
76
|
+
* BASIC mode drops the chip in favour of the outcome card carrying the same link at the top: the
|
|
77
|
+
* diff is still exactly as reachable, through a surface that says what the diff is about first.
|
|
78
|
+
* Advanced mode keeps both, since a reader who wants the diff directly is the reader that tier is
|
|
79
|
+
* for.
|
|
80
|
+
*
|
|
81
|
+
* Written as the INVARIANT ("the diff never stops being reachable from this card") rather than as
|
|
82
|
+
* `isAdvanced` alone, so the tier can only ever reorder two routes and never remove the last one:
|
|
83
|
+
* where the outcome card is not offered, the chip stays in both tiers.
|
|
137
84
|
*/
|
|
138
85
|
const showPrChip = computed(
|
|
139
86
|
() => Boolean(pr.value) && (uiMode.isAdvanced || !outcomeReadable.value),
|
|
@@ -149,10 +96,6 @@ const showPrChip = computed(
|
|
|
149
96
|
const showModuleChip = computed(
|
|
150
97
|
() => Boolean(task.value?.moduleName) && laneView.groupKey !== 'module',
|
|
151
98
|
)
|
|
152
|
-
function openOutcome() {
|
|
153
|
-
ui.openOutcome(props.taskId, task.value?.executionId ?? null)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
99
|
// This task's current agent run (if any). A failed run must surface the shared
|
|
157
100
|
// failure banner + retry — NOT a stuck progress bar — so the card never looks
|
|
158
101
|
// like it's still working after the run has terminated.
|
|
@@ -164,129 +107,6 @@ const runFailed = computed(() => agentRun.value?.status === 'failed')
|
|
|
164
107
|
const recurring = useRecurringPipelinesStore()
|
|
165
108
|
const schedule = computed(() => recurring.byBlock(props.taskId))
|
|
166
109
|
|
|
167
|
-
// Optimistic "Start": flip the button into a spinning "Starting…" state the
|
|
168
|
-
// instant it's clicked, before the server confirms. The button naturally
|
|
169
|
-
// unmounts once the stream pushes the block into `in_progress`; if the start
|
|
170
|
-
// call faults we revert and surface a toast.
|
|
171
|
-
const starting = ref(false)
|
|
172
|
-
|
|
173
|
-
async function run() {
|
|
174
|
-
if (!runnable.value) {
|
|
175
|
-
toast.add({
|
|
176
|
-
title: t('board.task.blockedByDependenciesTitle'),
|
|
177
|
-
description: t('board.task.waitingOn', { deps: unmet.value.map((d) => d.title).join(', ') }),
|
|
178
|
-
icon: 'i-lucide-lock',
|
|
179
|
-
})
|
|
180
|
-
return
|
|
181
|
-
}
|
|
182
|
-
const pipeline = defaultPipeline.value
|
|
183
|
-
if (!pipeline) {
|
|
184
|
-
toast.add({
|
|
185
|
-
title: t('board.task.noPipelineTitle'),
|
|
186
|
-
description: t('board.task.noPipelineBody'),
|
|
187
|
-
})
|
|
188
|
-
return
|
|
189
|
-
}
|
|
190
|
-
starting.value = true
|
|
191
|
-
// false ⇒ the run never started (the user cancelled the personal-password prompt, or
|
|
192
|
-
// the start was refused — the store surfaces the actionable toast itself). Revert the
|
|
193
|
-
// optimistic state; on success the button unmounts once the stream pushes in_progress.
|
|
194
|
-
const started = await execution.start(props.taskId, pipeline)
|
|
195
|
-
if (started) {
|
|
196
|
-
// Confirm the (optimistic) start landed — the button unmounts once the stream pushes
|
|
197
|
-
// in_progress, so without this the successful action gives no feedback. A sandboxed start
|
|
198
|
-
// says so here rather than borrowing the live wording: this is the moment the reader learns
|
|
199
|
-
// what they just started, and the two runs differ in what they will end up doing.
|
|
200
|
-
toast.add({
|
|
201
|
-
title: sandboxed.value ? t('board.dryRunToast.title') : t('board.task.startedToast.title'),
|
|
202
|
-
description: sandboxed.value
|
|
203
|
-
? t('board.dryRunToast.body', { name: pipeline.name })
|
|
204
|
-
: t('board.task.startedToast.body', { name: pipeline.name }),
|
|
205
|
-
color: sandboxed.value ? 'warning' : 'success',
|
|
206
|
-
icon: sandboxed.value ? 'i-lucide-shield' : 'i-lucide-play',
|
|
207
|
-
})
|
|
208
|
-
} else {
|
|
209
|
-
starting.value = false
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function review() {
|
|
214
|
-
ui.select(props.taskId)
|
|
215
|
-
ui.focus(props.taskId)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async function merge() {
|
|
219
|
-
// Merging a PR into its base is consequential and effectively irreversible — gate it behind a
|
|
220
|
-
// confirm (plain, not the destructive shape). `execution.mergePr` surfaces its own error toast.
|
|
221
|
-
const ok = await confirm({
|
|
222
|
-
title: t('board.task.mergeConfirm.title'),
|
|
223
|
-
description: t('board.task.mergeConfirm.body'),
|
|
224
|
-
confirmLabel: t('board.task.mergeConfirm.confirm'),
|
|
225
|
-
icon: 'i-lucide-git-merge',
|
|
226
|
-
})
|
|
227
|
-
if (!ok) return
|
|
228
|
-
await execution.mergePr(props.taskId)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// A `blocked` task is waiting on a human for one of two reasons — an agent-raised
|
|
232
|
-
// decision OR an approval gate — and both must surface here (a failed run is shown
|
|
233
|
-
// separately by the AgentFailureCard above). The board previously only handled
|
|
234
|
-
// decisions, so an approval-gated task was a dead end: it read "Decision needed"
|
|
235
|
-
// (the old generic `blocked` label) with no badge and a click that did nothing.
|
|
236
|
-
// Read off the per-block index rather than scanning the workspace-wide list: this computed is
|
|
237
|
-
// mounted once per card and invalidated by every execution event, so a `find` over `openDecisions`
|
|
238
|
-
// cost O(cards x open gates) per event. `decisionsByBlock` is the index built for exactly this.
|
|
239
|
-
const pendingDecision = computed(() => execution.decisionsByBlock.get(props.taskId)?.[0])
|
|
240
|
-
// The async stage an iterative reviewer gate (requirements-review / clarity-review) is
|
|
241
|
-
// mid-cycle in (folding the answers, then re-reviewing), or null. While set, the gate
|
|
242
|
-
// needs NO human action, so its approval is suppressed below and a working indicator
|
|
243
|
-
// shows instead.
|
|
244
|
-
const reviewStage = computed(() => reviews.stageForBlock(props.taskId))
|
|
245
|
-
const reviewStageLabel = computed(() =>
|
|
246
|
-
reviewStage.value === 'incorporating'
|
|
247
|
-
? t('board.task.incorporatingAnswers')
|
|
248
|
-
: reviewStage.value === 'reviewing'
|
|
249
|
-
? t('board.task.reReviewing')
|
|
250
|
-
: reviewStage.value === 'recommending'
|
|
251
|
-
? t('board.task.recommending')
|
|
252
|
-
: null,
|
|
253
|
-
)
|
|
254
|
-
const pendingApproval = computed(() => {
|
|
255
|
-
const a = execution.approvalsByBlock.get(props.taskId)?.[0]
|
|
256
|
-
// A reviewer gate whose review is incorporating / re-reviewing in the driver is doing
|
|
257
|
-
// background work, not awaiting a human — don't surface it as "Approval needed".
|
|
258
|
-
if (a && reviews.isBackground(a.agentKind, props.taskId)) return undefined
|
|
259
|
-
return a
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
/** What this blocked task actually needs from a human — drives the card's label,
|
|
263
|
-
* pulse and action. Decision takes precedence over approval (a step never holds
|
|
264
|
-
* both at once; this is just a stable order). Null when nothing is pending. */
|
|
265
|
-
const attention = computed<{
|
|
266
|
-
label: string
|
|
267
|
-
icon: string
|
|
268
|
-
action: string
|
|
269
|
-
open: () => void
|
|
270
|
-
} | null>(() => {
|
|
271
|
-
const d = pendingDecision.value
|
|
272
|
-
if (d)
|
|
273
|
-
return {
|
|
274
|
-
label: t('board.task.decisionNeeded'),
|
|
275
|
-
icon: 'i-lucide-circle-help',
|
|
276
|
-
action: t('board.task.resolve'),
|
|
277
|
-
open: () => ui.openDecision(d.instanceId, d.decision.id),
|
|
278
|
-
}
|
|
279
|
-
const a = pendingApproval.value
|
|
280
|
-
if (a)
|
|
281
|
-
return {
|
|
282
|
-
label: t('board.task.approvalNeeded'),
|
|
283
|
-
icon: 'i-lucide-shield-check',
|
|
284
|
-
action: t('board.task.approve'),
|
|
285
|
-
open: () => ui.openApprovalDetail(a.instanceId, a.approval.id),
|
|
286
|
-
}
|
|
287
|
-
return null
|
|
288
|
-
})
|
|
289
|
-
|
|
290
110
|
/** Specific header copy: a failed run reads "Failed", a parked task reads its
|
|
291
111
|
* decision/approval reason, otherwise the generic status label. */
|
|
292
112
|
const statusText = computed(() =>
|
|
@@ -295,6 +115,11 @@ const statusText = computed(() =>
|
|
|
295
115
|
: (reviewStageLabel.value ?? attention.value?.label ?? statusMeta.value?.label ?? ''),
|
|
296
116
|
)
|
|
297
117
|
|
|
118
|
+
function review() {
|
|
119
|
+
ui.select(props.taskId)
|
|
120
|
+
ui.focus(props.taskId)
|
|
121
|
+
}
|
|
122
|
+
|
|
298
123
|
// Clicking the card body only selects the task (opening the inspector so the human can
|
|
299
124
|
// interact with it). Whatever the task is parked on — a decision, an approval, or the
|
|
300
125
|
// requirements review — is opened explicitly via the action button below, never by a
|
|
@@ -461,10 +286,11 @@ function selectTask() {
|
|
|
461
286
|
size="xs"
|
|
462
287
|
:icon="!runnable ? 'i-lucide-lock' : sandboxed ? 'i-lucide-shield' : 'i-lucide-play'"
|
|
463
288
|
:loading="starting"
|
|
464
|
-
:disabled="!runnable || starting"
|
|
289
|
+
:disabled="!runnable || starting || !canExecute"
|
|
465
290
|
data-testid="task-start"
|
|
466
291
|
:title="
|
|
467
|
-
|
|
292
|
+
executeDeniedTitle ??
|
|
293
|
+
(!runnable
|
|
468
294
|
? t('board.task.waitingOn', { deps: unmet.map((d) => d.title).join(', ') })
|
|
469
295
|
: sandboxed
|
|
470
296
|
? t('board.task.startPipelineDryRun', {
|
|
@@ -472,7 +298,7 @@ function selectTask() {
|
|
|
472
298
|
})
|
|
473
299
|
: t('board.task.startPipeline', {
|
|
474
300
|
name: defaultPipeline?.name ?? t('board.task.pipelineFallback'),
|
|
475
|
-
})
|
|
301
|
+
}))
|
|
476
302
|
"
|
|
477
303
|
@click.stop="run"
|
|
478
304
|
>
|
|
@@ -529,7 +355,14 @@ function selectTask() {
|
|
|
529
355
|
>
|
|
530
356
|
{{ t('board.task.review') }}
|
|
531
357
|
</UButton>
|
|
532
|
-
<UButton
|
|
358
|
+
<UButton
|
|
359
|
+
color="success"
|
|
360
|
+
size="xs"
|
|
361
|
+
icon="i-lucide-git-merge"
|
|
362
|
+
:disabled="!canExecute"
|
|
363
|
+
:title="executeDeniedTitle"
|
|
364
|
+
@click.stop="merge"
|
|
365
|
+
>
|
|
533
366
|
{{ t('board.task.merge') }}
|
|
534
367
|
</UButton>
|
|
535
368
|
</template>
|
|
@@ -15,7 +15,7 @@ const props = defineProps<{ job: BootstrapJob }>()
|
|
|
15
15
|
const emit = defineEmits<{ close: [] }>()
|
|
16
16
|
|
|
17
17
|
const agentRuns = useAgentRunsStore()
|
|
18
|
-
const
|
|
18
|
+
const actionToast = useActionToast()
|
|
19
19
|
const { present } = usePipelineErrorToast()
|
|
20
20
|
const { t } = useI18n()
|
|
21
21
|
|
|
@@ -146,13 +146,10 @@ async function submit() {
|
|
|
146
146
|
})),
|
|
147
147
|
...(overallNotes.value.trim() ? { notes: overallNotes.value.trim() } : {}),
|
|
148
148
|
})
|
|
149
|
-
toast.
|
|
150
|
-
title: t('bootstrap.adoption.toast.approved'),
|
|
149
|
+
actionToast.success('bootstrap.adoption.toast.approved', {
|
|
151
150
|
description: t('bootstrap.adoption.toast.approvedDesc', {
|
|
152
151
|
directory: props.job.monorepo?.directory ?? '',
|
|
153
152
|
}),
|
|
154
|
-
icon: 'i-lucide-check',
|
|
155
|
-
color: 'success',
|
|
156
153
|
})
|
|
157
154
|
open.value = false
|
|
158
155
|
emit('close')
|
|
@@ -29,8 +29,8 @@ const bootstrap = useBootstrapStore()
|
|
|
29
29
|
const agentRuns = useAgentRunsStore()
|
|
30
30
|
const github = useGitHubStore()
|
|
31
31
|
const board = useBoardStore()
|
|
32
|
-
const
|
|
33
|
-
const { present } = usePipelineErrorToast()
|
|
32
|
+
const actionToast = useActionToast()
|
|
33
|
+
const { present, presentReported } = usePipelineErrorToast()
|
|
34
34
|
const { freeFramePosition, focusFrame } = useFramePlacement()
|
|
35
35
|
const { t } = useI18n()
|
|
36
36
|
const { confirmAction, toastDone } = useConfirmAction()
|
|
@@ -367,11 +367,8 @@ async function openCreateRepo() {
|
|
|
367
367
|
private: isPrivate.value,
|
|
368
368
|
description: description.value.trim() || undefined,
|
|
369
369
|
})
|
|
370
|
-
toast.
|
|
371
|
-
title: t('bootstrap.toast.repoCreated'),
|
|
370
|
+
actionToast.success('bootstrap.toast.repoCreated', {
|
|
372
371
|
description: `${repo.owner}/${repo.name}`,
|
|
373
|
-
icon: 'i-lucide-check',
|
|
374
|
-
color: 'success',
|
|
375
372
|
})
|
|
376
373
|
} catch (e) {
|
|
377
374
|
present(e, 'bootstrap.toast.repoCreateFailed')
|
|
@@ -426,26 +423,20 @@ async function launch() {
|
|
|
426
423
|
if (job.status === 'failed') {
|
|
427
424
|
// The container couldn't even start (pre-flight failure, e.g. the target
|
|
428
425
|
// repo isn't empty) — surfaced synchronously, before any board frame.
|
|
429
|
-
toast.
|
|
430
|
-
|
|
431
|
-
description: job.error ?? t('bootstrap.toast.failedFallback'),
|
|
432
|
-
icon: 'i-lucide-triangle-alert',
|
|
433
|
-
color: 'error',
|
|
426
|
+
presentReported(job.error, 'bootstrap.toast.failed', {
|
|
427
|
+
descriptionKey: 'bootstrap.toast.failedFallback',
|
|
434
428
|
})
|
|
435
429
|
} else {
|
|
436
430
|
// Running: the container is spinning up. A provisional service card now
|
|
437
431
|
// shows on the board and tracks live progress; the run continues in the
|
|
438
432
|
// background and becomes a real, droppable service when it finishes.
|
|
439
|
-
toast.
|
|
440
|
-
title: t('bootstrap.toast.started'),
|
|
433
|
+
actionToast.info('bootstrap.toast.started', {
|
|
441
434
|
// A monorepo run does not run straight through: it surveys, then waits for the
|
|
442
435
|
// reviewer. Saying "bootstrapping…" there would set the wrong expectation about who
|
|
443
436
|
// the next move belongs to.
|
|
444
437
|
description: job.monorepo
|
|
445
438
|
? t('bootstrap.toast.startedMonorepoDesc', { directory: job.monorepo.directory })
|
|
446
439
|
: t('bootstrap.toast.startedDesc', { repo: job.repoName }),
|
|
447
|
-
icon: 'i-lucide-loader-circle',
|
|
448
|
-
color: 'info',
|
|
449
440
|
})
|
|
450
441
|
repoName.value = ''
|
|
451
442
|
description.value = ''
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// are one rule, not two. How a state RENDERS is `BOOTSTRAP_STEP_STYLE`, beside the vocabulary
|
|
13
13
|
// it is keyed by.
|
|
14
14
|
import type { BootstrapStepId, BootstrapStepState } from '@cat-factory/contracts'
|
|
15
|
+
import Spinner from '~/components/common/Spinner.vue'
|
|
15
16
|
|
|
16
17
|
const props = defineProps<{ runId: string }>()
|
|
17
18
|
|
|
@@ -38,8 +39,9 @@ function stateLabel(state: BootstrapStepState): string {
|
|
|
38
39
|
:data-step="step.id"
|
|
39
40
|
:data-state="step.state"
|
|
40
41
|
>
|
|
41
|
-
<
|
|
42
|
+
<Spinner
|
|
42
43
|
:name="BOOTSTRAP_STEP_STYLE[step.state].icon"
|
|
44
|
+
:spinning="BOOTSTRAP_STEP_STYLE[step.state].spin"
|
|
43
45
|
class="mt-px h-3 w-3 shrink-0"
|
|
44
46
|
:class="BOOTSTRAP_STEP_STYLE[step.state].iconClass"
|
|
45
47
|
/>
|
|
@@ -20,10 +20,12 @@ import type {
|
|
|
20
20
|
ReviewItemStatus,
|
|
21
21
|
} from '~/types/brainstorm'
|
|
22
22
|
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
23
|
+
import Spinner from '~/components/common/Spinner.vue'
|
|
24
|
+
import EmptyState from '~/components/common/EmptyState.vue'
|
|
23
25
|
|
|
24
26
|
const board = useBoardStore()
|
|
25
27
|
const brainstorm = useBrainstormStore()
|
|
26
|
-
const
|
|
28
|
+
const actionToast = useActionToast()
|
|
27
29
|
const { present } = usePipelineErrorToast()
|
|
28
30
|
const { t } = useI18n()
|
|
29
31
|
const access = useWorkspaceAccess()
|
|
@@ -194,10 +196,9 @@ async function incorporate(feedback?: string) {
|
|
|
194
196
|
}
|
|
195
197
|
redoComment.value = ''
|
|
196
198
|
showRedo.value = false
|
|
197
|
-
toast.
|
|
198
|
-
|
|
199
|
+
actionToast.info('brainstorm.toast.draftingTitle', {
|
|
200
|
+
params: { doc: docNoun.value },
|
|
199
201
|
description: t('brainstorm.toast.draftingDescription'),
|
|
200
|
-
icon: 'i-lucide-wand-sparkles',
|
|
201
202
|
})
|
|
202
203
|
close()
|
|
203
204
|
}
|
|
@@ -207,15 +208,16 @@ async function reReview() {
|
|
|
207
208
|
try {
|
|
208
209
|
const updated = await brainstorm.reReview(blockId.value, activeStage.value)
|
|
209
210
|
const newCount = brainstorm.openCount(updated)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
211
|
+
if (updated.status === 'incorporated') {
|
|
212
|
+
actionToast.success('brainstorm.toast.reReviewSettled')
|
|
213
|
+
} else if (updated.status === 'exceeded') {
|
|
214
|
+
actionToast.warning('brainstorm.toast.reReviewExceeded')
|
|
215
|
+
} else {
|
|
216
|
+
actionToast.info('brainstorm.toast.reReviewNewOptions', {
|
|
217
|
+
params: { count: newCount },
|
|
218
|
+
plural: newCount,
|
|
219
|
+
})
|
|
220
|
+
}
|
|
219
221
|
} catch (e) {
|
|
220
222
|
present(e, 'brainstorm.toast.reReviewError')
|
|
221
223
|
}
|
|
@@ -226,7 +228,7 @@ async function proceed() {
|
|
|
226
228
|
acting.value = true
|
|
227
229
|
try {
|
|
228
230
|
await brainstorm.proceed(blockId.value, activeStage.value)
|
|
229
|
-
|
|
231
|
+
actionToast.info('brainstorm.toast.proceeding')
|
|
230
232
|
} catch (e) {
|
|
231
233
|
present(e, 'brainstorm.toast.proceedError')
|
|
232
234
|
} finally {
|
|
@@ -240,12 +242,12 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
240
242
|
try {
|
|
241
243
|
await brainstorm.resolveExceeded(blockId.value, activeStage.value, choice)
|
|
242
244
|
if (choice === 'stop-reset') {
|
|
243
|
-
|
|
245
|
+
actionToast.info('brainstorm.toast.taskReset')
|
|
244
246
|
close()
|
|
245
247
|
} else if (choice === 'proceed') {
|
|
246
|
-
|
|
248
|
+
actionToast.info('brainstorm.toast.proceeding')
|
|
247
249
|
} else {
|
|
248
|
-
|
|
250
|
+
actionToast.info('brainstorm.toast.extraRoundGranted')
|
|
249
251
|
}
|
|
250
252
|
} catch (e) {
|
|
251
253
|
present(e, 'brainstorm.toast.resolveError')
|
|
@@ -291,20 +293,24 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
291
293
|
</p>
|
|
292
294
|
|
|
293
295
|
<!-- empty state -->
|
|
294
|
-
<
|
|
296
|
+
<EmptyState
|
|
295
297
|
v-if="!session && !busy && !loading"
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
298
|
+
icon="i-lucide-lightbulb"
|
|
299
|
+
:title="t('brainstorm.empty')"
|
|
300
|
+
/>
|
|
301
|
+
|
|
302
|
+
<!-- initial fetch on open -->
|
|
303
|
+
<div v-else-if="loading && !busy && !session" class="flex flex-col gap-3">
|
|
304
|
+
<USkeleton v-for="n in 3" :key="n" class="h-20 w-full rounded-lg" />
|
|
299
305
|
</div>
|
|
300
306
|
|
|
301
|
-
<!--
|
|
307
|
+
<!-- an agent pass running -->
|
|
302
308
|
<div
|
|
303
|
-
v-else-if="
|
|
309
|
+
v-else-if="busy && !session"
|
|
304
310
|
class="flex items-center justify-center gap-2 p-8 text-sm text-muted"
|
|
305
311
|
>
|
|
306
|
-
<
|
|
307
|
-
{{
|
|
312
|
+
<Spinner class="h-4 w-4" />
|
|
313
|
+
{{ t('brainstorm.generating') }}
|
|
308
314
|
</div>
|
|
309
315
|
|
|
310
316
|
<template v-else-if="session">
|
|
@@ -332,7 +338,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
332
338
|
v-else-if="working"
|
|
333
339
|
class="mb-4 flex items-center gap-2 rounded-lg border border-app-warning-900/60 bg-app-warning-950/30 p-4 text-sm text-app-warning-200"
|
|
334
340
|
>
|
|
335
|
-
<
|
|
341
|
+
<Spinner class="h-5 w-5 shrink-0" />
|
|
336
342
|
<span v-if="incorporating">
|
|
337
343
|
{{ t('brainstorm.working.incorporating', { doc: docNoun }) }}
|
|
338
344
|
</span>
|
|
@@ -29,6 +29,8 @@ import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
|
29
29
|
import StepRunMeta from '~/components/panels/StepRunMeta.vue'
|
|
30
30
|
import MarkdownProse from '~/components/common/MarkdownProse.vue'
|
|
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 execution = useExecutionStore()
|
|
34
36
|
const board = useBoardStore()
|
|
@@ -304,11 +306,12 @@ const PHASE_ICON: Record<string, string> = {
|
|
|
304
306
|
:data-testid="`bug-fishing-phase-${phase.id}`"
|
|
305
307
|
@click="selectedPassKey = passKey(phase)"
|
|
306
308
|
>
|
|
307
|
-
<
|
|
309
|
+
<Spinner
|
|
308
310
|
:name="PHASE_ICON[phase.status] ?? 'i-lucide-circle-dashed'"
|
|
311
|
+
:spinning="phase.status === 'fishing'"
|
|
309
312
|
class="mt-0.5 h-3.5 w-3.5 shrink-0"
|
|
310
313
|
:class="{
|
|
311
|
-
'
|
|
314
|
+
'text-app-info-300': phase.status === 'fishing',
|
|
312
315
|
'text-app-success-400': phase.status === 'completed',
|
|
313
316
|
'text-app-warning-400': phase.status === 'failed',
|
|
314
317
|
'text-app-600': phase.status === 'pending',
|
|
@@ -379,7 +382,7 @@ const PHASE_ICON: Record<string, string> = {
|
|
|
379
382
|
data-testid="bug-fishing-in-progress"
|
|
380
383
|
class="mb-4 flex items-center gap-2 rounded-lg border border-app-info-500/25 bg-app-info-500/5 px-3 py-2 text-xs text-app-info-200"
|
|
381
384
|
>
|
|
382
|
-
<
|
|
385
|
+
<Spinner class="h-4 w-4 shrink-0" />
|
|
383
386
|
<span>
|
|
384
387
|
{{
|
|
385
388
|
t('bugFishing.stillFishing', {
|
|
@@ -459,20 +462,19 @@ const PHASE_ICON: Record<string, string> = {
|
|
|
459
462
|
{{ bugFishing.error }}
|
|
460
463
|
</p>
|
|
461
464
|
|
|
462
|
-
<!-- An expedition that caught nothing is a real answer
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
<
|
|
465
|
+
<!-- An expedition that caught nothing is a real answer. The copy says which of the two it
|
|
466
|
+
is, because "nothing found" and "nothing left to triage" are different things to be
|
|
467
|
+
told. -->
|
|
468
|
+
<EmptyState
|
|
466
469
|
v-if="visibleFindings.length === 0"
|
|
467
470
|
data-testid="bug-fishing-empty"
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
{{
|
|
471
|
+
:icon="findings.length === 0 ? 'i-lucide-fish' : 'i-lucide-check-check'"
|
|
472
|
+
:title="
|
|
471
473
|
findings.length === 0
|
|
472
474
|
? t('bugFishing.empty.nothingCaught')
|
|
473
475
|
: t('bugFishing.empty.allTriaged')
|
|
474
|
-
|
|
475
|
-
|
|
476
|
+
"
|
|
477
|
+
/>
|
|
476
478
|
|
|
477
479
|
<ul v-else class="space-y-2">
|
|
478
480
|
<li
|
|
@@ -591,7 +593,7 @@ const PHASE_ICON: Record<string, string> = {
|
|
|
591
593
|
data-testid="bug-fishing-finding-spawning"
|
|
592
594
|
class="mt-2 flex flex-wrap items-center gap-2 text-2xs text-muted"
|
|
593
595
|
>
|
|
594
|
-
<
|
|
596
|
+
<Spinner class="h-3.5 w-3.5" />
|
|
595
597
|
<span>{{ t('bugFishing.finding.spawning') }}</span>
|
|
596
598
|
</div>
|
|
597
599
|
|