@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
|
@@ -272,6 +272,30 @@ describe('usePipelineErrorToast', () => {
|
|
|
272
272
|
})
|
|
273
273
|
})
|
|
274
274
|
|
|
275
|
+
describe('presentReported', () => {
|
|
276
|
+
it('keeps a failure reported as data behind the disclosure, under translated copy', () => {
|
|
277
|
+
// A job row's `error` is backend prose: it must be copyable detail, never the headline, and it
|
|
278
|
+
// must not be read as a network fault the way `present` reads a bare string.
|
|
279
|
+
usePipelineErrorToast().presentReported('stack exited with code 1', 'x.startFailed')
|
|
280
|
+
const arg = add.mock.calls[0]![0]
|
|
281
|
+
expect(arg.title).toBe('x.startFailed')
|
|
282
|
+
expect(arg.description).toBe('errors.reported.description')
|
|
283
|
+
expect(arg.duration).toBe(0)
|
|
284
|
+
arg.actions[0].onClick()
|
|
285
|
+
expect(update).toHaveBeenCalledWith('toast-1', {
|
|
286
|
+
description: 'stack exited with code 1',
|
|
287
|
+
actions: [expect.objectContaining({ label: 'common.copyDetails' })],
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('takes a site description key, and offers no disclosure when nothing was reported', () => {
|
|
292
|
+
usePipelineErrorToast().presentReported(null, 'x.failed', { descriptionKey: 'x.fallback' })
|
|
293
|
+
const arg = add.mock.calls[0]![0]
|
|
294
|
+
expect(arg.description).toBe('x.fallback')
|
|
295
|
+
expect(arg.actions).toEqual([expect.objectContaining({ label: 'common.copyDetails' })])
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
|
|
275
299
|
describe('describeGenericFailure', () => {
|
|
276
300
|
it('maps each known status class to its own description key', () => {
|
|
277
301
|
for (const code of [
|
|
@@ -683,7 +683,12 @@ export function usePipelineErrorToast() {
|
|
|
683
683
|
// ships in the base `en` catalog, so a locale missing it renders English via `fallbackLocale`
|
|
684
684
|
// (better than the raw prose this replaced) and a bare key can never leak.
|
|
685
685
|
const title = t(fallbackTitleKey, titleParams ?? {})
|
|
686
|
-
|
|
686
|
+
showFailure(title, t(failure.descriptionKey), detail)
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** The failure toast both generic paths render: translated title and description, the raw
|
|
690
|
+
* detail behind "Show details", and the whole report copyable. */
|
|
691
|
+
function showFailure(title: string, description: string, detail: string): void {
|
|
687
692
|
const report = [title, description, detail].filter((part) => part.length > 0).join('\n')
|
|
688
693
|
const added = toast.add({
|
|
689
694
|
title,
|
|
@@ -713,6 +718,25 @@ export function usePipelineErrorToast() {
|
|
|
713
718
|
})
|
|
714
719
|
}
|
|
715
720
|
|
|
721
|
+
/**
|
|
722
|
+
* A failure the backend reported as DATA rather than as a failed call: a job row with
|
|
723
|
+
* `status: 'failed'` and its `error` text, a probe verdict with a `message`. There is no
|
|
724
|
+
* envelope, so `present` would misread it as a network fault. The reported text is backend prose
|
|
725
|
+
* and so DETAIL, never the headline: it goes behind "Show details" and into the copied report,
|
|
726
|
+
* under a translated description (`descriptionKey`, or the generic "it reported a failure").
|
|
727
|
+
*/
|
|
728
|
+
function presentReported(
|
|
729
|
+
detail: string | null | undefined,
|
|
730
|
+
titleKey: string,
|
|
731
|
+
opts: { params?: Record<string, unknown>; descriptionKey?: string } = {},
|
|
732
|
+
): void {
|
|
733
|
+
showFailure(
|
|
734
|
+
t(titleKey, opts.params ?? {}),
|
|
735
|
+
t(opts.descriptionKey ?? 'errors.reported.description'),
|
|
736
|
+
detail?.trim() ?? '',
|
|
737
|
+
)
|
|
738
|
+
}
|
|
739
|
+
|
|
716
740
|
/**
|
|
717
741
|
* Present `error` as a toast. `fallbackTitleKey` is an i18n message key used for
|
|
718
742
|
* non-conflict failures and any conflict reason without a dedicated title; `titleParams` carries
|
|
@@ -732,5 +756,5 @@ export function usePipelineErrorToast() {
|
|
|
732
756
|
presentGenericFailure(error, fallbackTitleKey, titleParams)
|
|
733
757
|
}
|
|
734
758
|
|
|
735
|
-
return { present }
|
|
759
|
+
return { present, presentReported }
|
|
736
760
|
}
|
|
@@ -2,6 +2,7 @@ import type { RunDefaultScope } from '@cat-factory/contracts'
|
|
|
2
2
|
import type { Pipeline } from '~/types/domain'
|
|
3
3
|
import { usePipelinesStore } from '~/stores/pipelines'
|
|
4
4
|
import { usePipelineErrorToast } from '~/composables/usePipelineErrorToast'
|
|
5
|
+
import { useActionToast } from '~/composables/useActionToast'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* The actions a row of the saved-pipeline LIBRARY offers: archive, promote to a scope default,
|
|
@@ -14,7 +15,7 @@ import { usePipelineErrorToast } from '~/composables/usePipelineErrorToast'
|
|
|
14
15
|
*/
|
|
15
16
|
export function usePipelineLibraryActions() {
|
|
16
17
|
const pipelines = usePipelinesStore()
|
|
17
|
-
const
|
|
18
|
+
const actionToast = useActionToast()
|
|
18
19
|
const { t } = useI18n()
|
|
19
20
|
const { present } = usePipelineErrorToast()
|
|
20
21
|
const { confirm } = useConfirm()
|
|
@@ -24,8 +25,8 @@ export function usePipelineLibraryActions() {
|
|
|
24
25
|
try {
|
|
25
26
|
if (p.archived) await pipelines.unarchive(p.id)
|
|
26
27
|
else await pipelines.archive(p.id)
|
|
27
|
-
} catch {
|
|
28
|
-
|
|
28
|
+
} catch (error) {
|
|
29
|
+
present(error, 'pipeline.builder.toast.updateFailed')
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -68,13 +69,11 @@ export function usePipelineLibraryActions() {
|
|
|
68
69
|
async function clone(p: Pipeline) {
|
|
69
70
|
try {
|
|
70
71
|
const copy = await pipelines.clonePipeline(p.id)
|
|
71
|
-
toast.
|
|
72
|
-
|
|
73
|
-
color: 'success',
|
|
74
|
-
icon: 'i-lucide-copy',
|
|
72
|
+
actionToast.success('pipeline.builder.toast.cloned', {
|
|
73
|
+
params: { name: p.name, copy: copy.name },
|
|
75
74
|
})
|
|
76
|
-
} catch {
|
|
77
|
-
|
|
75
|
+
} catch (error) {
|
|
76
|
+
present(error, 'pipeline.builder.toast.cloneFailed')
|
|
78
77
|
}
|
|
79
78
|
}
|
|
80
79
|
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { computed, ref } from 'vue'
|
|
2
|
+
import { defaultBuildPipelineId } from '@cat-factory/contracts'
|
|
3
|
+
import type { Block } from '~/types/domain'
|
|
4
|
+
import { composeRunOutcome, hasOutcomeToShow } from '~/utils/runOutcome'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What a task offers a human to DO, and the handlers that do it.
|
|
8
|
+
*
|
|
9
|
+
* Shared by the two surfaces that put a task's action within one click: the board's task card
|
|
10
|
+
* and the queue's card. Both must offer the same Start (same pipeline), the same Resolve (same
|
|
11
|
+
* gate) and the same Merge (same confirm), or a reader would learn two products.
|
|
12
|
+
*/
|
|
13
|
+
export function useTaskActions(taskId: () => string) {
|
|
14
|
+
const board = useBoardStore()
|
|
15
|
+
const execution = useExecutionStore()
|
|
16
|
+
const ui = useUiStore()
|
|
17
|
+
const reviews = useReviewStage()
|
|
18
|
+
const toast = useActionToast()
|
|
19
|
+
const { t } = useI18n()
|
|
20
|
+
const { confirm } = useConfirm()
|
|
21
|
+
const access = useWorkspaceAccess()
|
|
22
|
+
|
|
23
|
+
const task = computed<Block | undefined>(() => board.getBlock(taskId()))
|
|
24
|
+
|
|
25
|
+
// This Start is a ONE-TAP live start, so it has nothing to ask for and offers no dry-run
|
|
26
|
+
// request. What it does owe the reader is the half that is not a choice: a preset that sandboxes
|
|
27
|
+
// their role means this button opens a pull request and merges nothing, and a card that said so
|
|
28
|
+
// only after the fact would leave that to be discovered from a run that stops at the merge.
|
|
29
|
+
const { forcedFor } = useDryRunPolicy()
|
|
30
|
+
const sandboxed = computed(() => forcedFor(taskId()))
|
|
31
|
+
|
|
32
|
+
/** Deps that haven't merged yet: these block this task from running. */
|
|
33
|
+
const unmet = computed(() => board.unmetDeps(taskId()))
|
|
34
|
+
const runnable = computed(() => board.isRunnable(taskId()))
|
|
35
|
+
|
|
36
|
+
// Start and Merge both need `runs.execute`. A read-only member sees them disabled with the reason,
|
|
37
|
+
// the way the shared failure banner treats Retry, rather than pressing a button the server
|
|
38
|
+
// refuses. Asked once here so the two cards cannot gate them differently.
|
|
39
|
+
const canExecute = computed(() => access.canExecuteRuns.value)
|
|
40
|
+
const executeDeniedTitle = computed(() =>
|
|
41
|
+
canExecute.value ? undefined : t('access.noRunExecute'),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const pipelineFor = useStartPipelineResolver()
|
|
45
|
+
const defaultPipeline = computed(() => pipelineFor(task.value))
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether the outcome summary has something to show, asked of the SAME reduction the window
|
|
49
|
+
* renders and the inspector's button gates on: a surface that offered "read the result" on a
|
|
50
|
+
* task whose every section says "nothing here" would teach people the surface is empty. A task
|
|
51
|
+
* marked done by hand, with no pull request and no run, is that task.
|
|
52
|
+
*/
|
|
53
|
+
const outcomeReadable = computed(() => {
|
|
54
|
+
const block = task.value
|
|
55
|
+
if (!block) return false
|
|
56
|
+
return hasOutcomeToShow(
|
|
57
|
+
composeRunOutcome({ block, instance: execution.getInstance(block.executionId) ?? null }),
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
function openOutcome() {
|
|
62
|
+
ui.openOutcome(taskId(), task.value?.executionId ?? null)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Optimistic "Start": flip the button into a spinning "Starting…" state the instant it's
|
|
66
|
+
// clicked, before the server confirms. The button naturally unmounts once the stream pushes the
|
|
67
|
+
// block into `in_progress`; if the start call faults we revert and surface a toast.
|
|
68
|
+
const starting = ref(false)
|
|
69
|
+
|
|
70
|
+
async function start() {
|
|
71
|
+
if (!runnable.value) {
|
|
72
|
+
// A refusal decided in the browser, in translated copy: the `error` tone (`useActionToast`).
|
|
73
|
+
toast.error('board.task.blockedByDependenciesTitle', {
|
|
74
|
+
description: t('board.task.waitingOn', {
|
|
75
|
+
deps: unmet.value.map((d) => d.title).join(', '),
|
|
76
|
+
}),
|
|
77
|
+
})
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
const pipeline = defaultPipeline.value
|
|
81
|
+
if (!pipeline) {
|
|
82
|
+
toast.error('board.task.noPipelineTitle', { description: t('board.task.noPipelineBody') })
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
starting.value = true
|
|
86
|
+
// false ⇒ the run never started (the user cancelled the personal-password prompt, or the
|
|
87
|
+
// start was refused: the store surfaces the actionable toast itself). Revert the optimistic
|
|
88
|
+
// state; on success the button unmounts once the stream pushes in_progress.
|
|
89
|
+
const started = await execution.start(taskId(), pipeline)
|
|
90
|
+
if (started) {
|
|
91
|
+
// Confirm the (optimistic) start landed: the button unmounts once the stream pushes
|
|
92
|
+
// in_progress, so without this the successful action gives no feedback. A sandboxed start
|
|
93
|
+
// says so here rather than borrowing the live wording: this is the moment the reader learns
|
|
94
|
+
// what they just started, and the two runs differ in what they will end up doing.
|
|
95
|
+
// A sandboxed run is `warning`: it ran, and its outcome differs in a way the reader should
|
|
96
|
+
// know (it opens a pull request and merges nothing), so it stays until closed.
|
|
97
|
+
if (sandboxed.value) {
|
|
98
|
+
toast.warning('board.dryRunToast.title', {
|
|
99
|
+
description: t('board.dryRunToast.body', { name: pipeline.name }),
|
|
100
|
+
})
|
|
101
|
+
} else {
|
|
102
|
+
toast.success('board.task.startedToast.title', {
|
|
103
|
+
description: t('board.task.startedToast.body', { name: pipeline.name }),
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
starting.value = false
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function merge() {
|
|
112
|
+
// Merging a PR into its base is consequential and effectively irreversible: gate it behind a
|
|
113
|
+
// confirm (plain, not the destructive shape). `execution.mergePr` surfaces its own error toast.
|
|
114
|
+
const ok = await confirm({
|
|
115
|
+
title: t('board.task.mergeConfirm.title'),
|
|
116
|
+
description: t('board.task.mergeConfirm.body'),
|
|
117
|
+
confirmLabel: t('board.task.mergeConfirm.confirm'),
|
|
118
|
+
icon: 'i-lucide-git-merge',
|
|
119
|
+
})
|
|
120
|
+
if (!ok) return
|
|
121
|
+
await execution.mergePr(taskId())
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// A `blocked` task is waiting on a human for one of two reasons, an agent-raised decision OR an
|
|
125
|
+
// approval gate, and both must surface (a failed run is shown separately by the
|
|
126
|
+
// AgentFailureCard). Read off the per-block index rather than scanning the workspace-wide list:
|
|
127
|
+
// this computed is mounted once per card and invalidated by every execution event.
|
|
128
|
+
const pendingDecision = computed(() => execution.decisionsByBlock.get(taskId())?.[0])
|
|
129
|
+
// The async stage an iterative reviewer gate (requirements-review / clarity-review) is mid-cycle
|
|
130
|
+
// in (folding the answers, then re-reviewing), or null. While set, the gate needs NO human
|
|
131
|
+
// action, so its approval is suppressed below and a working indicator shows instead.
|
|
132
|
+
const reviewStage = computed(() => reviews.stageForBlock(taskId()))
|
|
133
|
+
const reviewStageLabel = computed(() =>
|
|
134
|
+
reviewStage.value === 'incorporating'
|
|
135
|
+
? t('board.task.incorporatingAnswers')
|
|
136
|
+
: reviewStage.value === 'reviewing'
|
|
137
|
+
? t('board.task.reReviewing')
|
|
138
|
+
: reviewStage.value === 'recommending'
|
|
139
|
+
? t('board.task.recommending')
|
|
140
|
+
: null,
|
|
141
|
+
)
|
|
142
|
+
const pendingApproval = computed(() => {
|
|
143
|
+
// A reviewer gate whose review is incorporating / re-reviewing in the driver is doing
|
|
144
|
+
// background work, not awaiting a human: skip it, and offer the first gate a human IS
|
|
145
|
+
// awaiting. The lane classifier counts any such gate, so a card filed under Needs you for an
|
|
146
|
+
// approval always has that approval to open.
|
|
147
|
+
return execution.approvalsByBlock
|
|
148
|
+
.get(taskId())
|
|
149
|
+
?.find((a) => !reviews.isBackground(a.agentKind, taskId()))
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* What this blocked task actually needs from a human: drives the card's label, pulse and
|
|
154
|
+
* action. Decision takes precedence over approval (a step never holds both at once; this is
|
|
155
|
+
* just a stable order). Null when nothing is pending.
|
|
156
|
+
*/
|
|
157
|
+
const attention = computed<{
|
|
158
|
+
label: string
|
|
159
|
+
icon: string
|
|
160
|
+
action: string
|
|
161
|
+
open: () => void
|
|
162
|
+
} | null>(() => {
|
|
163
|
+
const d = pendingDecision.value
|
|
164
|
+
if (d)
|
|
165
|
+
return {
|
|
166
|
+
label: t('board.task.decisionNeeded'),
|
|
167
|
+
icon: 'i-lucide-circle-help',
|
|
168
|
+
action: t('board.task.resolve'),
|
|
169
|
+
open: () => ui.openDecision(d.instanceId, d.decision.id),
|
|
170
|
+
}
|
|
171
|
+
const a = pendingApproval.value
|
|
172
|
+
if (a)
|
|
173
|
+
return {
|
|
174
|
+
label: t('board.task.approvalNeeded'),
|
|
175
|
+
icon: 'i-lucide-shield-check',
|
|
176
|
+
action: t('board.task.approve'),
|
|
177
|
+
open: () => ui.openApprovalDetail(a.instanceId, a.approval.id),
|
|
178
|
+
}
|
|
179
|
+
return null
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
task,
|
|
184
|
+
sandboxed,
|
|
185
|
+
unmet,
|
|
186
|
+
runnable,
|
|
187
|
+
canExecute,
|
|
188
|
+
executeDeniedTitle,
|
|
189
|
+
defaultPipeline,
|
|
190
|
+
outcomeReadable,
|
|
191
|
+
openOutcome,
|
|
192
|
+
starting,
|
|
193
|
+
start,
|
|
194
|
+
merge,
|
|
195
|
+
pendingDecision,
|
|
196
|
+
reviewStage,
|
|
197
|
+
reviewStageLabel,
|
|
198
|
+
attention,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The pipeline a plain "Start" runs, as the descriptor `execution.start` takes. */
|
|
203
|
+
export interface StartPipeline {
|
|
204
|
+
id: string
|
|
205
|
+
name: string
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Resolve the pipeline a plain "Start" uses for a task. A function rather than a computed so
|
|
210
|
+
* a caller can ask it about any task, not only about the one its card shows.
|
|
211
|
+
*/
|
|
212
|
+
export function useStartPipelineResolver() {
|
|
213
|
+
const pipelines = usePipelinesStore()
|
|
214
|
+
const uiMode = useUiModeStore()
|
|
215
|
+
const { t } = useI18n()
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The pipeline a plain "Start" will use: the task's pinned pipeline, else the build rung this
|
|
219
|
+
* INTERFACE MODE defaults to (`defaultBuildPipelineId`: the fixed Standard build in basic mode,
|
|
220
|
+
* the Adaptive one in advanced). The workspace's positional first pipeline remains the last
|
|
221
|
+
* resort, for a board whose catalog does not carry the rung (an older seed, or a deployment that
|
|
222
|
+
* retired it).
|
|
223
|
+
*
|
|
224
|
+
* A PIN is honoured even when the library holds no row for it, and that branch is the whole reason
|
|
225
|
+
* this returns a descriptor rather than a `Pipeline`. An INTERNAL pipeline is withheld from the
|
|
226
|
+
* library on purpose (the platform starts it on its own behalf, so no picker may offer it), and a
|
|
227
|
+
* task can legitimately be pinned to one: the docs-refresh preset spawns its tasks onto
|
|
228
|
+
* `pl_code_comments`. Resolving that pin through the library alone answers undefined, and the
|
|
229
|
+
* fallback below then starts a FULL BUILD on a comment-only task while the button still reads as
|
|
230
|
+
* an ordinary Start. The fallback chain exists for a task with NO pin; a pin the library cannot
|
|
231
|
+
* show is still the task's answer, and the backend resolves the id for the run.
|
|
232
|
+
*/
|
|
233
|
+
return (task: Pick<Block, 'pipelineId'> | undefined): StartPipeline | undefined => {
|
|
234
|
+
const pinnedId = task?.pipelineId
|
|
235
|
+
if (pinnedId) {
|
|
236
|
+
return (
|
|
237
|
+
pipelines.getPipeline(pinnedId) ?? {
|
|
238
|
+
id: pinnedId,
|
|
239
|
+
// The catalog NAME map spans the whole catalog (unlike the versions map), so an internal
|
|
240
|
+
// pin still names itself here; the generic label covers a pin to something this build's
|
|
241
|
+
// catalog does not know at all.
|
|
242
|
+
name: pipelines.catalogNames[pinnedId] ?? t('board.task.pipelineFallback'),
|
|
243
|
+
}
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
// No pin: the workspace's own DECLARED in-app default outranks the interface-mode rung, because
|
|
247
|
+
// an operator who named one said something a tier cannot overrule.
|
|
248
|
+
const declared = pipelines.declaredDefaultId('interactive')
|
|
249
|
+
return (
|
|
250
|
+
pipelines.getPipeline(declared ?? defaultBuildPipelineId(uiMode.isAdvanced)) ??
|
|
251
|
+
pipelines.pipelines[0]
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Block } from '~/types/domain'
|
|
2
|
+
import { runActivityAt, runWaitingSince, type LaneTaskEntry } from '~/utils/laneSort'
|
|
3
|
+
import { classifyTask, type TaskLane } from '~/utils/swimlanes'
|
|
4
|
+
|
|
5
|
+
/** A lane entry plus the lane it was classified into. */
|
|
6
|
+
export interface ClassifiedEntry {
|
|
7
|
+
readonly entry: LaneTaskEntry
|
|
8
|
+
readonly lane: TaskLane
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The store-facing half of `classifyTask`: resolve one task's lane, reason and rendered entry.
|
|
13
|
+
*
|
|
14
|
+
* Shared by the two surfaces that lay tasks out in lanes, a service frame's swimlanes
|
|
15
|
+
* (`useFrameLanes`) and the workspace-wide queue (`useWorkspaceQueueStore`), so both state the SAME
|
|
16
|
+
* claim about a task. A card in "Needs you" on the board and in "In flight" in the queue would
|
|
17
|
+
* make the reader arbitrate between two answers to one question.
|
|
18
|
+
*
|
|
19
|
+
* Every cross-block lookup here is a Map read off an index a STORE maintains, never a reduction
|
|
20
|
+
* of its own: `useFrameLanes` runs one instance per mounted frame, so deriving a workspace-wide
|
|
21
|
+
* fact here makes a board with n frames pay for it n times on every change. The review-debt map
|
|
22
|
+
* (`notifications.reviewDebtByBlock`) is the worked example: a reduction over the whole
|
|
23
|
+
* workspace's open notifications, and it belongs on the store that owns its input.
|
|
24
|
+
*/
|
|
25
|
+
export function useTaskLaneClassifier() {
|
|
26
|
+
const board = useBoardStore()
|
|
27
|
+
const execution = useExecutionStore()
|
|
28
|
+
const agentRuns = useAgentRunsStore()
|
|
29
|
+
const notifications = useNotificationsStore()
|
|
30
|
+
const reviews = useReviewStage()
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The module a task belongs to: the module BLOCK's title when it already lives in one, else
|
|
34
|
+
* the module it DECLARES. The engine only materialises the block on merge
|
|
35
|
+
* (`applyModuleAssignment`), so keying on the parent alone would leave every unmerged task in
|
|
36
|
+
* "no module" while its own card names one.
|
|
37
|
+
*/
|
|
38
|
+
function moduleNameOf(task: Block): string | null {
|
|
39
|
+
const parent = task.parentId ? board.getBlock(task.parentId) : undefined
|
|
40
|
+
if (parent?.level === 'module') return parent.title
|
|
41
|
+
return task.moduleName?.trim() || null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One task's lane, reason and rendered entry. `order` is the caller's stable tiebreak. */
|
|
45
|
+
function classify(task: Block, order: number): ClassifiedEntry {
|
|
46
|
+
const run = execution.getByBlock(task.id) ?? null
|
|
47
|
+
const decisions = execution.decisionsByBlock.get(task.id) ?? []
|
|
48
|
+
const allApprovals = execution.approvalsByBlock.get(task.id) ?? []
|
|
49
|
+
// The same suppression the card and the frame badge apply: an iterative reviewer mid-cycle
|
|
50
|
+
// holds a pending approval while the driver folds answers in, and nobody is waiting on it.
|
|
51
|
+
const humanApprovals = allApprovals.filter((a) => !reviews.isBackground(a.agentKind, a.blockId))
|
|
52
|
+
|
|
53
|
+
const { lane, reason } = classifyTask({
|
|
54
|
+
status: task.status,
|
|
55
|
+
// Read from the coarse per-block summary, which also covers a bootstrap run.
|
|
56
|
+
runFailed: agentRuns.byBlock[task.id]?.status === 'failed',
|
|
57
|
+
run,
|
|
58
|
+
// A park is background exactly when everything asking was suppressed AND nothing else
|
|
59
|
+
// asks. With no approvals at all it is NOT background: it is a park on a surface this
|
|
60
|
+
// layer cannot name, which `classifyTask` reports as `parked` rather than as work.
|
|
61
|
+
parkIsBackground:
|
|
62
|
+
decisions.length === 0 &&
|
|
63
|
+
humanApprovals.length === 0 &&
|
|
64
|
+
allApprovals.length > humanApprovals.length,
|
|
65
|
+
pendingDecision: decisions.length > 0,
|
|
66
|
+
pendingApproval: humanApprovals.length > 0,
|
|
67
|
+
hasUnmetDeps: board.unmetDeps(task.id).length > 0,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
lane,
|
|
72
|
+
entry: {
|
|
73
|
+
task,
|
|
74
|
+
reason,
|
|
75
|
+
order,
|
|
76
|
+
activityAt: runActivityAt(run),
|
|
77
|
+
waitingSince: runWaitingSince(run, notifications.reviewDebtByBlock.get(task.id) ?? null),
|
|
78
|
+
moduleName: moduleNameOf(task),
|
|
79
|
+
initiativeName: task.initiativeId
|
|
80
|
+
? (board.getBlock(task.initiativeId)?.title ?? null)
|
|
81
|
+
: null,
|
|
82
|
+
epicName: board.epicOf(task)?.title ?? null,
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { classify }
|
|
88
|
+
}
|
|
@@ -224,6 +224,8 @@ describe('navSlotFilter', () => {
|
|
|
224
224
|
tutorial: 'the walkthroughs - the surface with the fewest destinations needs them most',
|
|
225
225
|
'keyboard-shortcuts': 'the cheatsheet covers the board and the palette, which every role has',
|
|
226
226
|
'ui-role': 'the way BACK out of the narrowed role',
|
|
227
|
+
'queue-view': 'which view of the board a person works in is their own choice, in every role',
|
|
228
|
+
'board-view': 'the way back to the canvas from the queue view, in every role',
|
|
227
229
|
}
|
|
228
230
|
const intake = NAV_CONTRIBUTIONS.filter((i) => i.intake).map((i) => i.id)
|
|
229
231
|
expect(intake.sort()).toEqual(Object.keys(REASON).sort())
|
|
@@ -522,6 +524,8 @@ describe('nav grouping helpers', () => {
|
|
|
522
524
|
'ui-role',
|
|
523
525
|
'tutorial',
|
|
524
526
|
'foundational-services',
|
|
527
|
+
'queue-view',
|
|
528
|
+
'board-view',
|
|
525
529
|
])
|
|
526
530
|
})
|
|
527
531
|
|
|
@@ -220,6 +220,8 @@ export const NAV_ACTIONS = [
|
|
|
220
220
|
'tutorial',
|
|
221
221
|
'toggleUiMode',
|
|
222
222
|
'chooseRole',
|
|
223
|
+
'queueView',
|
|
224
|
+
'boardView',
|
|
223
225
|
] as const
|
|
224
226
|
|
|
225
227
|
export type NavActionId = (typeof NAV_ACTIONS)[number]
|
|
@@ -737,6 +739,35 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
|
|
|
737
739
|
keywordsKey: 'layout.commandBar.keywords.chooseRole',
|
|
738
740
|
},
|
|
739
741
|
},
|
|
742
|
+
{
|
|
743
|
+
// The board's two views (issue #2258). The sidebar's Views switcher is the everyday route;
|
|
744
|
+
// these make each view reachable by name from the palette too. Every tier and role: which view
|
|
745
|
+
// a person works in is their own choice, and neither view is an override of the other.
|
|
746
|
+
id: 'queue-view',
|
|
747
|
+
labelKey: 'layout.commandBar.cmd.queueView',
|
|
748
|
+
icon: 'i-lucide-list-checks',
|
|
749
|
+
surfaces: S('command'),
|
|
750
|
+
intake: true,
|
|
751
|
+
action: 'queueView',
|
|
752
|
+
command: {
|
|
753
|
+
group: 'workspace',
|
|
754
|
+
order: 130,
|
|
755
|
+
keywordsKey: 'layout.commandBar.keywords.queueView',
|
|
756
|
+
},
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
id: 'board-view',
|
|
760
|
+
labelKey: 'layout.commandBar.cmd.boardView',
|
|
761
|
+
icon: 'i-lucide-map',
|
|
762
|
+
surfaces: S('command'),
|
|
763
|
+
intake: true,
|
|
764
|
+
action: 'boardView',
|
|
765
|
+
command: {
|
|
766
|
+
group: 'workspace',
|
|
767
|
+
order: 131,
|
|
768
|
+
keywordsKey: 'layout.commandBar.keywords.boardView',
|
|
769
|
+
},
|
|
770
|
+
},
|
|
740
771
|
]
|
|
741
772
|
|
|
742
773
|
/**
|
|
@@ -194,6 +194,16 @@ export const TUTORIAL_TOURS: readonly TutorialTour[] = [
|
|
|
194
194
|
titleKey: 'tutorial.tours.boardBasics.steps.canvas.title',
|
|
195
195
|
bodyKey: 'tutorial.tours.boardBasics.steps.canvas.body',
|
|
196
196
|
},
|
|
197
|
+
{
|
|
198
|
+
// A new person lands on the QUEUE view, and every tour moves them to the canvas, so the
|
|
199
|
+
// orientation tour is where they learn the queue exists. The switcher renders in the rail
|
|
200
|
+
// and the expanded sidebar, in every tier and role, so the step needs no `when`.
|
|
201
|
+
id: 'views',
|
|
202
|
+
target: 'work-view-switcher',
|
|
203
|
+
placement: 'right',
|
|
204
|
+
titleKey: 'tutorial.tours.boardBasics.steps.views.title',
|
|
205
|
+
bodyKey: 'tutorial.tours.boardBasics.steps.views.body',
|
|
206
|
+
},
|
|
197
207
|
{
|
|
198
208
|
id: 'sidebar',
|
|
199
209
|
target: 'sidebar',
|
package/app/pages/index.vue
CHANGED
|
@@ -17,6 +17,7 @@ import GitHubOnboarding from '~/components/github/GitHubOnboarding.vue'
|
|
|
17
17
|
import CommandBar from '~/components/layout/CommandBar.vue'
|
|
18
18
|
import PersonalCredentialModal from '~/components/providers/PersonalCredentialModal.vue'
|
|
19
19
|
import ConfirmDialog from '~/components/common/ConfirmDialog.vue'
|
|
20
|
+
import Spinner from '~/components/common/Spinner.vue'
|
|
20
21
|
import { defineAsyncView } from '~/utils/asyncView'
|
|
21
22
|
import KeyboardShortcutsHelp from '~/components/common/KeyboardShortcutsHelp.vue'
|
|
22
23
|
|
|
@@ -158,12 +159,15 @@ const TutorialNudge = defineAsyncView(() => import('~/components/tutorial/Tutori
|
|
|
158
159
|
// The first-run role question (Engineer / Product manager / Designer). Same shape as the tutorial
|
|
159
160
|
// launch prompt: mounted only while its store flag is set, so an answered question costs nothing.
|
|
160
161
|
const RolePrompt = defineAsyncView(() => import('~/components/layout/RolePrompt.vue'))
|
|
162
|
+
// The queue: the board's second view (issue #2258), loaded the first time someone opens it.
|
|
163
|
+
const QueueView = defineAsyncView(() => import('~/components/queue/QueueView.vue'))
|
|
161
164
|
|
|
162
165
|
const workspace = useWorkspaceStore()
|
|
163
166
|
const github = useGitHubStore()
|
|
164
167
|
const models = useModelsStore()
|
|
165
168
|
const ui = useUiStore()
|
|
166
169
|
const aiReadiness = useAiReadiness()
|
|
170
|
+
const views = useWorkViewStore()
|
|
167
171
|
|
|
168
172
|
// App-wide keyboard shortcuts (Escape to deselect, Delete to remove the selected block, ?
|
|
169
173
|
// for the cheatsheet). Registered ONCE here so a single global listener owns them.
|
|
@@ -175,6 +179,8 @@ onMounted(() => {
|
|
|
175
179
|
// board it names is the one that opens. This is what makes the observability link on an
|
|
176
180
|
// engine-published PR verification report resolve.
|
|
177
181
|
useRunDeepLink()
|
|
182
|
+
// `?view=queue` / `?view=board`: a link that opens one of the board's two views.
|
|
183
|
+
views.consumeDeepLink()
|
|
178
184
|
void workspace.init()
|
|
179
185
|
// Honour a `cat-factory k3s` CLI hand-off (`?infraSetup=local-k3s&…`): open the Infrastructure
|
|
180
186
|
// window pre-seeded with the provisioned connection so the user only pastes the token + saves.
|
|
@@ -400,6 +406,14 @@ useTutorialNudge()
|
|
|
400
406
|
// plus the funnel counters. Unconditional and best-effort: with no accounts or no store wired it
|
|
401
407
|
// simply has nothing to talk to, and the browser-persisted store carries on alone.
|
|
402
408
|
useTutorialSync()
|
|
409
|
+
// Starting a tour switches to the canvas, which every tour walks (the view store holds it for the
|
|
410
|
+
// tour's length), and the canvas is then the view used last.
|
|
411
|
+
watch(
|
|
412
|
+
() => tutorial.touring,
|
|
413
|
+
(touring) => {
|
|
414
|
+
if (touring) views.show('board')
|
|
415
|
+
},
|
|
416
|
+
)
|
|
403
417
|
|
|
404
418
|
// Subscribe to the backend's real-time event stream and (re)connect whenever the
|
|
405
419
|
// active workspace changes. Runs advance durably server-side; progress arrives as
|
|
@@ -438,7 +452,7 @@ watch(
|
|
|
438
452
|
v-if="workspace.ready && githubProbePending"
|
|
439
453
|
class="m-auto flex flex-col items-center gap-3 text-muted"
|
|
440
454
|
>
|
|
441
|
-
<
|
|
455
|
+
<Spinner class="h-8 w-8" />
|
|
442
456
|
<span class="text-sm">{{ $t('app.loading') }}</span>
|
|
443
457
|
</div>
|
|
444
458
|
|
|
@@ -461,12 +475,32 @@ watch(
|
|
|
461
475
|
<main class="relative min-w-0 flex-1">
|
|
462
476
|
<BoardCanvas />
|
|
463
477
|
<!-- Toolbar, nav trigger and every advisory banner, in ONE stacked region that owns
|
|
464
|
-
their placement, so no two of them can cover each other.
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
478
|
+
their placement, so no two of them can cover each other.
|
|
479
|
+
|
|
480
|
+
The queue view sits OVER the canvas rather than replacing it, so switching back keeps
|
|
481
|
+
the canvas's camera and selection. Below the inspector (z-20), so a task opened from
|
|
482
|
+
the queue shows its panel beside it. The same advisories render IN FLOW above its
|
|
483
|
+
columns, so they push the queue down instead of covering its header.
|
|
484
|
+
|
|
485
|
+
ONE overlay instance serves both views, at the same place in this template: two
|
|
486
|
+
instances would remount every banner on each switch, and a banner whose dismissal
|
|
487
|
+
is its own component state would come back. On the canvas the wrapper is
|
|
488
|
+
`display: contents`, so it adds no box and the overlays float over the canvas as
|
|
489
|
+
before. -->
|
|
490
|
+
<div
|
|
491
|
+
:class="
|
|
492
|
+
views.view === 'queue' ? 'absolute inset-0 z-10 flex flex-col bg-app-950' : 'contents'
|
|
493
|
+
"
|
|
494
|
+
:data-testid="views.view === 'queue' ? 'queue-view-host' : undefined"
|
|
495
|
+
>
|
|
496
|
+
<BoardTopOverlays
|
|
497
|
+
:in-flow="views.view === 'queue'"
|
|
498
|
+
:connected="streamConnected"
|
|
499
|
+
:ever-connected="streamEverConnected"
|
|
500
|
+
:connection-failed="streamConnectionFailed"
|
|
501
|
+
/>
|
|
502
|
+
<QueueView v-if="views.view === 'queue'" class="min-h-0 flex-1" />
|
|
503
|
+
</div>
|
|
470
504
|
<InspectorPanel />
|
|
471
505
|
<!-- Code-split focus view. The fade lives here (not inside the component) so the
|
|
472
506
|
leave animation still plays when `focusBlockId` clears and the v-if unmounts
|
|
@@ -567,7 +601,7 @@ watch(
|
|
|
567
601
|
|
|
568
602
|
<!-- Initial load -->
|
|
569
603
|
<div v-else class="m-auto flex flex-col items-center gap-3 text-muted">
|
|
570
|
-
<
|
|
604
|
+
<Spinner class="h-8 w-8" />
|
|
571
605
|
<span class="text-sm">{{ $t('app.loadingBoard') }}</span>
|
|
572
606
|
</div>
|
|
573
607
|
</div>
|