@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
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ The SPA source lives under `app/` (the Nuxt srcDir).
|
|
|
17
17
|
- [Layout](#layout)
|
|
18
18
|
- [Nuxt UI: agent tooling and overrides](#nuxt-ui-agent-tooling-and-where-the-spa-overrides-the-skill)
|
|
19
19
|
- [Task swimlanes](#task-swimlanes)
|
|
20
|
+
- [Queue view](#queue-view)
|
|
20
21
|
- [Roles (engineer / product manager / designer)](#roles-engineer--product-manager--designer)
|
|
21
22
|
- [Interface modes (basic / advanced)](#interface-modes-basic--advanced)
|
|
22
23
|
- [Agent tiers (basic / intermediate / advanced)](#agent-tiers-basic--intermediate--advanced)
|
|
@@ -257,14 +258,72 @@ a recognised refusal keeps that branch and drains only its fallback into the fun
|
|
|
257
258
|
answered, so a local check that never left the browser must not be dressed up as one: a synthesized
|
|
258
259
|
`new Error(t('...'))` has no envelope and no status, which is precisely the input `describeGenericFailure`
|
|
259
260
|
reads as a network fault. A blank required field then renders as "The server could not be reached",
|
|
260
|
-
with the real sentence hidden behind a disclosure. Client-side validation
|
|
261
|
-
`
|
|
261
|
+
with the real sentence hidden behind a disclosure. Client-side validation is
|
|
262
|
+
`useActionToast().error(titleKey, { description })` instead (see below). A failure the backend
|
|
263
|
+
reports as DATA (a job row with `status: 'failed'` and its `error`, a probe verdict's `message`) is
|
|
264
|
+
`presentReported(detail, titleKey, { descriptionKey })`: translated copy as the headline, the
|
|
265
|
+
backend's prose behind "Show details" and in the copied report, the same as a failed call.
|
|
262
266
|
|
|
263
267
|
The still-open remainder is the INLINE family: `error.value = e.message` rendered in a panel, and
|
|
264
268
|
`testResult = { ok: false, message }` rendered by `ConnectionTestVerdict`. Those need a render
|
|
265
269
|
surface rather than a toast, and are tracked as G4 in
|
|
266
270
|
[`error-message-coverage.md`](https://github.com/kibertoad/cat-factory/blob/main/docs/initiatives/error-message-coverage.md).
|
|
267
271
|
|
|
272
|
+
#### Every other toast goes through the second funnel, `useActionToast`
|
|
273
|
+
|
|
274
|
+
**`toast.add` is called from exactly two composables**: `usePipelineErrorToast` (with its
|
|
275
|
+
`pipelineErrorToast/` factory) for a failed call, and `useActionToast` for everything else. A call
|
|
276
|
+
site picks a TONE and supplies a title KEY; the tone fixes the colour, the icon and the duration, so
|
|
277
|
+
two toasts of one tone render identically wherever they come from. Before this, some success toasts
|
|
278
|
+
were green with a check and the next was grey with no icon, depending on who wrote the site.
|
|
279
|
+
|
|
280
|
+
| Tone | For | Dismisses |
|
|
281
|
+
| --------- | -------------------------------------------------------------------------------------------------------- | ----------- |
|
|
282
|
+
| `success` | The action the user asked for completed (saved, connected, removed, copied, moved). | After 5s |
|
|
283
|
+
| `info` | Progress or a state report that asks nothing ("drafting", "up to date"). | After 5s |
|
|
284
|
+
| `warning` | The action ran, but the outcome is partial or needs a look. | When closed |
|
|
285
|
+
| `error` | A refusal in translated copy that is NOT a failed call: client-side validation, a mapped refusal reason. | When closed |
|
|
286
|
+
|
|
287
|
+
`warning` and `error` stay for the same reason the failure funnel's toast does: they ask the reader
|
|
288
|
+
to do something. Options are content, never styling: `params` and `plural` for the title key, a
|
|
289
|
+
resolved `description`, `actions` for a call-to-action, and `undo: { run, windowMs }` for a write
|
|
290
|
+
held open behind an Undo (the board's delete and move), which keeps the toast exactly as long as
|
|
291
|
+
the window. A toast is action feedback. Something the reader must act on later belongs in an inline
|
|
292
|
+
`UAlert` or the notification inbox, not a toast.
|
|
293
|
+
|
|
294
|
+
#### Loading: the component's `loading` prop, `USkeleton` for content, `Spinner` for a glyph
|
|
295
|
+
|
|
296
|
+
- **A Nuxt UI component with a `loading` prop takes `:loading`.** Never spin its icon by hand
|
|
297
|
+
(`:ui="{ leadingIcon: 'animate-spin' }"`, or an `:icon` swapped to a loader while busy). One
|
|
298
|
+
exception: `UButton`'s `loading` also DISABLES it (`disabled || isLoading`). A button that must
|
|
299
|
+
stay clickable while its work runs (a tab whose thread is mid-reply, a "planning in progress"
|
|
300
|
+
button that opens the planning window) puts `<Spinner>` in its `#leading` slot instead, with a
|
|
301
|
+
comment saying why. `UInput` and the other inputs do not disable on `loading`.
|
|
302
|
+
- **A panel or list waiting on its first fetch shows `USkeleton` rows sized like the content they
|
|
303
|
+
replace**, so it does not show nothing and then everything. Its fill is `bg-accented`, set once
|
|
304
|
+
in `app.config.ts`: Nuxt UI's default skeleton is `bg-elevated`, which disappears on the
|
|
305
|
+
`bg-elevated` and `app-950` window bodies and cards many panels paint. An ACTION in progress (a button
|
|
306
|
+
press, a sync) is not a content load and keeps `:loading` or a spinner.
|
|
307
|
+
- **A bare glyph showing work in progress is `<Spinner>`** (`components/common/Spinner.vue`), the
|
|
308
|
+
one file that writes `animate-spin`. `:spinning` covers a status icon that spins in one state
|
|
309
|
+
only (a running step), and `name` defaults to Nuxt UI's own loading icon so it matches the one
|
|
310
|
+
`:loading` draws. It spins under `motion-safe:` only.
|
|
311
|
+
|
|
312
|
+
#### Empty states: `EmptyState`, always
|
|
313
|
+
|
|
314
|
+
**A list, panel or section with nothing to show renders `common/EmptyState.vue`**, with its copy as
|
|
315
|
+
`title` / `description` and any button that fills the list in its default slot. `compact` is the
|
|
316
|
+
inline size for an inspector section or a picker. It wraps Nuxt UI's `UEmpty` (the structure, the
|
|
317
|
+
action row and the tokens) and decides once what each `UEmpty` caller would otherwise pick: the
|
|
318
|
+
`naked` variant, because an empty state sits inside a panel that already draws its border; the
|
|
319
|
+
spacing, because `UEmpty` pads `p-4 sm:p-6 lg:p-8` for a page body; and plain text for the title,
|
|
320
|
+
because `UEmpty` renders it as an `<h2>`, which would put a heading into every picker. An inline
|
|
321
|
+
value placeholder ("None", a dash in a table cell) is not an empty state.
|
|
322
|
+
|
|
323
|
+
`scripts/check-frontend-feedback.mjs` (CI's `repo-guards` job) bans `useToast(` and `toast.add(`
|
|
324
|
+
outside the two funnels and `animate-spin` outside `Spinner.vue`. Its `PENDING` list names files another open
|
|
325
|
+
change is rewriting, and fails once one of them is clean so the entry cannot outlive the reason.
|
|
326
|
+
|
|
268
327
|
### A panel that seeds state on open uses `onModalOpen`, never a bare `watch(open)`
|
|
269
328
|
|
|
270
329
|
**Work done on the way IN to a modal goes through `onModalOpen(open, fn)`
|
|
@@ -412,9 +471,12 @@ protects, so the two never contradict silently:
|
|
|
412
471
|
skill teaches the hand-rolled `UForm` + `UFormField` shape for every form; when the backend
|
|
413
472
|
declares the fields and the SPA only collects them, that shape is forbidden here.
|
|
414
473
|
[Rule](#a-backend-declared-form-renders-through-descriptorfieldsvue).
|
|
415
|
-
- **
|
|
416
|
-
freely; here a failed backend call is reported with `usePipelineErrorToast().present(error, key)
|
|
474
|
+
- **Toasts go through the two funnels, never a bare `toast.add`.** The skill uses `useToast()`
|
|
475
|
+
freely; here a failed backend call is reported with `usePipelineErrorToast().present(error, key)`
|
|
476
|
+
and every other toast with `useActionToast().success|info|warning|error(key, opts)`.
|
|
417
477
|
[Rule](#every-failure-toast-goes-through-one-funnel).
|
|
478
|
+
- **Loading is `:loading`, `USkeleton` or `<Spinner>`; empty is `<EmptyState>`.** Never a
|
|
479
|
+
hand-written `animate-spin`. [Rule](#loading-the-components-loading-prop-uskeleton-for-content-spinner-for-a-glyph).
|
|
418
480
|
- **Icon-only buttons carry an accessible name.** Use `common/IconButton.vue`
|
|
419
481
|
([source](./app/components/common/IconButton.vue)), which applies `label` as both the tooltip
|
|
420
482
|
and `aria-label`, rather than a bare `<UButton icon>`.
|
|
@@ -563,6 +625,32 @@ does not visibly jump when the response lands. "No module" is the EMPTY STRING o
|
|
|
563
625
|
every other clearable field spells a clear; `undefined` is dropped by `JSON.stringify` and reaches
|
|
564
626
|
the server as an empty patch.
|
|
565
627
|
|
|
628
|
+
## Queue view
|
|
629
|
+
|
|
630
|
+
A board has two views (issue [#2258](https://github.com/kibertoad/cat-factory/issues/2258)): the
|
|
631
|
+
**queue**, which lists every task across every service in the column that says what it needs, and
|
|
632
|
+
the **board**, the canvas where the work is planned. They are two views of one board, not two
|
|
633
|
+
products. The sidebar's Views switcher (`components/layout/WorkViewSwitcher.vue`) moves between
|
|
634
|
+
them, and so do the palette entries and a `?view=queue` / `?view=board` link. The initiative it
|
|
635
|
+
starts, with the slices after it: [`delivery-surface.md`](https://github.com/kibertoad/cat-factory/blob/main/docs/initiatives/delivery-surface.md).
|
|
636
|
+
|
|
637
|
+
- **Which view opens is the person's own choice**: the view they used last in this browser
|
|
638
|
+
(`stores/workView.ts`, a cookie persisted like the interface tier, so two accounts on one browser
|
|
639
|
+
share it). A person who has not chosen
|
|
640
|
+
lands on the queue, because it answers the first question a person opens the app with. Both
|
|
641
|
+
views exist in every tier and every role: neither is an override of the other.
|
|
642
|
+
- **The queue states the canvas's verdict, regrouped.** `useTaskLaneClassifier` is the one
|
|
643
|
+
classifier the frame swimlanes and the queue share; `utils/queueSections.ts` only regroups its
|
|
644
|
+
output (a waiting pull request gets its own column), and no task crosses a lane. The
|
|
645
|
+
workspace-wide walk lives on one store (`stores/workspaceQueue.ts`), per the swimlane rule on
|
|
646
|
+
workspace-wide derivations.
|
|
647
|
+
- **A card's actions are the board card's** (`useTaskActions`), so Start, Resolve and Merge
|
|
648
|
+
cannot drift between the two views.
|
|
649
|
+
- **The queue sits over the canvas**, which stays mounted, so switching back keeps its camera and
|
|
650
|
+
selection. The advisory banners render in the page flow above the queue's columns
|
|
651
|
+
(`BoardTopOverlays` with `in-flow`), and the canvas toolbar does not, being the canvas's own
|
|
652
|
+
chrome. The canvas's decision counter opens the queue view.
|
|
653
|
+
|
|
566
654
|
## Roles (engineer / product manager / designer)
|
|
567
655
|
|
|
568
656
|
The outermost of the three narrowing axes, and the only one the app **asks about**: on a first-ever
|
package/app/app.config.ts
CHANGED
|
@@ -23,6 +23,13 @@ export default defineAppConfig({
|
|
|
23
23
|
badge: {
|
|
24
24
|
defaultVariants: { color: 'primary', variant: 'solid', size: 'md' },
|
|
25
25
|
},
|
|
26
|
+
// A skeleton is one step darker than Nuxt UI's `bg-elevated`, which is also the default
|
|
27
|
+
// skeleton fill. Many windows paint their body or inner cards `bg-elevated` or `app-950`
|
|
28
|
+
// (neutral-100 in light mode, the same colour), and a default skeleton there is invisible.
|
|
29
|
+
// `bg-accented` reads on those, on `bg-default`, and in dark mode.
|
|
30
|
+
skeleton: {
|
|
31
|
+
base: 'animate-pulse rounded-md bg-accented',
|
|
32
|
+
},
|
|
26
33
|
// The toaster's scroll region is the ONE `[data-slot='viewport']` the app's own
|
|
27
34
|
// CSS may target: Nuxt UI reuses that attribute for the item list of every menu
|
|
28
35
|
// component too. This marker class is what `main.css` hangs the safe-area offset
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import type { AssistantActionId, AssistantOutcome } from '~/types/domain'
|
|
24
24
|
import { answerFor, assistantSurface, revealTarget, submitGate } from './AssistantModal.logic'
|
|
25
25
|
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
26
|
+
import Spinner from '~/components/common/Spinner.vue'
|
|
26
27
|
|
|
27
28
|
const { t } = useI18n()
|
|
28
29
|
const ui = useUiStore()
|
|
@@ -243,11 +244,7 @@ function reveal(blockId: string): void {
|
|
|
243
244
|
:class="submitReason ? 'text-app-warning-400' : 'text-dimmed'"
|
|
244
245
|
data-testid="assistant-submit-status"
|
|
245
246
|
>
|
|
246
|
-
<
|
|
247
|
-
v-if="gate.state === 'checking'"
|
|
248
|
-
name="i-lucide-loader-circle"
|
|
249
|
-
class="h-3 w-3 shrink-0 animate-spin"
|
|
250
|
-
/>
|
|
247
|
+
<Spinner v-if="gate.state === 'checking'" class="h-3 w-3 shrink-0" />
|
|
251
248
|
{{ submitReason ?? t('assistant.submitHint') }}
|
|
252
249
|
</span>
|
|
253
250
|
</div>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { computed } from 'vue'
|
|
3
3
|
import BackendMisconfiguredScreen from '~/components/auth/BackendMisconfiguredScreen.vue'
|
|
4
4
|
import LoginScreen from '~/components/auth/LoginScreen.vue'
|
|
5
|
+
import Spinner from '~/components/common/Spinner.vue'
|
|
5
6
|
|
|
6
7
|
// Resolves auth state once on mount, then either renders the app (auth off, or
|
|
7
8
|
// on with a signed-in user) or the login screen. The board's own bootstrap runs
|
|
@@ -24,7 +25,7 @@ onMounted(() => void auth.bootstrap().finally(() => markBoot('auth-ready')))
|
|
|
24
25
|
v-if="!auth.ready"
|
|
25
26
|
class="flex h-screen w-screen flex-col items-center justify-center gap-3 bg-app-950 text-muted"
|
|
26
27
|
>
|
|
27
|
-
<
|
|
28
|
+
<Spinner class="h-8 w-8" />
|
|
28
29
|
<span class="text-sm">{{ t('auth.gate.loading') }}</span>
|
|
29
30
|
</div>
|
|
30
31
|
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from '~/utils/binaryCandidates'
|
|
29
29
|
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
30
30
|
import StoredAssetView from '~/components/binaryOutput/StoredAssetView.vue'
|
|
31
|
+
import EmptyState from '~/components/common/EmptyState.vue'
|
|
31
32
|
|
|
32
33
|
const execution = useExecutionStore()
|
|
33
34
|
const board = useBoardStore()
|
|
@@ -399,21 +400,20 @@ const { requestClose } = useUnsavedGuard({
|
|
|
399
400
|
Retry), or the step genuinely compared nothing. Collapsing any of the first three into the
|
|
400
401
|
last would put "nothing to compare" in front of a person whose candidates exist and were
|
|
401
402
|
simply not fetched. -->
|
|
402
|
-
<
|
|
403
|
+
<EmptyState
|
|
403
404
|
v-else-if="absence === 'no_run'"
|
|
404
|
-
class="
|
|
405
|
+
class="min-h-0 flex-1 px-5"
|
|
406
|
+
icon="i-lucide-unlink"
|
|
407
|
+
:title="t('binaryCandidates.noRun.title')"
|
|
408
|
+
:description="t('binaryCandidates.noRun.hint')"
|
|
405
409
|
data-testid="binary-candidates-no-run"
|
|
406
|
-
|
|
407
|
-
<UIcon name="i-lucide-unlink" class="h-8 w-8 opacity-40" />
|
|
408
|
-
<p class="text-sm">{{ t('binaryCandidates.noRun.title') }}</p>
|
|
409
|
-
<p class="max-w-md text-2xs text-dimmed">{{ t('binaryCandidates.noRun.hint') }}</p>
|
|
410
|
-
</div>
|
|
410
|
+
/>
|
|
411
411
|
<div
|
|
412
412
|
v-else-if="absence === 'loading'"
|
|
413
|
-
class="
|
|
413
|
+
class="grid min-h-0 flex-1 grid-cols-1 content-start gap-3 px-5 py-4 sm:grid-cols-2 lg:grid-cols-3"
|
|
414
414
|
data-testid="binary-candidates-loading"
|
|
415
415
|
>
|
|
416
|
-
<
|
|
416
|
+
<USkeleton v-for="n in 3" :key="n" class="h-48 w-full rounded-lg" />
|
|
417
417
|
</div>
|
|
418
418
|
<div
|
|
419
419
|
v-else-if="absence === 'load_failed'"
|
|
@@ -434,13 +434,12 @@ const { requestClose } = useUnsavedGuard({
|
|
|
434
434
|
{{ t('common.retry') }}
|
|
435
435
|
</UButton>
|
|
436
436
|
</div>
|
|
437
|
-
<
|
|
437
|
+
<EmptyState
|
|
438
438
|
v-else
|
|
439
|
-
class="
|
|
439
|
+
class="min-h-0 flex-1 px-5"
|
|
440
|
+
icon="i-lucide-images"
|
|
441
|
+
:title="t('binaryCandidates.empty.title')"
|
|
440
442
|
data-testid="binary-candidates-empty"
|
|
441
|
-
|
|
442
|
-
<UIcon name="i-lucide-images" class="h-8 w-8 opacity-40" />
|
|
443
|
-
<p class="text-sm">{{ t('binaryCandidates.empty.title') }}</p>
|
|
444
|
-
</div>
|
|
443
|
+
/>
|
|
445
444
|
</ResultWindowShell>
|
|
446
445
|
</template>
|
|
@@ -105,9 +105,7 @@ const filename = computed(() => {
|
|
|
105
105
|
{{ t('binaryOutput.asset.retry') }}
|
|
106
106
|
</UButton>
|
|
107
107
|
</p>
|
|
108
|
-
<
|
|
109
|
-
{{ t('binaryOutput.asset.loading') }}
|
|
110
|
-
</p>
|
|
108
|
+
<USkeleton v-else class="h-16 w-full rounded-sm" data-testid="stored-asset-loading" />
|
|
111
109
|
|
|
112
110
|
<!-- The two things a person does with a delivered asset. Both hang off the object URL, so
|
|
113
111
|
they appear only once the bytes are in hand. -->
|
|
@@ -65,7 +65,7 @@ const modelPresets = useModelPresetsStore()
|
|
|
65
65
|
const pipelines = usePipelinesStore()
|
|
66
66
|
const agentConfig = useAgentConfigStore()
|
|
67
67
|
const fragments = useFragmentsStore()
|
|
68
|
-
const
|
|
68
|
+
const actionToast = useActionToast()
|
|
69
69
|
const { present } = usePipelineErrorToast()
|
|
70
70
|
const { t } = useI18n()
|
|
71
71
|
|
|
@@ -789,9 +789,7 @@ async function submitCreate(acknowledgeReviewDebt: boolean) {
|
|
|
789
789
|
const { resolved, failures } = await resolvePending(pendingContext.value)
|
|
790
790
|
pendingContext.value = resolved
|
|
791
791
|
if (failures.length) {
|
|
792
|
-
presentLinkFailures(failures, undefined, {
|
|
793
|
-
title: (count) => t('board.addTask.contextFailed', { count }, count),
|
|
794
|
-
})
|
|
792
|
+
presentLinkFailures(failures, undefined, { titleKey: 'board.addTask.contextFailed' })
|
|
795
793
|
return
|
|
796
794
|
}
|
|
797
795
|
const typeFields = buildTypeFields()
|
|
@@ -839,12 +837,7 @@ async function submitCreate(acknowledgeReviewDebt: boolean) {
|
|
|
839
837
|
}
|
|
840
838
|
const refusal = createRefusalMessage(e)
|
|
841
839
|
if (refusal) {
|
|
842
|
-
|
|
843
|
-
title: t('board.addTask.addFailedTitle'),
|
|
844
|
-
description: refusal,
|
|
845
|
-
icon: 'i-lucide-triangle-alert',
|
|
846
|
-
color: 'error',
|
|
847
|
-
})
|
|
840
|
+
actionToast.error('board.addTask.addFailedTitle', { description: refusal })
|
|
848
841
|
} else present(e, 'board.addTask.addFailedTitle')
|
|
849
842
|
} finally {
|
|
850
843
|
saving.value = false
|
|
@@ -202,15 +202,14 @@ async function retry() {
|
|
|
202
202
|
variant="ghost"
|
|
203
203
|
class="nodrag flex items-center gap-1 rounded-md bg-app-error-900/40 text-app-error-200 hover:bg-app-error-900/70 disabled:opacity-60"
|
|
204
204
|
:class="compact ? 'px-2 py-0.5 text-3xs' : 'px-2 py-1 text-2xs'"
|
|
205
|
-
|
|
205
|
+
icon="i-lucide-rotate-ccw"
|
|
206
|
+
:loading="retrying"
|
|
207
|
+
:ui="{ leadingIcon: compact ? 'h-3 w-3' : 'h-3.5 w-3.5' }"
|
|
208
|
+
:disabled="!access.canExecuteRuns.value"
|
|
206
209
|
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
207
210
|
data-testid="agent-failure-retry"
|
|
208
211
|
@click.stop="retry"
|
|
209
212
|
>
|
|
210
|
-
<UIcon
|
|
211
|
-
:name="retrying ? 'i-lucide-loader-circle' : 'i-lucide-rotate-ccw'"
|
|
212
|
-
:class="[compact ? 'h-3 w-3' : 'h-3.5 w-3.5', { 'animate-spin': retrying }]"
|
|
213
|
-
/>
|
|
214
213
|
{{ retrying ? t('board.failure.retrying') : compact ? t('common.retry') : retryLabel }}
|
|
215
214
|
</UButton>
|
|
216
215
|
|
|
@@ -22,7 +22,7 @@ const props = withDefaults(
|
|
|
22
22
|
const { t } = useI18n()
|
|
23
23
|
const agentRuns = useAgentRunsStore()
|
|
24
24
|
const access = useWorkspaceAccess()
|
|
25
|
-
const
|
|
25
|
+
const actionToast = useActionToast()
|
|
26
26
|
const { present } = usePipelineErrorToast()
|
|
27
27
|
const { confirm } = useConfirm()
|
|
28
28
|
const stopping = ref(false)
|
|
@@ -44,12 +44,10 @@ async function stop() {
|
|
|
44
44
|
stopping.value = true
|
|
45
45
|
try {
|
|
46
46
|
const kind = await agentRuns.stop(props.runId)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
description: t('board.stop.stoppedDescription'),
|
|
50
|
-
|
|
51
|
-
color: 'warning',
|
|
52
|
-
})
|
|
47
|
+
actionToast.success(
|
|
48
|
+
kind === 'bootstrap' ? 'board.stop.bootstrapStopped' : 'board.stop.runStopped',
|
|
49
|
+
{ description: t('board.stop.stoppedDescription') },
|
|
50
|
+
)
|
|
53
51
|
} catch (e) {
|
|
54
52
|
present(e, 'board.stop.stopFailed')
|
|
55
53
|
} finally {
|
|
@@ -16,6 +16,7 @@ import { useFrameOverlapGuard } from '~/composables/useFrameOverlapGuard'
|
|
|
16
16
|
import { useViewport } from '~/composables/useViewport'
|
|
17
17
|
import { boardPanMode } from '~/utils/boardPanMode'
|
|
18
18
|
import { createBoardNodeProjection } from './BoardCanvas.logic'
|
|
19
|
+
import EmptyState from '~/components/common/EmptyState.vue'
|
|
19
20
|
|
|
20
21
|
const board = useBoardStore()
|
|
21
22
|
const ui = useUiStore()
|
|
@@ -162,39 +163,39 @@ function onPaneClick() {
|
|
|
162
163
|
the buttons themselves. -->
|
|
163
164
|
<div
|
|
164
165
|
v-if="board.frames.length === 0"
|
|
165
|
-
class="pointer-events-none absolute inset-0 flex
|
|
166
|
+
class="pointer-events-none absolute inset-0 flex items-center justify-center"
|
|
166
167
|
>
|
|
167
|
-
<
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
</p>
|
|
173
|
-
</div>
|
|
174
|
-
<div
|
|
175
|
-
v-if="
|
|
176
|
-
access.canManageIntegrations.value || (github.available && access.canWriteBoard.value)
|
|
177
|
-
"
|
|
178
|
-
class="pointer-events-auto flex flex-wrap items-center justify-center gap-2"
|
|
168
|
+
<EmptyState
|
|
169
|
+
icon="i-lucide-layout-dashboard"
|
|
170
|
+
:title="t('board.canvas.emptyTitle')"
|
|
171
|
+
:description="t('board.canvas.emptyBody')"
|
|
172
|
+
class="max-w-sm"
|
|
179
173
|
>
|
|
180
|
-
<
|
|
181
|
-
v-if="
|
|
182
|
-
|
|
183
|
-
|
|
174
|
+
<div
|
|
175
|
+
v-if="
|
|
176
|
+
access.canManageIntegrations.value || (github.available && access.canWriteBoard.value)
|
|
177
|
+
"
|
|
178
|
+
class="pointer-events-auto flex flex-wrap items-center justify-center gap-2"
|
|
184
179
|
>
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
180
|
+
<UButton
|
|
181
|
+
v-if="access.canManageIntegrations.value"
|
|
182
|
+
icon="i-lucide-git-branch-plus"
|
|
183
|
+
@click="ui.openBootstrap()"
|
|
184
|
+
>
|
|
185
|
+
{{ t('nav.bootstrapRepo') }}
|
|
186
|
+
</UButton>
|
|
187
|
+
<UButton
|
|
188
|
+
v-if="github.available && access.canWriteBoard.value"
|
|
189
|
+
variant="soft"
|
|
190
|
+
icon="i-lucide-folder-git-2"
|
|
191
|
+
@click="ui.openAddService()"
|
|
192
|
+
>
|
|
193
|
+
{{ t('nav.addFromRepo') }}
|
|
194
|
+
</UButton>
|
|
195
|
+
</div>
|
|
196
|
+
<!-- A read-only viewer sees the empty state but no create affordances. -->
|
|
197
|
+
<p v-else class="max-w-sm text-xs text-app-600">{{ t('access.noBoardWrite') }}</p>
|
|
198
|
+
</EmptyState>
|
|
198
199
|
</div>
|
|
199
200
|
|
|
200
201
|
<!-- task dependency arrows, overlaid in screen space -->
|
|
@@ -146,9 +146,7 @@ async function create() {
|
|
|
146
146
|
const { resolved, failures } = await resolvePending(pendingContext.value)
|
|
147
147
|
pendingContext.value = resolved
|
|
148
148
|
if (failures.length) {
|
|
149
|
-
presentLinkFailures(failures, undefined, {
|
|
150
|
-
title: (count) => t('initiative.create.contextFailed', { count }, count),
|
|
151
|
-
})
|
|
149
|
+
presentLinkFailures(failures, undefined, { titleKey: 'initiative.create.contextFailed' })
|
|
152
150
|
return
|
|
153
151
|
}
|
|
154
152
|
const { block } = await initiatives.create(frameId, {
|
|
@@ -163,7 +161,7 @@ async function create() {
|
|
|
163
161
|
// doc another task already holds), surfaced with its specific cause rather than a bare count.
|
|
164
162
|
// The initiative is already created, so a failed link never costs the user the form.
|
|
165
163
|
presentLinkFailures(await linkPending(block.id, pendingContext.value), block.id, {
|
|
166
|
-
|
|
164
|
+
titleKey: 'initiative.create.linkFailed',
|
|
167
165
|
})
|
|
168
166
|
ui.closeCreateInitiative()
|
|
169
167
|
// Select the fresh block so the inspector offers "Run planning" right away.
|
|
@@ -20,7 +20,7 @@ const pipelines = usePipelinesStore()
|
|
|
20
20
|
const recurring = useRecurringPipelinesStore()
|
|
21
21
|
const tracker = useTrackerStore()
|
|
22
22
|
const tasks = useTasksStore()
|
|
23
|
-
const
|
|
23
|
+
const actionToast = useActionToast()
|
|
24
24
|
const { present } = usePipelineErrorToast()
|
|
25
25
|
const access = useWorkspaceAccess()
|
|
26
26
|
const { t, te } = useI18n()
|
|
@@ -394,12 +394,7 @@ async function add() {
|
|
|
394
394
|
} catch (e) {
|
|
395
395
|
const refusal = intakeRefusalCopy(e)
|
|
396
396
|
if (refusal) {
|
|
397
|
-
|
|
398
|
-
title: t('board.recurring.addFailedTitle'),
|
|
399
|
-
description: refusal,
|
|
400
|
-
icon: 'i-lucide-triangle-alert',
|
|
401
|
-
color: 'error',
|
|
402
|
-
})
|
|
397
|
+
actionToast.error('board.recurring.addFailedTitle', { description: refusal })
|
|
403
398
|
} else present(e, 'board.recurring.addFailedTitle')
|
|
404
399
|
} finally {
|
|
405
400
|
saving.value = false
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { AgentState, PipelineStep } from '~/types/domain'
|
|
3
3
|
import { agentKindMeta } from '~/utils/catalog'
|
|
4
|
+
import Spinner from '~/components/common/Spinner.vue'
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{
|
|
6
7
|
step: PipelineStep
|
|
@@ -19,7 +20,7 @@ const stateRing: Record<AgentState, string> = {
|
|
|
19
20
|
|
|
20
21
|
const stateIcon: Record<AgentState, string | null> = {
|
|
21
22
|
pending: null,
|
|
22
|
-
working:
|
|
23
|
+
working: null,
|
|
23
24
|
waiting_decision: 'i-lucide-circle-help',
|
|
24
25
|
done: 'i-lucide-check',
|
|
25
26
|
}
|
|
@@ -39,7 +40,7 @@ const dim = computed(() => (props.size === 'sm' ? 'h-7 w-7' : 'h-9 w-9'))
|
|
|
39
40
|
v-if="step.state === 'working'"
|
|
40
41
|
class="absolute -bottom-1 -end-1 rounded-full bg-default p-0.5"
|
|
41
42
|
>
|
|
42
|
-
<
|
|
43
|
+
<Spinner class="h-3 w-3 text-primary" />
|
|
43
44
|
</span>
|
|
44
45
|
<span
|
|
45
46
|
v-else-if="stateIcon[step.state]"
|
|
@@ -10,6 +10,7 @@ import AgentStopButton from '~/components/board/AgentStopButton.vue'
|
|
|
10
10
|
import AdoptionReviewModal from '~/components/bootstrap/AdoptionReviewModal.vue'
|
|
11
11
|
import BootstrapRunSteps from '~/components/bootstrap/BootstrapRunSteps.vue'
|
|
12
12
|
import IconButton from '~/components/common/IconButton.vue'
|
|
13
|
+
import Spinner from '~/components/common/Spinner.vue'
|
|
13
14
|
import { useBlockDrag } from '~/composables/useBlockDrag'
|
|
14
15
|
import { useFrameStacking } from '~/composables/useFrameStacking'
|
|
15
16
|
import { useViewport } from '~/composables/useViewport'
|
|
@@ -224,9 +225,10 @@ const bootstrapPct = computed(() => {
|
|
|
224
225
|
// The actual todo items the agent is working through, surfaced on the expanded
|
|
225
226
|
// card so a zoomed-in user sees the task list, not just the "N/M" count.
|
|
226
227
|
const bootstrapItems = computed(() => bootstrapSubtasks.value?.items ?? [])
|
|
227
|
-
|
|
228
|
+
// `in_progress` takes the spinner's own loading glyph.
|
|
229
|
+
const ITEM_ICON: Record<string, string | undefined> = {
|
|
228
230
|
completed: 'i-lucide-check-circle-2',
|
|
229
|
-
in_progress:
|
|
231
|
+
in_progress: undefined,
|
|
230
232
|
pending: 'i-lucide-circle',
|
|
231
233
|
}
|
|
232
234
|
</script>
|
|
@@ -302,10 +304,7 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
302
304
|
data-testid="bootstrap-progress"
|
|
303
305
|
>
|
|
304
306
|
<div class="flex items-center gap-1.5 text-xs">
|
|
305
|
-
<
|
|
306
|
-
name="i-lucide-loader-circle"
|
|
307
|
-
class="h-4 w-4 shrink-0 animate-spin text-app-warning-400"
|
|
308
|
-
/>
|
|
307
|
+
<Spinner class="h-4 w-4 shrink-0 text-app-warning-400" />
|
|
309
308
|
<span class="text-app-warning-300">{{ t('board.frame.bootstrappingRepository') }}</span>
|
|
310
309
|
<span v-if="bootstrapSubtasks" class="ms-auto text-app-warning-200/80">
|
|
311
310
|
{{
|
|
@@ -336,13 +335,17 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
336
335
|
: 'text-app-warning-200/80'
|
|
337
336
|
"
|
|
338
337
|
>
|
|
339
|
-
<
|
|
338
|
+
<Spinner
|
|
340
339
|
:name="ITEM_ICON[item.status]"
|
|
340
|
+
:spinning="item.status === 'in_progress'"
|
|
341
341
|
class="mt-px h-3 w-3 shrink-0"
|
|
342
|
-
:class="
|
|
343
|
-
item.status === 'in_progress'
|
|
344
|
-
|
|
345
|
-
|
|
342
|
+
:class="
|
|
343
|
+
item.status === 'in_progress'
|
|
344
|
+
? 'text-app-warning-400'
|
|
345
|
+
: item.status === 'completed'
|
|
346
|
+
? 'text-app-success-400'
|
|
347
|
+
: 'text-app-warning-400/70'
|
|
348
|
+
"
|
|
346
349
|
/>
|
|
347
350
|
<span>{{ item.label }}</span>
|
|
348
351
|
</li>
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// renders now. An initiative is still a first-class board block with its own inspector.
|
|
19
19
|
import type { InitiativeStatus } from '~/types/domain'
|
|
20
20
|
import { useInitiativePlanning } from '~/composables/useInitiativePlanning'
|
|
21
|
+
import Spinner from '~/components/common/Spinner.vue'
|
|
21
22
|
import {
|
|
22
23
|
INITIATIVE_ATTENTION_ICONS,
|
|
23
24
|
INITIATIVE_ATTENTION_LABEL_KEYS,
|
|
@@ -120,17 +121,19 @@ function select() {
|
|
|
120
121
|
>
|
|
121
122
|
{{ t('initiative.inspector.answerPlanning') }}
|
|
122
123
|
</UButton>
|
|
123
|
-
<!-- Mid-pass: nothing to answer, but keep the route into the window (it shows the wait).
|
|
124
|
+
<!-- Mid-pass: nothing to answer, but keep the route into the window (it shows the wait).
|
|
125
|
+
A spinner in the leading slot, not `:loading`, which would disable the button. -->
|
|
124
126
|
<UButton
|
|
125
127
|
v-else-if="interviewing"
|
|
126
128
|
data-testid="initiative-card-planning-in-progress"
|
|
127
129
|
size="xs"
|
|
128
130
|
variant="soft"
|
|
129
131
|
color="neutral"
|
|
130
|
-
icon="i-lucide-loader-circle"
|
|
131
|
-
:ui="{ leadingIcon: 'animate-spin' }"
|
|
132
132
|
@click.stop="openPlanning"
|
|
133
133
|
>
|
|
134
|
+
<template #leading>
|
|
135
|
+
<Spinner class="size-4 shrink-0" />
|
|
136
|
+
</template>
|
|
134
137
|
{{ t('initiative.inspector.planningInProgress') }}
|
|
135
138
|
</UButton>
|
|
136
139
|
<UButton
|