@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,143 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
2
|
+
import { dirname, join, relative } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { describe, expect, it } from 'vitest'
|
|
5
|
+
import { hasI18nKey } from '../../test/i18nKeys'
|
|
6
|
+
|
|
7
|
+
// The two toast funnels take a title KEY, resolved inside the composable. Typed message keys and
|
|
8
|
+
// `vue-i18n-extract` only see a key written literally inside a `t()` call, so a key handed to a funnel is
|
|
9
|
+
// invisible to both: a typo renders its own key path in the toast, and deleting a live key reads
|
|
10
|
+
// as a clean removal (`i18n:check` even lists these keys as unused). This spec closes that gap by
|
|
11
|
+
// reading every literal key passed to a funnel and resolving it against the base catalog.
|
|
12
|
+
|
|
13
|
+
const appRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
14
|
+
|
|
15
|
+
function* sources(dir: string): Generator<string> {
|
|
16
|
+
for (const entry of readdirSync(dir)) {
|
|
17
|
+
const abs = join(dir, entry)
|
|
18
|
+
if (statSync(abs).isDirectory()) yield* sources(abs)
|
|
19
|
+
else if (/\.(vue|ts)$/.test(entry) && !entry.endsWith('.spec.ts')) yield abs
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The call heads whose FIRST argument is the title key, and those whose SECOND argument is: the
|
|
24
|
+
// error funnel's two (after the error or the reported detail) and `useInterviewDrafts`' `flushThen`,
|
|
25
|
+
// which forwards its key to `present`.
|
|
26
|
+
const FIRST = /actionToast\.(?:success|info|warning|error)\(\s*/g
|
|
27
|
+
const SECOND = /\b(?:present|presentReported|flushThen)\(/g
|
|
28
|
+
// Options that carry a key to a funnel through a helper. Only a LITERAL value is read: the same
|
|
29
|
+
// names also appear in type declarations and in the helpers that forward them.
|
|
30
|
+
const OPTION = /\b(?:titleKey|failureTitleKeys)\s*:\s*/g
|
|
31
|
+
// A dotted key literal. A conditional argument (`ok ? 'a.b' : 'a.c'`) yields both.
|
|
32
|
+
const KEY = /'([a-zA-Z][\w-]*(?:\.[\w-]+)+)'/g
|
|
33
|
+
// A tone passed around instead of called (`const show = ok ? actionToast.success : …`): every key
|
|
34
|
+
// then reaches the toast through the alias, where no call head above can read it.
|
|
35
|
+
const ALIAS = /actionToast\.(?:success|info|warning|error)\b(?!\s*\()/g
|
|
36
|
+
|
|
37
|
+
// Files whose funnel call takes its key from somewhere other than a literal argument. A prefix
|
|
38
|
+
// means the file spells its keys out in a table or in calls to a local helper, and every literal
|
|
39
|
+
// under that prefix in the file is resolved instead. `null` means the file only forwards a key
|
|
40
|
+
// its callers name, and those callers are read through `SECOND` and `OPTION`. A new indirect call
|
|
41
|
+
// fails until it is either made literal or listed here.
|
|
42
|
+
const INDIRECT: Record<string, string | null> = {
|
|
43
|
+
'composables/useConfirmAction.ts': 'common.toast.',
|
|
44
|
+
'components/github/GitHubPanel.vue': 'vcs.panel.pulls.',
|
|
45
|
+
'components/pipeline/PipelineHealthModal.vue': 'pipeline.health.toast.',
|
|
46
|
+
'components/settings/RiskPolicyPanel.vue': 'settings.riskPolicy.toast.',
|
|
47
|
+
'composables/useInterviewDrafts.ts': null,
|
|
48
|
+
'composables/useContextLinking.ts': null,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The funnels themselves: they DEFINE the calls above (a signature reads like a call head), so
|
|
52
|
+
// they hold no key to check.
|
|
53
|
+
const FUNNELS =
|
|
54
|
+
/^composables\/(?:useActionToast|usePipelineErrorToast)\.ts$|^composables\/pipelineErrorToast\//
|
|
55
|
+
|
|
56
|
+
/** The argument starting at `from`: up to the first top-level `,` or the closing `)`. */
|
|
57
|
+
function argumentAt(text: string, from: number): string {
|
|
58
|
+
let depth = 0
|
|
59
|
+
for (let i = from; i < text.length; i++) {
|
|
60
|
+
const char = text[i]
|
|
61
|
+
if (char === '(' || char === '[' || char === '{') depth++
|
|
62
|
+
else if (char === ')' || char === ']' || char === '}') {
|
|
63
|
+
if (depth === 0) return text.slice(from, i)
|
|
64
|
+
depth--
|
|
65
|
+
} else if (char === ',' && depth === 0) return text.slice(from, i)
|
|
66
|
+
}
|
|
67
|
+
return text.slice(from)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Every key argument in `text`, each with the literal keys it contains (none for an indirect one). */
|
|
71
|
+
function keyArguments(text: string): { arg: string; keys: string[] }[] {
|
|
72
|
+
const args: string[] = []
|
|
73
|
+
for (const m of text.matchAll(FIRST)) args.push(argumentAt(text, m.index + m[0].length))
|
|
74
|
+
for (const m of text.matchAll(SECOND)) {
|
|
75
|
+
const first = argumentAt(text, m.index + m[0].length)
|
|
76
|
+
const comma = m.index + m[0].length + first.length
|
|
77
|
+
// `present(error)` with no key takes the funnel's own default title.
|
|
78
|
+
if (text[comma] === ',') args.push(argumentAt(text, comma + 1).trim())
|
|
79
|
+
}
|
|
80
|
+
const keyed = args.map((arg) => ({
|
|
81
|
+
arg: arg.trim(),
|
|
82
|
+
keys: [...arg.matchAll(KEY)].map(([, k]) => k!),
|
|
83
|
+
}))
|
|
84
|
+
for (const m of text.matchAll(OPTION)) {
|
|
85
|
+
const keys = [...argumentAt(text, m.index + m[0].length).matchAll(KEY)].map(([, k]) => k!)
|
|
86
|
+
if (keys.length) keyed.push({ arg: m[0], keys })
|
|
87
|
+
}
|
|
88
|
+
return keyed
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
describe('toast title keys', () => {
|
|
92
|
+
it('reads the key argument of every call shape', () => {
|
|
93
|
+
const keys = (src: string) => keyArguments(src).flatMap((a) => a.keys)
|
|
94
|
+
expect(keys(`actionToast.success('a.saved', { params: { n } })`)).toEqual(['a.saved'])
|
|
95
|
+
expect(keys(`present(error, 'b.failed')`)).toEqual(['b.failed'])
|
|
96
|
+
expect(keys(`presentReported(job.error, 'b.reported', { descriptionKey: 'x' })`)).toEqual([
|
|
97
|
+
'b.reported',
|
|
98
|
+
])
|
|
99
|
+
expect(keys(`actionToast.info(ok ? 'c.yes' : 'c.no')`)).toEqual(['c.yes', 'c.no'])
|
|
100
|
+
expect(keys(`presentLinkFailures(f, id, { titleKey: 'd.linkFailed' })`)).toEqual([
|
|
101
|
+
'd.linkFailed',
|
|
102
|
+
])
|
|
103
|
+
expect(keys(`flushThen(submit, 'e.submitFailed')`)).toEqual(['e.submitFailed'])
|
|
104
|
+
expect(keys(`failureTitleKeys: { one: 'f.one', many: 'f.many' }`)).toEqual(['f.one', 'f.many'])
|
|
105
|
+
// A declaration or a forward names no key, and is not a call to check.
|
|
106
|
+
expect(keyArguments(`titleKey: string`)).toEqual([])
|
|
107
|
+
expect(keyArguments(`present(e)`)).toEqual([])
|
|
108
|
+
expect(keyArguments(`actionToast.success(meta.toastKey)`)).toEqual([
|
|
109
|
+
{ arg: 'meta.toastKey', keys: [] },
|
|
110
|
+
])
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('resolves every key a funnel is handed against the base catalog', () => {
|
|
114
|
+
const missing: string[] = []
|
|
115
|
+
const unreadable: string[] = []
|
|
116
|
+
let seen = 0
|
|
117
|
+
for (const file of sources(appRoot)) {
|
|
118
|
+
const rel = relative(appRoot, file).replaceAll('\\', '/')
|
|
119
|
+
if (FUNNELS.test(rel)) continue
|
|
120
|
+
const text = readFileSync(file, 'utf8')
|
|
121
|
+
for (const alias of text.matchAll(ALIAS)) unreadable.push(`${rel}: ${alias[0]} passed around`)
|
|
122
|
+
for (const { arg, keys } of keyArguments(text)) {
|
|
123
|
+
if (!keys.length && !(rel in INDIRECT)) unreadable.push(`${rel}: ${arg}`)
|
|
124
|
+
for (const key of keys) {
|
|
125
|
+
seen++
|
|
126
|
+
if (!hasI18nKey(key)) missing.push(`${rel}: ${key}`)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const prefix = INDIRECT[rel]
|
|
130
|
+
if (prefix !== undefined && prefix !== null) {
|
|
131
|
+
for (const [, key] of text.matchAll(KEY)) {
|
|
132
|
+
if (!key!.startsWith(prefix)) continue
|
|
133
|
+
seen++
|
|
134
|
+
if (!hasI18nKey(key!)) missing.push(`${rel}: ${key}`)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// A scan that matched nothing would pass vacuously, so it must have read some keys.
|
|
139
|
+
expect(seen).toBeGreaterThan(0)
|
|
140
|
+
expect(unreadable).toEqual([])
|
|
141
|
+
expect(missing).toEqual([])
|
|
142
|
+
})
|
|
143
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { useActionToast } from '~/composables/useActionToast'
|
|
3
|
+
|
|
4
|
+
// `test/setup.ts` stubs `useNuxtApp().$i18n.t` as a key echo; this spec swaps in a `t` that also
|
|
5
|
+
// records its params, and a `useToast` whose `add` it can read back.
|
|
6
|
+
const add = vi.fn()
|
|
7
|
+
const t = vi.fn((key: string, params?: Record<string, unknown>, plural?: number) =>
|
|
8
|
+
[key, params && Object.keys(params).length ? JSON.stringify(params) : '', plural ?? '']
|
|
9
|
+
.filter((part) => part !== '')
|
|
10
|
+
.join('|'),
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
add.mockReset()
|
|
15
|
+
t.mockClear()
|
|
16
|
+
vi.stubGlobal('useToast', () => ({ add }))
|
|
17
|
+
vi.stubGlobal('useNuxtApp', () => ({ $i18n: { t } }))
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const lastToast = () => add.mock.calls.at(-1)?.[0]
|
|
21
|
+
|
|
22
|
+
describe('useActionToast', () => {
|
|
23
|
+
it('renders every success toast the same, whatever the call site', () => {
|
|
24
|
+
const toast = useActionToast()
|
|
25
|
+
toast.success('a.saved')
|
|
26
|
+
const first = lastToast()
|
|
27
|
+
toast.success('b.connected', { params: { name: 'x' } })
|
|
28
|
+
const second = lastToast()
|
|
29
|
+
for (const key of ['color', 'icon', 'duration'] as const) expect(second[key]).toBe(first[key])
|
|
30
|
+
expect(first.color).toBe('success')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('auto-dismisses success and info, and keeps warning and error until closed', () => {
|
|
34
|
+
const toast = useActionToast()
|
|
35
|
+
toast.success('k')
|
|
36
|
+
expect(lastToast().duration).toBeUndefined()
|
|
37
|
+
toast.info('k')
|
|
38
|
+
expect(lastToast().duration).toBeUndefined()
|
|
39
|
+
toast.warning('k')
|
|
40
|
+
expect(lastToast().duration).toBe(0)
|
|
41
|
+
toast.error('k')
|
|
42
|
+
expect(lastToast().duration).toBe(0)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('resolves the title key with its params and plural count', () => {
|
|
46
|
+
useActionToast().info('x.found', { params: { count: 3 }, plural: 3, description: 'body' })
|
|
47
|
+
expect(lastToast().title).toBe('x.found|{"count":3}|3')
|
|
48
|
+
expect(lastToast().description).toBe('body')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('holds an undo toast exactly as long as the window and leads with the Undo action', () => {
|
|
52
|
+
const run = vi.fn()
|
|
53
|
+
const extra = { label: 'Open', onClick: vi.fn() }
|
|
54
|
+
useActionToast().success('board.toast.deleted', {
|
|
55
|
+
undo: { run, windowMs: 6000 },
|
|
56
|
+
actions: [extra],
|
|
57
|
+
})
|
|
58
|
+
const shown = lastToast()
|
|
59
|
+
expect(shown.duration).toBe(6000)
|
|
60
|
+
expect(shown.actions).toHaveLength(2)
|
|
61
|
+
expect(shown.actions[0].label).toBe('common.undo')
|
|
62
|
+
shown.actions[0].onClick()
|
|
63
|
+
expect(run).toHaveBeenCalledOnce()
|
|
64
|
+
expect(shown.actions[1]).toBe(extra)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('sends no empty actions array', () => {
|
|
68
|
+
useActionToast().success('k')
|
|
69
|
+
expect(lastToast()).not.toHaveProperty('actions')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The toast for everything that is NOT a failed call: the second of the SPA's two toast funnels.
|
|
3
|
+
* A failed call goes to `usePipelineErrorToast().present(error, titleKey)`; every other toast comes
|
|
4
|
+
* from here, so `toast.add` is called from exactly those two composables
|
|
5
|
+
* (`scripts/check-frontend-feedback.mjs` enforces it).
|
|
6
|
+
*
|
|
7
|
+
* Each tone fixes its colour, its icon and whether it auto-dismisses, so two toasts of one tone
|
|
8
|
+
* render identically wherever they come from. The call site picks a TONE and supplies COPY; it
|
|
9
|
+
* cannot restyle the toast. Before this, some success toasts carried `color: 'success'` and some
|
|
10
|
+
* none, and the icon was a choice made per site (`i-lucide-check`, `-link`, `-unplug`, `-copy`,
|
|
11
|
+
* `-rocket`, or nothing).
|
|
12
|
+
*
|
|
13
|
+
* - `success`: the action the user asked for completed. Dismisses after Nuxt UI's default.
|
|
14
|
+
* - `info`: progress or a state report that asks nothing of the reader ("drafting…", "up to
|
|
15
|
+
* date"). Dismisses after the default.
|
|
16
|
+
* - `warning`: the action ran, but the outcome is partial or nothing changed in a way the reader
|
|
17
|
+
* should know about. Stays until closed, because it asks the reader to look at something.
|
|
18
|
+
* - `error`: a refusal in TRANSLATED copy that is not a failed call: a client-side validation that
|
|
19
|
+
* never left the browser, or a recognised refusal reason the site maps to its own key. It has no
|
|
20
|
+
* envelope, so `present` would misread it as a network fault (README, "Every failure toast goes
|
|
21
|
+
* through ONE funnel"). Stays until closed, like every failure. A failure the backend reported
|
|
22
|
+
* as DATA with its own prose (a job row's `error`) is `presentReported` on the failure funnel
|
|
23
|
+
* instead, which keeps that prose as copyable detail rather than the headline.
|
|
24
|
+
*
|
|
25
|
+
* Titles are i18n KEYS, resolved here (the same contract as `present`), so a call site cannot hand
|
|
26
|
+
* in untranslated prose as the headline. A `description` is already-resolved text, because it
|
|
27
|
+
* often carries data (a repo name, a count line) built from several keys. A key handed in here is
|
|
28
|
+
* invisible to typed message keys and to `vue-i18n-extract`, which only read a key written literally inside a `t()` call, so
|
|
29
|
+
* `toastTitleKeys.spec.ts` resolves every literal key passed to either funnel against the catalog.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** One button on the toast. A call-to-action is content, so the call site may add one. */
|
|
33
|
+
export interface ActionToastAction {
|
|
34
|
+
label: string
|
|
35
|
+
icon?: string
|
|
36
|
+
onClick: () => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ActionToastOptions {
|
|
40
|
+
/** Interpolation for the title key. */
|
|
41
|
+
params?: Record<string, unknown>
|
|
42
|
+
/** The plural count for the title key, when it has plural forms. */
|
|
43
|
+
plural?: number
|
|
44
|
+
/** Resolved body text. */
|
|
45
|
+
description?: string
|
|
46
|
+
actions?: ActionToastAction[]
|
|
47
|
+
/**
|
|
48
|
+
* Offer an Undo for a write whose real effect is deferred. The toast then stays exactly as long
|
|
49
|
+
* as the window the caller holds the write open for, because an Undo button that outlives the
|
|
50
|
+
* window would undo nothing.
|
|
51
|
+
*/
|
|
52
|
+
undo?: { run: () => void; windowMs: number }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type Tone = 'success' | 'info' | 'warning' | 'error'
|
|
56
|
+
|
|
57
|
+
// The only place a tone's look is decided. `duration: 0` keeps the toast until the reader closes
|
|
58
|
+
// it; `undefined` takes Nuxt UI's default (5s).
|
|
59
|
+
const TONES: Record<Tone, { color: Tone; icon: string; duration: number | undefined }> = {
|
|
60
|
+
success: { color: 'success', icon: 'i-lucide-circle-check', duration: undefined },
|
|
61
|
+
info: { color: 'info', icon: 'i-lucide-info', duration: undefined },
|
|
62
|
+
warning: { color: 'warning', icon: 'i-lucide-triangle-alert', duration: 0 },
|
|
63
|
+
error: { color: 'error', icon: 'i-lucide-triangle-alert', duration: 0 },
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function useActionToast() {
|
|
67
|
+
const toast = useToast()
|
|
68
|
+
// The Nuxt app's global i18n instance, not `useI18n()`: this is reached from store setup (the
|
|
69
|
+
// board store's undo toasts) and from `useCopyToClipboard`, which the error funnel calls from
|
|
70
|
+
// store setup. Same reason as `usePipelineErrorToast`.
|
|
71
|
+
const { t } = useNuxtApp().$i18n as ReturnType<typeof useI18n>
|
|
72
|
+
|
|
73
|
+
function show(tone: Tone, titleKey: string, opts: ActionToastOptions = {}): void {
|
|
74
|
+
const look = TONES[tone]
|
|
75
|
+
const params = opts.params ?? {}
|
|
76
|
+
const title = opts.plural === undefined ? t(titleKey, params) : t(titleKey, params, opts.plural)
|
|
77
|
+
const actions = [
|
|
78
|
+
...(opts.undo
|
|
79
|
+
? [{ label: t('common.undo'), icon: 'i-lucide-undo-2', onClick: opts.undo.run }]
|
|
80
|
+
: []),
|
|
81
|
+
...(opts.actions ?? []),
|
|
82
|
+
]
|
|
83
|
+
toast.add({
|
|
84
|
+
title,
|
|
85
|
+
description: opts.description,
|
|
86
|
+
color: look.color,
|
|
87
|
+
icon: look.icon,
|
|
88
|
+
duration: opts.undo ? opts.undo.windowMs : look.duration,
|
|
89
|
+
...(actions.length ? { actions } : {}),
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
success: (titleKey: string, opts?: ActionToastOptions) => show('success', titleKey, opts),
|
|
95
|
+
info: (titleKey: string, opts?: ActionToastOptions) => show('info', titleKey, opts),
|
|
96
|
+
warning: (titleKey: string, opts?: ActionToastOptions) => show('warning', titleKey, opts),
|
|
97
|
+
error: (titleKey: string, opts?: ActionToastOptions) => show('error', titleKey, opts),
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type ActionToast = ReturnType<typeof useActionToast>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Block } from '~/types/domain'
|
|
2
|
+
import { useActionToast } from '~/composables/useActionToast'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* The single, confirm-gated block deletion used by BOTH the inspector's Delete button and
|
|
@@ -15,7 +16,7 @@ export function useBlockDeletion() {
|
|
|
15
16
|
const execution = useExecutionStore()
|
|
16
17
|
const ui = useUiStore()
|
|
17
18
|
const recurring = useRecurringPipelinesStore()
|
|
18
|
-
const
|
|
19
|
+
const actionToast = useActionToast()
|
|
19
20
|
const { present } = usePipelineErrorToast()
|
|
20
21
|
const { confirm } = useConfirm()
|
|
21
22
|
const { t } = useI18n()
|
|
@@ -52,11 +53,7 @@ export function useBlockDeletion() {
|
|
|
52
53
|
ui.select(null)
|
|
53
54
|
try {
|
|
54
55
|
await board.archiveService(block.id)
|
|
55
|
-
toast.
|
|
56
|
-
title: t('board.toast.archived', { name: block.title }),
|
|
57
|
-
icon: 'i-lucide-archive',
|
|
58
|
-
color: 'neutral',
|
|
59
|
-
})
|
|
56
|
+
actionToast.success('board.toast.archived', { params: { name: block.title } })
|
|
60
57
|
} catch (e) {
|
|
61
58
|
present(e, 'board.toast.archiveFailed')
|
|
62
59
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Confirm-gate + success-toast for the recurring destructive actions that aren't board
|
|
3
3
|
* blocks (disconnect a connection, remove/revoke a credential, clear a config, destroy an
|
|
4
|
-
* environment). Built on the same `useConfirm()` singleton + `
|
|
4
|
+
* environment). Built on the same `useConfirm()` singleton + `useActionToast()` the board delete
|
|
5
5
|
* path uses, so every destructive affordance across the settings/connection surfaces routes
|
|
6
6
|
* through ONE confirm-then-mutate + toast path rather than each re-inventing its own copy.
|
|
7
7
|
*
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* per surface. `name` is a short noun for the target (a brand like "Slack", a data value like
|
|
20
20
|
* the invite email, or a feature noun like "the test environment").
|
|
21
21
|
*/
|
|
22
|
+
import { useActionToast } from '~/composables/useActionToast'
|
|
23
|
+
|
|
22
24
|
type ConfirmShape = 'disconnect' | 'remove' | 'revoke' | 'clear' | 'destroy'
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -71,7 +73,7 @@ const SHAPE_META: Record<
|
|
|
71
73
|
|
|
72
74
|
export function useConfirmAction() {
|
|
73
75
|
const { confirm } = useConfirm()
|
|
74
|
-
const
|
|
76
|
+
const actionToast = useActionToast()
|
|
75
77
|
const { t } = useI18n()
|
|
76
78
|
|
|
77
79
|
/** Prompt before a destructive action against `name`. Resolves `true` only if confirmed. */
|
|
@@ -89,7 +91,7 @@ export function useConfirmAction() {
|
|
|
89
91
|
/** Toast the completed destructive action (call only on real success). */
|
|
90
92
|
function toastDone(shape: ConfirmShape, name: string): void {
|
|
91
93
|
const meta = SHAPE_META[shape]
|
|
92
|
-
|
|
94
|
+
actionToast.success(meta.toastKey, { params: { name } })
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
return { confirmAction, toastDone }
|
|
@@ -105,6 +105,7 @@ describe('resolvePending', () => {
|
|
|
105
105
|
vi.stubGlobal('useWorkspaceStore', () => ({ workspaceId: 'ws_1' }))
|
|
106
106
|
vi.stubGlobal('useToast', () => ({ add: vi.fn() }))
|
|
107
107
|
vi.stubGlobal('useI18n', () => ({ t: (key: string) => key }))
|
|
108
|
+
vi.stubGlobal('useNuxtApp', () => ({ $i18n: { t: (key: string) => key } }))
|
|
108
109
|
vi.stubGlobal('useCopyToClipboard', () => ({ copyAction: () => ({ label: 'copy' }) }))
|
|
109
110
|
}
|
|
110
111
|
|
|
@@ -192,6 +193,7 @@ describe('presentLinkFailures', () => {
|
|
|
192
193
|
vi.stubGlobal('useWorkspaceStore', () => ({ workspaceId: 'ws_1' }))
|
|
193
194
|
vi.stubGlobal('useToast', () => ({ add }))
|
|
194
195
|
vi.stubGlobal('useI18n', () => ({ t: (key: string) => key }))
|
|
196
|
+
vi.stubGlobal('useNuxtApp', () => ({ $i18n: { t: (key: string) => key } }))
|
|
195
197
|
vi.stubGlobal('useCopyToClipboard', () => ({ copyAction }))
|
|
196
198
|
return { add, copyAction }
|
|
197
199
|
}
|
|
@@ -221,6 +223,7 @@ describe('presentLinkFailures', () => {
|
|
|
221
223
|
const toast = add.mock.calls[0]![0]
|
|
222
224
|
// Sticky so the cause stays readable, titled by the count key, and per-item reason shown.
|
|
223
225
|
expect(toast.title).toBe('board.addTask.linkFailed')
|
|
226
|
+
expect(toast.color).toBe('warning')
|
|
224
227
|
expect(toast.duration).toBe(0)
|
|
225
228
|
expect(toast.description).toContain('GitHub denied access')
|
|
226
229
|
expect(toast.actions).toHaveLength(1)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DocumentSourceKind, TaskSourceKind } from '~/types/domain'
|
|
2
2
|
import { apiErrorEnvelope, apiErrorStatus } from './api/errors'
|
|
3
|
+
import { useActionToast } from '~/composables/useActionToast'
|
|
3
4
|
|
|
4
5
|
// Shared model + orchestration for attaching external context (imported docs and
|
|
5
6
|
// tracker issues) to a board block. A "pending" item is something the user has
|
|
@@ -121,7 +122,7 @@ export function useContextLinking() {
|
|
|
121
122
|
const documents = useDocumentsStore()
|
|
122
123
|
const tasks = useTasksStore()
|
|
123
124
|
const workspace = useWorkspaceStore()
|
|
124
|
-
const
|
|
125
|
+
const actionToast = useActionToast()
|
|
125
126
|
const { t } = useI18n()
|
|
126
127
|
const { copyAction } = useCopyToClipboard()
|
|
127
128
|
|
|
@@ -240,14 +241,12 @@ export function useContextLinking() {
|
|
|
240
241
|
/**
|
|
241
242
|
* Surface link failures as a single actionable toast: the specific per-item
|
|
242
243
|
* reasons as the body, and a "Copy details" action that puts the full diagnostic
|
|
243
|
-
* report ({@link buildLinkFailureReport}) on the clipboard.
|
|
244
|
-
*
|
|
244
|
+
* report ({@link buildLinkFailureReport}) on the clipboard. A warning, so it stays until
|
|
245
|
+
* closed and the cause stays readable long enough to act on. No-op when nothing failed.
|
|
245
246
|
*
|
|
246
|
-
* `opts.
|
|
247
|
-
* "Initiative created, but …"). It is a
|
|
248
|
-
*
|
|
249
|
-
* would make it a variable, which defeats both the typed-message-key check and the extractor's
|
|
250
|
-
* static scan — and the plural choice has to be made against the same count.
|
|
247
|
+
* `opts.titleKey` names what WAS created, which differs per host ("Task added, but …" vs
|
|
248
|
+
* "Initiative created, but …"). It is a plural key, resolved against the failure count;
|
|
249
|
+
* `toastTitleKeys.spec.ts` checks the literal each caller passes, which typed keys cannot see.
|
|
251
250
|
*/
|
|
252
251
|
/**
|
|
253
252
|
* A failure's line in the toast: TRANSLATED copy where the backend named a reason we have a
|
|
@@ -269,7 +268,7 @@ export function useContextLinking() {
|
|
|
269
268
|
function presentLinkFailures(
|
|
270
269
|
failures: LinkFailure[],
|
|
271
270
|
blockId?: string,
|
|
272
|
-
opts: {
|
|
271
|
+
opts: { titleKey?: string } = {},
|
|
273
272
|
): void {
|
|
274
273
|
if (failures.length === 0) return
|
|
275
274
|
const description = failures.map((f) => `${f.item.title}: ${describeFailure(f)}`).join('\n')
|
|
@@ -278,14 +277,10 @@ export function useContextLinking() {
|
|
|
278
277
|
blockId,
|
|
279
278
|
when: new Date().toISOString(),
|
|
280
279
|
})
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
t('board.addTask.linkFailed', { count: failures.length }, failures.length),
|
|
280
|
+
actionToast.warning(opts.titleKey ?? 'board.addTask.linkFailed', {
|
|
281
|
+
params: { count: failures.length },
|
|
282
|
+
plural: failures.length,
|
|
285
283
|
description,
|
|
286
|
-
icon: 'i-lucide-triangle-alert',
|
|
287
|
-
color: 'warning',
|
|
288
|
-
duration: 0,
|
|
289
284
|
actions: [copyAction(report)],
|
|
290
285
|
})
|
|
291
286
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// silent no-op the user couldn't tell apart from success. Routing every copy through this seam
|
|
8
8
|
// makes the outcome always visible.
|
|
9
9
|
import { useClipboard } from '@vueuse/core'
|
|
10
|
+
import { useActionToast } from '~/composables/useActionToast'
|
|
10
11
|
|
|
11
12
|
export function useCopyToClipboard() {
|
|
12
13
|
// Resolved through the Nuxt app's global i18n instance rather than `useI18n()`, which requires an
|
|
@@ -16,7 +17,7 @@ export function useCopyToClipboard() {
|
|
|
16
17
|
// component. `useI18n()` there throws `MUST_BE_CALL_SETUP_TOP`, and a throw in a store body takes
|
|
17
18
|
// the whole app to Nuxt's error page. Same reason, and the same fix, as `usePipelineErrorToast`.
|
|
18
19
|
const { t } = useNuxtApp().$i18n as ReturnType<typeof useI18n>
|
|
19
|
-
const
|
|
20
|
+
const actionToast = useActionToast()
|
|
20
21
|
const { copy: writeClipboard, isSupported } = useClipboard()
|
|
21
22
|
|
|
22
23
|
async function copy(text: string) {
|
|
@@ -25,9 +26,9 @@ export function useCopyToClipboard() {
|
|
|
25
26
|
try {
|
|
26
27
|
if (!isSupported.value) throw new Error('clipboard unsupported')
|
|
27
28
|
await writeClipboard(text)
|
|
28
|
-
|
|
29
|
+
actionToast.success('common.copied')
|
|
29
30
|
} catch {
|
|
30
|
-
|
|
31
|
+
actionToast.error('common.copyFailed')
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -1,28 +1,18 @@
|
|
|
1
1
|
import { computed, type Ref } from 'vue'
|
|
2
|
-
import type { Block } from '~/types/domain'
|
|
3
2
|
import { createLaneMemo } from '~/utils/laneIdentity'
|
|
4
3
|
import {
|
|
5
4
|
groupLaneTasks,
|
|
6
|
-
runActivityAt,
|
|
7
|
-
runWaitingSince,
|
|
8
5
|
sortLaneTasks,
|
|
9
6
|
type LaneTaskEntry,
|
|
10
7
|
type RenderedLane,
|
|
11
8
|
} from '~/utils/laneSort'
|
|
12
9
|
import {
|
|
13
|
-
classifyTask,
|
|
14
10
|
selectDoneLaneTasks,
|
|
15
11
|
TASK_LANES,
|
|
16
12
|
type DoneLaneSelection,
|
|
17
13
|
type TaskLane,
|
|
18
14
|
} from '~/utils/swimlanes'
|
|
19
15
|
|
|
20
|
-
/** A lane entry plus the lane it was classified into. */
|
|
21
|
-
interface ClassifiedEntry {
|
|
22
|
-
readonly entry: LaneTaskEntry
|
|
23
|
-
readonly lane: TaskLane
|
|
24
|
-
}
|
|
25
|
-
|
|
26
16
|
/**
|
|
27
17
|
* Assemble a service frame's tasks into swimlanes.
|
|
28
18
|
*
|
|
@@ -34,12 +24,10 @@ interface ClassifiedEntry {
|
|
|
34
24
|
*/
|
|
35
25
|
export function useFrameLanes(frameId: Ref<string>) {
|
|
36
26
|
const board = useBoardStore()
|
|
37
|
-
const execution = useExecutionStore()
|
|
38
|
-
const agentRuns = useAgentRunsStore()
|
|
39
|
-
const notifications = useNotificationsStore()
|
|
40
27
|
const settings = useWorkspaceSettingsStore()
|
|
41
28
|
const laneView = useLaneViewStore()
|
|
42
|
-
|
|
29
|
+
// The workspace-wide classification: every frame and the queue read one verdict per task.
|
|
30
|
+
const queue = useWorkspaceQueueStore()
|
|
43
31
|
|
|
44
32
|
/** Tasks directly in the frame plus those inside its modules: a module renders no box now. */
|
|
45
33
|
const tasks = computed(() => board.allTasksUnder(frameId.value))
|
|
@@ -49,78 +37,14 @@ export function useFrameLanes(frameId: Ref<string>) {
|
|
|
49
37
|
() => new Map(board.modulesOf(frameId.value).map((m) => [m.title, m.id])),
|
|
50
38
|
)
|
|
51
39
|
|
|
52
|
-
/**
|
|
53
|
-
* The module a task belongs to: the module BLOCK's title when it already lives in one, else
|
|
54
|
-
* the module it DECLARES. The engine only materialises the block on merge
|
|
55
|
-
* (`applyModuleAssignment`), so keying on the parent alone would leave every unmerged task in
|
|
56
|
-
* "no module" while its own card names one.
|
|
57
|
-
*/
|
|
58
|
-
function moduleNameOf(task: Block): string | null {
|
|
59
|
-
const parent = task.parentId ? board.getBlock(task.parentId) : undefined
|
|
60
|
-
if (parent?.level === 'module') return parent.title
|
|
61
|
-
return task.moduleName?.trim() || null
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* One task's lane, reason and rendered entry.
|
|
66
|
-
*
|
|
67
|
-
* Every cross-block lookup here is a Map read off an index a STORE maintains, never a reduction
|
|
68
|
-
* of its own: this composable runs ONE INSTANCE PER MOUNTED FRAME, so deriving a workspace-wide
|
|
69
|
-
* fact here makes a board with n frames pay for it n times on every change. The review-debt map
|
|
70
|
-
* (`notifications.reviewDebtByBlock`) is the worked example: a reduction over the whole
|
|
71
|
-
* workspace's open notifications, and it belongs on the store that owns its input. The rule for
|
|
72
|
-
* anything else this assembly needs: a per-FRAME derivation belongs here, a workspace-wide one
|
|
73
|
-
* belongs on that store.
|
|
74
|
-
*/
|
|
75
|
-
function classify(task: Block, order: number): ClassifiedEntry {
|
|
76
|
-
const run = execution.getByBlock(task.id) ?? null
|
|
77
|
-
const decisions = execution.decisionsByBlock.get(task.id) ?? []
|
|
78
|
-
const allApprovals = execution.approvalsByBlock.get(task.id) ?? []
|
|
79
|
-
// The same suppression the card and the frame badge apply: an iterative reviewer mid-cycle
|
|
80
|
-
// holds a pending approval while the driver folds answers in, and nobody is waiting on it.
|
|
81
|
-
const humanApprovals = allApprovals.filter((a) => !reviews.isBackground(a.agentKind, a.blockId))
|
|
82
|
-
|
|
83
|
-
const { lane, reason } = classifyTask({
|
|
84
|
-
status: task.status,
|
|
85
|
-
// Read from the coarse per-block summary, which also covers a bootstrap run.
|
|
86
|
-
runFailed: agentRuns.byBlock[task.id]?.status === 'failed',
|
|
87
|
-
run,
|
|
88
|
-
// A park is background exactly when everything asking was suppressed AND nothing else
|
|
89
|
-
// asks. With no approvals at all it is NOT background: it is a park on a surface this
|
|
90
|
-
// layer cannot name, which `classifyTask` reports as `parked` rather than as work.
|
|
91
|
-
parkIsBackground:
|
|
92
|
-
decisions.length === 0 &&
|
|
93
|
-
humanApprovals.length === 0 &&
|
|
94
|
-
allApprovals.length > humanApprovals.length,
|
|
95
|
-
pendingDecision: decisions.length > 0,
|
|
96
|
-
pendingApproval: humanApprovals.length > 0,
|
|
97
|
-
hasUnmetDeps: board.unmetDeps(task.id).length > 0,
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
lane,
|
|
102
|
-
entry: {
|
|
103
|
-
task,
|
|
104
|
-
reason,
|
|
105
|
-
order,
|
|
106
|
-
activityAt: runActivityAt(run),
|
|
107
|
-
waitingSince: runWaitingSince(run, notifications.reviewDebtByBlock.get(task.id) ?? null),
|
|
108
|
-
moduleName: moduleNameOf(task),
|
|
109
|
-
initiativeName: task.initiativeId
|
|
110
|
-
? (board.getBlock(task.initiativeId)?.title ?? null)
|
|
111
|
-
: null,
|
|
112
|
-
epicName: board.epicOf(task)?.title ?? null,
|
|
113
|
-
},
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
40
|
/** Every task bucketed by lane, in board order, before sorting. */
|
|
118
41
|
const byLane = computed(() => {
|
|
119
42
|
const buckets = new Map<TaskLane, LaneTaskEntry[]>(TASK_LANES.map((lane) => [lane, []]))
|
|
120
|
-
tasks.value
|
|
121
|
-
|
|
43
|
+
for (const task of tasks.value) {
|
|
44
|
+
// The store walks every frame's tasks, this one's included, so each has a verdict.
|
|
45
|
+
const { lane, entry } = queue.classified.get(task.id)!
|
|
122
46
|
buckets.get(lane)!.push(entry)
|
|
123
|
-
}
|
|
47
|
+
}
|
|
124
48
|
return buckets
|
|
125
49
|
})
|
|
126
50
|
|
|
@@ -4,6 +4,7 @@ import { groupCommands, groupSidebar, sortToolbar } from '~/modular/nav-contribu
|
|
|
4
4
|
import { EXTERNAL_TOOL_UNAVAILABLE_KEYS, projectExternalTools } from '~/modular/external-tools'
|
|
5
5
|
import { toMetadataBag } from '~/modular/workspace-metadata'
|
|
6
6
|
import type { ExternalToolContext, ExternalToolContribution } from '~/modular/external-tools'
|
|
7
|
+
import { useActionToast } from '~/composables/useActionToast'
|
|
7
8
|
import type {
|
|
8
9
|
AppSlots,
|
|
9
10
|
CommandGroup,
|
|
@@ -30,7 +31,7 @@ export function useNavContributions() {
|
|
|
30
31
|
// an active component instance) — the same handle, and the same reason, as
|
|
31
32
|
// `usePipelineErrorToast`: nothing about this composable should depend on WHERE it is called.
|
|
32
33
|
const { t } = useNuxtApp().$i18n as ReturnType<typeof useI18n>
|
|
33
|
-
const
|
|
34
|
+
const actionToast = useActionToast()
|
|
34
35
|
const auth = useAuthStore()
|
|
35
36
|
const workspace = useWorkspaceStore()
|
|
36
37
|
const workspaceSettings = useWorkspaceSettingsStore()
|
|
@@ -71,6 +72,8 @@ export function useNavContributions() {
|
|
|
71
72
|
// The QUESTION, not a toggle: with three roles there is no unambiguous "next one", and the
|
|
72
73
|
// prompt is the one surface that states what each role gives you before you pick it.
|
|
73
74
|
chooseRole: () => useUiRoleStore().openPrompt(),
|
|
75
|
+
queueView: () => useWorkViewStore().show('queue'),
|
|
76
|
+
boardView: () => useWorkViewStore().show('board'),
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
/** Run a contribution's action (consumer `run` closure wins over the id map). */
|
|
@@ -148,13 +151,11 @@ export function useNavContributions() {
|
|
|
148
151
|
resolution.cause,
|
|
149
152
|
)
|
|
150
153
|
}
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
actionToast.error('externalTools.unavailable.title', {
|
|
155
|
+
params: { tool: tool.title },
|
|
153
156
|
description: t(EXTERNAL_TOOL_UNAVAILABLE_KEYS[resolution.reason], {
|
|
154
157
|
fields: resolution.missing.join(', '),
|
|
155
158
|
}),
|
|
156
|
-
icon: 'i-lucide-triangle-alert',
|
|
157
|
-
color: 'warning',
|
|
158
159
|
})
|
|
159
160
|
},
|
|
160
161
|
},
|