@cat-factory/app 0.309.0 → 0.310.1
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 +11 -2
- package/app/components/assistant/AssistantModal.vue +5 -4
- package/app/components/auth/BackendMisconfiguredScreen.vue +4 -3
- package/app/components/auth/LoginScreen.vue +40 -26
- package/app/components/auth/ResetPasswordScreen.vue +9 -4
- package/app/components/auth/UserMenu.vue +4 -3
- package/app/components/binaryCandidates/BinaryCandidatesWindow.vue +34 -23
- package/app/components/binaryOutput/StoredAssetView.vue +8 -6
- package/app/components/board/AddTaskModal.vue +16 -15
- package/app/components/board/AgentFailureCard.vue +12 -9
- package/app/components/board/AgentFailureHistory.vue +18 -9
- package/app/components/board/CreateInitiativeModal.vue +5 -4
- package/app/components/board/FailureDetail.vue +22 -12
- package/app/components/board/ReviewFrictionDialog.vue +4 -3
- package/app/components/board/nodes/BlockNode.vue +17 -15
- package/app/components/board/nodes/DecisionBadge.vue +4 -3
- package/app/components/board/nodes/FrameSwimlanes.vue +4 -3
- package/app/components/board/nodes/TaskCard.vue +12 -7
- package/app/components/board/nodes/TaskPipelineMini.vue +9 -7
- package/app/components/bootstrap/AdoptionReviewModal.vue +30 -21
- package/app/components/bootstrap/BootstrapModal.vue +3 -3
- package/app/components/brainstorm/BrainstormWindow.vue +5 -3
- package/app/components/bugFishing/BugFishingWindow.vue +49 -27
- package/app/components/clarity/ClarityReviewWindow.vue +5 -3
- package/app/components/common/CopyButton.vue +12 -11
- package/app/components/common/IconButton.vue +34 -9
- package/app/components/context/ContextAttachmentFields.vue +23 -12
- package/app/components/documents/ContextDocumentPicker.vue +12 -9
- package/app/components/documents/DocumentImportModal.vue +4 -3
- package/app/components/documents/DocumentOriginLink.vue +12 -9
- package/app/components/documents/DocumentSyncState.vue +3 -3
- package/app/components/documents/RepoContextDocPicker.vue +4 -3
- package/app/components/environments/EnvironmentStatusPanel.vue +4 -3
- package/app/components/followUp/FollowUpWindow.vue +9 -6
- package/app/components/forkDecision/ForkDecisionWindow.vue +71 -80
- package/app/components/foundational/FoundationalServiceCatalogList.vue +3 -2
- package/app/components/foundational/FoundationalServiceRegistry.vue +7 -6
- package/app/components/foundational/FoundationalServiceSources.vue +7 -6
- package/app/components/fragments/FragmentLibraryManager.vue +5 -4
- package/app/components/fragments/FragmentSelector.vue +12 -9
- package/app/components/gates/GateFailingCheckList.vue +4 -3
- package/app/components/gates/GateResultView.vue +17 -12
- package/app/components/github/AddServiceFromRepoModal.vue +11 -7
- package/app/components/github/GitHubOnboarding.vue +7 -2
- package/app/components/github/GitHubPanel.vue +9 -6
- package/app/components/github/RepoTreeBrowser.vue +15 -12
- package/app/components/humanTest/HumanTestWindow.vue +13 -9
- package/app/components/initiative/InitiativePlanReview.vue +28 -18
- package/app/components/initiative/InitiativeTrackerWindow.vue +171 -124
- package/app/components/judge/JudgeResultView.vue +5 -3
- package/app/components/kaizen/KaizenPanel.vue +67 -53
- package/app/components/layout/AccountTeamSettings.vue +9 -9
- package/app/components/layout/AppearanceSwitcher.vue +4 -3
- package/app/components/layout/BoardSwitcher.vue +10 -8
- package/app/components/layout/BoardToolbar.vue +5 -4
- package/app/components/layout/CommandBar.vue +4 -3
- package/app/components/layout/DefaultTestEnvBanner.vue +4 -3
- package/app/components/layout/IntegrationsHub.vue +8 -6
- package/app/components/layout/LanguageSwitcher.vue +4 -3
- package/app/components/layout/ModelProvidersHub.vue +4 -3
- package/app/components/layout/NotificationsInbox.vue +40 -30
- package/app/components/layout/PersonalSetupModal.vue +4 -3
- package/app/components/layout/RolePrompt.vue +4 -3
- package/app/components/layout/SideBar.vue +4 -3
- package/app/components/layout/TranslationWarningBanner.vue +4 -3
- package/app/components/layout/UiModeSwitcher.vue +8 -6
- package/app/components/layout/UiRoleSwitcher.vue +8 -6
- package/app/components/layout/WorkspaceMembersSettings.vue +2 -2
- package/app/components/media/ArtifactLightbox.vue +69 -39
- package/app/components/media/ImageCompare.vue +69 -74
- package/app/components/observability/OutcomeFilterChips.vue +5 -4
- package/app/components/observability/RunFailureSummary.vue +8 -6
- package/app/components/observability/StepModelActivity.vue +5 -3
- package/app/components/observability/ToolCallList.vue +4 -2
- package/app/components/outcome/OutcomeSummaryWindow.vue +5 -4
- package/app/components/palettes/AgentPalette.vue +18 -14
- package/app/components/panels/AgentStepDetail.vue +30 -19
- package/app/components/panels/ObservabilityPanel.vue +149 -114
- package/app/components/panels/OperatorDashboardPanel.vue +99 -91
- package/app/components/panels/ReportsPanel.vue +64 -61
- package/app/components/panels/ResultWindowShell.vue +148 -134
- package/app/components/panels/StepContainerStatus.vue +9 -9
- package/app/components/panels/StepDelegatedStatus.vue +11 -9
- package/app/components/panels/StepRestartControl.vue +3 -2
- package/app/components/panels/inspector/EpicChildren.vue +4 -3
- package/app/components/panels/inspector/FrontendConfig.vue +3 -2
- package/app/components/panels/inspector/InspectorSection.vue +22 -17
- package/app/components/panels/inspector/ServiceConnections.vue +3 -2
- package/app/components/panels/inspector/ServiceTestConfig.vue +5 -4
- package/app/components/panels/inspector/TaskExecution.vue +5 -4
- package/app/components/panels/inspector/TaskRunSettings.vue +10 -6
- package/app/components/pipeline/EstimateThresholdFields.vue +13 -10
- package/app/components/pipeline/GateConfigFields.vue +14 -18
- package/app/components/pipeline/PipelineBuilder.vue +90 -77
- package/app/components/pipeline/PipelinePicker.vue +8 -6
- package/app/components/pipeline/PipelineProgress.vue +26 -21
- package/app/components/prReview/PrReviewWindow.vue +46 -25
- package/app/components/providers/ApiKeysSection.vue +4 -3
- package/app/components/ralph/RalphLoopResultView.vue +4 -3
- package/app/components/requirements/RequirementsReviewWindow.vue +10 -6
- package/app/components/riskPolicy/RiskPolicyPicker.vue +8 -6
- package/app/components/sandbox/SandboxPanel.vue +73 -54
- package/app/components/settings/CloudflareHandlerSection.vue +33 -30
- package/app/components/settings/ConsensusGroupsSection.vue +40 -30
- package/app/components/settings/InfraHandlersConfigurator.vue +16 -13
- package/app/components/settings/InfrastructureBackendPicker.vue +17 -17
- package/app/components/settings/IssueTrackerPanel.vue +21 -6
- package/app/components/settings/LocalModelEndpointsPanel.vue +3 -2
- package/app/components/settings/ModelConfigurationPanel.vue +9 -8
- package/app/components/settings/OpenRouterCatalogPanel.vue +4 -3
- package/app/components/settings/ProviderPreferenceEditor.vue +5 -6
- package/app/components/settings/RiskPolicyEditorRow.vue +3 -2
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +10 -8
- package/app/components/skills/ReviewSkillQueue.vue +4 -3
- package/app/components/skills/SkillLibraryManager.vue +7 -6
- package/app/components/slack/SlackPanel.vue +9 -5
- package/app/components/spec/ServiceSpecWindow.vue +4 -3
- package/app/components/tasks/ContextIssuePicker.vue +12 -9
- package/app/components/tasks/TaskContextIssues.vue +4 -3
- package/app/components/testing/TestReportWindow.vue +14 -8
- package/app/components/theme/ThemeImportModal.vue +2 -2
- package/app/components/visualConfirm/VisualConfirmationWindow.vue +62 -35
- package/i18n/locales/de.json +7 -3
- package/i18n/locales/en.json +7 -3
- package/i18n/locales/es.json +7 -3
- package/i18n/locales/fr.json +7 -3
- package/i18n/locales/he.json +7 -3
- package/i18n/locales/it.json +7 -3
- package/i18n/locales/ja.json +7 -3
- package/i18n/locales/pl.json +7 -3
- package/i18n/locales/tr.json +7 -3
- package/i18n/locales/uk.json +7 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -321,6 +321,15 @@ That is the whole mechanism, and it is why there is no rule here about WHICH ste
|
|
|
321
321
|
|
|
322
322
|
The default `radius` lives on the `Cat Factory` document in `app/utils/theme/builtins.ts`. It equals `THEME_DEFAULTS.radius` and so writes no CSS, which is the point: changing the number there is the whole edit.
|
|
323
323
|
|
|
324
|
+
### A control is its Nuxt UI component
|
|
325
|
+
|
|
326
|
+
Every CONTROL in the SPA is the Nuxt UI component for it, never the raw HTML element. A raw `<button>` beside a `UButton` is the same control rendered twice, and only one of the two follows the theme, the focus ring, the disabled state and the size scale; the SPA carried 152 of them, 24 raw `<input>`, five hand-built tables and six hand-rolled `<details>`, each with its own border, background and hover recipe.
|
|
327
|
+
|
|
328
|
+
- **The mapping**: `<button>` → `UButton`; `<input>` → `UInput` / `UInputNumber` / `UCheckbox` / `URadioGroup` / `USlider` / `UFileUpload`, by what the control IS rather than by its `type=`; `<select>` → `USelect` (`USelectMenu` when it needs searching, `UInputMenu` when the list is open-ended); `<textarea>` → `UTextarea`; `<table>` → `UTable` with column definitions; `<details>` → `UCollapsible`; `<form>` → `UForm`; `<a href>` → `ULink`; `<datalist>` → `UInputMenu` with `mode="autocomplete"`, which is what a datalist was (the DEFAULT combobox mode writes its model only on SELECT, so a typed free-text value is dropped). Which input for which job is the skill's [component-selection matrix](https://github.com/nuxt/ui/blob/v4/skills/nuxt-ui/references/component-selection.md), not restated here.
|
|
329
|
+
- **An icon-only button is [`common/IconButton.vue`](./app/components/common/IconButton.vue)**, which wraps `UButton` in a `UTooltip` and applies `label` as the tooltip text AND `aria-label`. A `title` attribute is not a tooltip: it never fires on touch, is unstyled, waits about a second, and looks nothing like the app's other hints. `IconButton` and `CopyButton` own their tooltip, so passing either a `title` renders a second hint over the first. A `title` on a LABELLED `UButton` is still fine: there it is a hover hint beside the control's own name rather than the name itself. `class` on `IconButton` lands on the wrapper the tooltip needs, which is the box the parent lays out, so it carries layout (`ms-auto`, `shrink-0`, `nodrag`); the button's own look goes through its props and `:ui="{ base: '…' }"`. The guard refuses an icon-only `UButton` whose only name is a `title`.
|
|
330
|
+
- **Prose and layout markup is not in scope.** `<p>`, `<h1>`-`<h6>`, `<ul>`, `<li>`, `<dl>`, `<pre>`, `<code>`, `<img>`, `<nav>`, `<div>` and `<span>` are not controls and have no Nuxt UI component; heading typography is the type scale's rule above.
|
|
331
|
+
- `scripts/check-frontend-primitives.mjs` (CI) holds the count at zero. There is deliberately **no allow-list**: the migration expected two exceptions (a Vue Flow gesture handle, a full-bleed media tile) and both converted cleanly, so the exception table would have had more surface than the thing it governed. A site that genuinely cannot convert says why with a `raw-control-ok:` comment, on that line or the one above; nothing in the tree needs one, and a second one appearing is the signal that the rule needs revisiting rather than the site.
|
|
332
|
+
|
|
324
333
|
### Themes and colour mode
|
|
325
334
|
|
|
326
335
|
A **theme** is a Nuxt UI theme editor document (https://ui.nuxt.com/theme): the sparse `ThemeDoc` the editor edits and shares as a `?doc=` link, decoded by `utils/theme/link.ts` and typed in `utils/theme/doc.ts` (a port of the editor engine's tables). The app speaks THAT format rather than one of its own, so a theme built in the editor imports as-is and a built-in theme is a document a user could have exported. `utils/theme/builtins.ts` ships `Cat Factory` (the default; its `colors` MUST equal `app.config.ts`'s `ui.colors`, which paints the first frame) and the editor's own `Mono` preset, referenced from the preset table rather than copied. The `theme` store persists the pick and the user's imported documents in this browser; `plugins/theme.client.ts` applies the active document at runtime: aliases onto `appConfig.ui.colors` (Nuxt UI's own colours plugin re-emits the ramps), default variants and component overrides onto `appConfig.ui.<component>` merged INTO the layer's own overrides, and tokens / radius / fonts onto one `<style id="app-theme">` whose selectors anchor on the `data-theme` attribute on `<html>` (`utils/theme/css.ts`, which is also the write boundary for untrusted values). The faces the built-in themes name (Geist, Geist Mono) are self-hosted through `@nuxt/fonts` (`fonts.families` in `nuxt.config.ts`, pinned by `builtins.spec.ts`); a font an IMPORTED theme names is applied as a family name only, so it renders when the device or the deployment provides it. A editor link whose document equals one of the editor's presets collapses to `{ preset: 'mono' }`; `utils/theme/presets.ts` is a copy of the editor's preset table so such a link rebuilds (a decode table, not menu entries).
|
|
@@ -375,8 +384,8 @@ protects, so the two never contradict silently:
|
|
|
375
384
|
freely; here a failed backend call is reported with `usePipelineErrorToast().present(error, key)`.
|
|
376
385
|
[Rule](#every-failure-toast-goes-through-one-funnel).
|
|
377
386
|
- **Icon-only buttons carry an accessible name.** Use `common/IconButton.vue`
|
|
378
|
-
([source](./app/components/common/IconButton.vue)), which applies `label` as both
|
|
379
|
-
`aria-label`, rather than a bare `<UButton icon>`.
|
|
387
|
+
([source](./app/components/common/IconButton.vue)), which applies `label` as both the tooltip
|
|
388
|
+
and `aria-label`, rather than a bare `<UButton icon>`.
|
|
380
389
|
- **Colour goes through theme tokens, never a fixed palette shade.** The skill bans raw Tailwind
|
|
381
390
|
hues; this repo also bans fixed numbered aliases, enforced by
|
|
382
391
|
[`scripts/check-frontend-palette.mjs`](https://github.com/kibertoad/cat-factory/blob/main/scripts/check-frontend-palette.mjs).
|
|
@@ -259,16 +259,17 @@ function reveal(blockId: string): void {
|
|
|
259
259
|
{{ t('assistant.examplesTitle') }}
|
|
260
260
|
</SectionLabel>
|
|
261
261
|
<div class="flex flex-col gap-1">
|
|
262
|
-
<
|
|
262
|
+
<UButton
|
|
263
|
+
color="neutral"
|
|
264
|
+
variant="ghost"
|
|
263
265
|
v-for="entry in examples"
|
|
264
266
|
:key="entry.actionId"
|
|
265
|
-
|
|
266
|
-
class="rounded-md px-2 py-1 text-left text-sm text-toned hover:bg-elevated"
|
|
267
|
+
class="block rounded-md px-2 py-1 text-left text-sm text-toned hover:bg-elevated"
|
|
267
268
|
@click="useExample(entry.example)"
|
|
268
269
|
>
|
|
269
270
|
<span class="text-muted">{{ entry.label }}</span>
|
|
270
271
|
<span class="block text-xs text-dimmed">“{{ entry.example }}”</span>
|
|
271
|
-
</
|
|
272
|
+
</UButton>
|
|
272
273
|
</div>
|
|
273
274
|
</div>
|
|
274
275
|
|
|
@@ -41,16 +41,17 @@ function reload() {
|
|
|
41
41
|
<span class="font-medium text-toned">{{ t('app.misconfigured.howToFix') }}</span>
|
|
42
42
|
{{ problem.remedy }}
|
|
43
43
|
</p>
|
|
44
|
-
<
|
|
44
|
+
<ULink
|
|
45
|
+
raw
|
|
45
46
|
v-if="problem.docsUrl"
|
|
46
|
-
:
|
|
47
|
+
:to="problem.docsUrl"
|
|
47
48
|
target="_blank"
|
|
48
49
|
rel="noopener noreferrer"
|
|
49
50
|
class="mt-2 inline-flex items-center gap-1 text-sm text-app-warning-300 hover:text-app-warning-200"
|
|
50
51
|
>
|
|
51
52
|
<UIcon name="i-lucide-book-open" class="h-4 w-4" />
|
|
52
53
|
{{ t('app.misconfigured.viewDocs') }}
|
|
53
|
-
</
|
|
54
|
+
</ULink>
|
|
54
55
|
</li>
|
|
55
56
|
</ul>
|
|
56
57
|
|
|
@@ -286,16 +286,17 @@ const noSignInMethod = computed(
|
|
|
286
286
|
<!-- Only when nothing can be installed here: the paste form below carries its own
|
|
287
287
|
per-provider link, so showing these too would offer the same thing twice. -->
|
|
288
288
|
<div v-if="installableProviders.length === 0" class="flex flex-wrap gap-3 px-1">
|
|
289
|
-
<
|
|
289
|
+
<ULink
|
|
290
|
+
raw
|
|
290
291
|
v-for="p in ALL_PROVIDERS"
|
|
291
292
|
:key="p"
|
|
292
|
-
:
|
|
293
|
+
:to="tokenCreateUrl(p)"
|
|
293
294
|
target="_blank"
|
|
294
295
|
rel="noopener noreferrer"
|
|
295
296
|
class="text-xs text-primary hover:underline"
|
|
296
297
|
>
|
|
297
298
|
{{ t('auth.localMode.createToken', { provider: PROVIDER_LABELS[p] }) }}
|
|
298
|
-
</
|
|
299
|
+
</ULink>
|
|
299
300
|
</div>
|
|
300
301
|
</template>
|
|
301
302
|
|
|
@@ -379,10 +380,10 @@ const noSignInMethod = computed(
|
|
|
379
380
|
</div>
|
|
380
381
|
|
|
381
382
|
<!-- Email / password -->
|
|
382
|
-
<
|
|
383
|
+
<UForm
|
|
383
384
|
v-if="auth.providers.password && mode !== 'forgot'"
|
|
384
385
|
class="space-y-3"
|
|
385
|
-
@submit
|
|
386
|
+
@submit="submitPassword"
|
|
386
387
|
>
|
|
387
388
|
<UInput
|
|
388
389
|
v-if="mode === 'signup'"
|
|
@@ -426,36 +427,43 @@ const noSignInMethod = computed(
|
|
|
426
427
|
<template v-if="mode === 'login'">
|
|
427
428
|
<i18n-t keypath="auth.login.needAccount" tag="span" scope="global">
|
|
428
429
|
<template #signUp>
|
|
429
|
-
<
|
|
430
|
-
|
|
431
|
-
|
|
430
|
+
<UButton
|
|
431
|
+
color="primary"
|
|
432
|
+
variant="link"
|
|
433
|
+
class="p-0 text-xs text-primary hover:underline"
|
|
432
434
|
@click="setMode('signup')"
|
|
433
435
|
>
|
|
434
436
|
{{ t('auth.login.signUp') }}
|
|
435
|
-
</
|
|
437
|
+
</UButton>
|
|
436
438
|
</template>
|
|
437
439
|
</i18n-t>
|
|
438
440
|
</template>
|
|
439
441
|
<template v-else>
|
|
440
442
|
<i18n-t keypath="auth.login.haveAccount" tag="span" scope="global">
|
|
441
443
|
<template #signIn>
|
|
442
|
-
<
|
|
443
|
-
|
|
444
|
-
|
|
444
|
+
<UButton
|
|
445
|
+
color="primary"
|
|
446
|
+
variant="link"
|
|
447
|
+
class="p-0 text-xs text-primary hover:underline"
|
|
445
448
|
@click="setMode('login')"
|
|
446
449
|
>
|
|
447
450
|
{{ t('auth.login.signIn') }}
|
|
448
|
-
</
|
|
451
|
+
</UButton>
|
|
449
452
|
</template>
|
|
450
453
|
</i18n-t>
|
|
451
454
|
</template>
|
|
452
455
|
</p>
|
|
453
456
|
<p v-if="mode === 'login'" class="text-center text-xs text-muted">
|
|
454
|
-
<
|
|
457
|
+
<UButton
|
|
458
|
+
color="primary"
|
|
459
|
+
variant="link"
|
|
460
|
+
class="p-0 text-xs text-primary hover:underline"
|
|
461
|
+
@click="setMode('forgot')"
|
|
462
|
+
>
|
|
455
463
|
{{ t('auth.login.forgotPassword') }}
|
|
456
|
-
</
|
|
464
|
+
</UButton>
|
|
457
465
|
</p>
|
|
458
|
-
</
|
|
466
|
+
</UForm>
|
|
459
467
|
|
|
460
468
|
<!-- Paste-a-token sign-in: your own PAT on a hosted node; on local mode the token this
|
|
461
469
|
deployment will operate with (see `remotePatProviders`). -->
|
|
@@ -472,7 +480,7 @@ const noSignInMethod = computed(
|
|
|
472
480
|
<span class="h-px flex-1 bg-elevated" /> {{ t('auth.login.or') }}
|
|
473
481
|
<span class="h-px flex-1 bg-elevated" />
|
|
474
482
|
</div>
|
|
475
|
-
<
|
|
483
|
+
<UForm class="space-y-3" @submit="submitRemotePat">
|
|
476
484
|
<div v-if="remotePatProviders.length > 1" class="flex gap-2">
|
|
477
485
|
<UButton
|
|
478
486
|
v-for="p in remotePatProviders"
|
|
@@ -517,8 +525,9 @@ const noSignInMethod = computed(
|
|
|
517
525
|
{{ t('auth.localMode.tokenBecomesCredential') }}
|
|
518
526
|
</p>
|
|
519
527
|
<p class="px-1 text-center">
|
|
520
|
-
<
|
|
521
|
-
|
|
528
|
+
<ULink
|
|
529
|
+
raw
|
|
530
|
+
:to="tokenCreateUrl(remotePatProvider)"
|
|
522
531
|
target="_blank"
|
|
523
532
|
rel="noopener noreferrer"
|
|
524
533
|
class="text-xs text-primary hover:underline"
|
|
@@ -526,9 +535,9 @@ const noSignInMethod = computed(
|
|
|
526
535
|
{{
|
|
527
536
|
t('auth.localMode.createToken', { provider: PROVIDER_LABELS[remotePatProvider] })
|
|
528
537
|
}}
|
|
529
|
-
</
|
|
538
|
+
</ULink>
|
|
530
539
|
</p>
|
|
531
|
-
</
|
|
540
|
+
</UForm>
|
|
532
541
|
</template>
|
|
533
542
|
|
|
534
543
|
<!-- No sign-in method configured on a remote deployment: explain, don't show a blank card -->
|
|
@@ -542,10 +551,10 @@ const noSignInMethod = computed(
|
|
|
542
551
|
/>
|
|
543
552
|
|
|
544
553
|
<!-- Forgot password: request a reset link by email -->
|
|
545
|
-
<
|
|
554
|
+
<UForm
|
|
546
555
|
v-if="auth.providers.password && mode === 'forgot'"
|
|
547
556
|
class="space-y-3"
|
|
548
|
-
@submit
|
|
557
|
+
@submit="submitForgot"
|
|
549
558
|
>
|
|
550
559
|
<template v-if="forgotSent">
|
|
551
560
|
<p class="text-sm text-toned">
|
|
@@ -568,11 +577,16 @@ const noSignInMethod = computed(
|
|
|
568
577
|
</UButton>
|
|
569
578
|
</template>
|
|
570
579
|
<p class="text-center text-xs text-muted">
|
|
571
|
-
<
|
|
580
|
+
<UButton
|
|
581
|
+
color="primary"
|
|
582
|
+
variant="link"
|
|
583
|
+
class="p-0 text-xs text-primary hover:underline"
|
|
584
|
+
@click="setMode('login')"
|
|
585
|
+
>
|
|
572
586
|
{{ t('auth.login.backToSignIn') }}
|
|
573
|
-
</
|
|
587
|
+
</UButton>
|
|
574
588
|
</p>
|
|
575
|
-
</
|
|
589
|
+
</UForm>
|
|
576
590
|
</div>
|
|
577
591
|
</div>
|
|
578
592
|
</template>
|
|
@@ -76,7 +76,7 @@ function goToLogin() {
|
|
|
76
76
|
</UButton>
|
|
77
77
|
</template>
|
|
78
78
|
|
|
79
|
-
<
|
|
79
|
+
<UForm v-else class="space-y-3" @submit="submit">
|
|
80
80
|
<SecretInput
|
|
81
81
|
v-model="password"
|
|
82
82
|
required
|
|
@@ -98,11 +98,16 @@ function goToLogin() {
|
|
|
98
98
|
{{ t('auth.resetPassword.submit') }}
|
|
99
99
|
</UButton>
|
|
100
100
|
<p class="text-center text-xs text-muted">
|
|
101
|
-
<
|
|
101
|
+
<UButton
|
|
102
|
+
color="primary"
|
|
103
|
+
variant="link"
|
|
104
|
+
class="p-0 text-xs text-primary hover:underline"
|
|
105
|
+
@click="goToLogin"
|
|
106
|
+
>
|
|
102
107
|
{{ t('auth.resetPassword.backToSignIn') }}
|
|
103
|
-
</
|
|
108
|
+
</UButton>
|
|
104
109
|
</p>
|
|
105
|
-
</
|
|
110
|
+
</UForm>
|
|
106
111
|
</div>
|
|
107
112
|
</div>
|
|
108
113
|
</template>
|
|
@@ -33,8 +33,9 @@ const items = computed<DropdownMenuItem[][]>(() => [
|
|
|
33
33
|
|
|
34
34
|
<template>
|
|
35
35
|
<UDropdownMenu v-if="auth.user" :items="items" :content="{ side: 'top', align: 'start' }">
|
|
36
|
-
<
|
|
37
|
-
|
|
36
|
+
<UButton
|
|
37
|
+
color="neutral"
|
|
38
|
+
variant="ghost"
|
|
38
39
|
data-testid="user-menu"
|
|
39
40
|
:title="collapsed ? auth.user.name || auth.user.login : undefined"
|
|
40
41
|
class="flex w-full items-center gap-2 rounded-lg border border-default bg-default/60 p-2 text-start transition hover:bg-elevated/60"
|
|
@@ -53,6 +54,6 @@ const items = computed<DropdownMenuItem[][]>(() => [
|
|
|
53
54
|
<div class="truncate text-3xs text-dimmed">@{{ auth.user.login }}</div>
|
|
54
55
|
</div>
|
|
55
56
|
<UIcon v-if="!collapsed" name="i-lucide-chevron-up" class="h-4 w-4 shrink-0 text-dimmed" />
|
|
56
|
-
</
|
|
57
|
+
</UButton>
|
|
57
58
|
</UDropdownMenu>
|
|
58
59
|
</template>
|
|
@@ -235,6 +235,16 @@ const { requestClose } = useUnsavedGuard({
|
|
|
235
235
|
>
|
|
236
236
|
</p>
|
|
237
237
|
|
|
238
|
+
<!-- Single-select renders a checkbox per card, not one radio group, so this line is what
|
|
239
|
+
tells everyone (a screen reader through each box's description) that ticking one clears
|
|
240
|
+
the other. -->
|
|
241
|
+
<p
|
|
242
|
+
v-if="view.awaiting && !view.multiSelect"
|
|
243
|
+
id="binary-candidates-pick-one"
|
|
244
|
+
class="mb-3 text-xs text-muted"
|
|
245
|
+
>
|
|
246
|
+
{{ t('binaryCandidates.pickOne') }}
|
|
247
|
+
</p>
|
|
238
248
|
<div v-for="group in view.groups" :key="group.subject ?? '·'" class="mb-6">
|
|
239
249
|
<h3 class="mb-2 text-xs font-medium text-muted">
|
|
240
250
|
{{ group.subject ?? t('binaryCandidates.unlabelledSubject') }}
|
|
@@ -255,33 +265,34 @@ const { requestClose } = useUnsavedGuard({
|
|
|
255
265
|
>
|
|
256
266
|
<!-- The REAL control, not the card's click handler: ticking a candidate is the only
|
|
257
267
|
way to keep an asset, so without a focusable input the gate could not be completed
|
|
258
|
-
by keyboard at all (UX-80).
|
|
259
|
-
|
|
260
|
-
group.
|
|
268
|
+
by keyboard at all (UX-80). In single-select mode `toggle` replaces the selection
|
|
269
|
+
across every subject rather than per group.
|
|
261
270
|
|
|
262
|
-
`@click.stop` belongs on the
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
271
|
+
`@click.stop` belongs on the WRAPPER, which is what the card's own toggle has to
|
|
272
|
+
be shielded from, and NOT on the control: activating a label forwards a synthetic
|
|
273
|
+
click to its control (which `.stop` there does not prevent, only its
|
|
274
|
+
propagation), so a click on the label TEXT bubbles to the card and toggles, then
|
|
275
|
+
the forwarded click toggles back. On a checkbox that nets to no change, which
|
|
276
|
+
means no re-render, which leaves the box ticked over a candidate that is no
|
|
277
|
+
longer selected. UCheckbox is not the place for it either: Nuxt UI forwards
|
|
278
|
+
`$attrs` onto the inner checkbox button rather than onto its root, so a listener
|
|
279
|
+
put there is the same "on the control alone" mistake spelled differently. -->
|
|
280
|
+
<!-- One control either way. Single-select is a checkbox rather than a radio group,
|
|
281
|
+
because the rows render per candidate across several subjects and a radio group
|
|
282
|
+
needs the whole set in one place. So the box announces as a checkbox, and the
|
|
283
|
+
"pick one" line above is its description: that is the cue that ticking it clears
|
|
284
|
+
the previous pick. -->
|
|
285
|
+
<div v-if="view.awaiting" class="mb-1.5" @click.stop>
|
|
286
|
+
<UCheckbox
|
|
287
|
+
size="xs"
|
|
288
|
+
:model-value="selected.includes(row.id)"
|
|
289
|
+
:label="candidateLabel(row)"
|
|
279
290
|
:aria-label="candidateLabel(row)"
|
|
291
|
+
:aria-describedby="view.multiSelect ? undefined : 'binary-candidates-pick-one'"
|
|
280
292
|
data-testid="binary-candidate-select"
|
|
281
|
-
@
|
|
293
|
+
@update:model-value="toggle(row.id)"
|
|
282
294
|
/>
|
|
283
|
-
|
|
284
|
-
</label>
|
|
295
|
+
</div>
|
|
285
296
|
<!-- Staged through the platform's OWN asset storage: we hold the bytes, so the card
|
|
286
297
|
renders them (and offers to open or save one) rather than waiting for a public
|
|
287
298
|
link the shipped storage never issues. Checked first because a candidate can
|
|
@@ -112,22 +112,24 @@ const filename = computed(() => {
|
|
|
112
112
|
<!-- The two things a person does with a delivered asset. Both hang off the object URL, so
|
|
113
113
|
they appear only once the bytes are in hand. -->
|
|
114
114
|
<div v-if="url" class="flex items-center gap-3 text-2xs">
|
|
115
|
-
<
|
|
116
|
-
|
|
115
|
+
<ULink
|
|
116
|
+
raw
|
|
117
|
+
:to="url"
|
|
117
118
|
target="_blank"
|
|
118
119
|
rel="noopener"
|
|
119
120
|
class="text-app-info-300 hover:underline"
|
|
120
121
|
data-testid="stored-asset-open"
|
|
121
122
|
@click.stop
|
|
122
|
-
>{{ t('binaryOutput.asset.open') }}</
|
|
123
|
+
>{{ t('binaryOutput.asset.open') }}</ULink
|
|
123
124
|
>
|
|
124
|
-
<
|
|
125
|
-
|
|
125
|
+
<ULink
|
|
126
|
+
raw
|
|
127
|
+
:to="url"
|
|
126
128
|
:download="filename"
|
|
127
129
|
class="text-app-info-300 hover:underline"
|
|
128
130
|
data-testid="stored-asset-download"
|
|
129
131
|
@click.stop
|
|
130
|
-
>{{ t('binaryOutput.asset.download') }}</
|
|
132
|
+
>{{ t('binaryOutput.asset.download') }}</ULink
|
|
131
133
|
>
|
|
132
134
|
</div>
|
|
133
135
|
</div>
|
|
@@ -142,6 +142,11 @@ const timeboxHours = ref<number | undefined>(undefined)
|
|
|
142
142
|
// default — an expedition exists to cover ground nobody thought to look at, so narrowing it is
|
|
143
143
|
// the deliberate act) plus an optional focus folded into every angle's prompt.
|
|
144
144
|
const fishingPhaseIds = ref<string[]>([])
|
|
145
|
+
function toggleFishingPhase(id: string, checked: boolean) {
|
|
146
|
+
fishingPhaseIds.value = checked
|
|
147
|
+
? [...fishingPhaseIds.value, id]
|
|
148
|
+
: fishingPhaseIds.value.filter((x) => x !== id)
|
|
149
|
+
}
|
|
145
150
|
const fishingFocus = ref('')
|
|
146
151
|
/** Held as a string because the input is a text field; parsed at submit, blank ⇒ the default. */
|
|
147
152
|
const fishingMaxPasses = ref('')
|
|
@@ -1105,23 +1110,19 @@ function openReviewFrictionDialog(conflict: NonNullable<ReturnType<typeof parseC
|
|
|
1105
1110
|
:description="t('board.addTask.bugFishingFields.angles.hint')"
|
|
1106
1111
|
>
|
|
1107
1112
|
<div class="grid gap-1.5 sm:grid-cols-2">
|
|
1108
|
-
|
|
1113
|
+
<!-- Each angle keeps its own test hook, so the boolean is derived from the id
|
|
1114
|
+
list rather than bound through a checkbox group. -->
|
|
1115
|
+
<UCheckbox
|
|
1109
1116
|
v-for="phase in BUG_FISHING_PHASES"
|
|
1110
1117
|
:key="phase.id"
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
/>
|
|
1120
|
-
<span class="min-w-0">
|
|
1121
|
-
<span class="block text-default">{{ phase.title }}</span>
|
|
1122
|
-
<span class="block text-2xs text-dimmed">{{ phase.goal }}</span>
|
|
1123
|
-
</span>
|
|
1124
|
-
</label>
|
|
1118
|
+
size="xs"
|
|
1119
|
+
:model-value="fishingPhaseIds.includes(phase.id)"
|
|
1120
|
+
:label="phase.title"
|
|
1121
|
+
:description="phase.goal"
|
|
1122
|
+
class="rounded-md px-1.5 py-1 hover:bg-elevated/40"
|
|
1123
|
+
:data-testid="`add-task-fishing-angle-${phase.id}`"
|
|
1124
|
+
@update:model-value="toggleFishingPhase(phase.id, $event === true)"
|
|
1125
|
+
/>
|
|
1125
1126
|
</div>
|
|
1126
1127
|
<p v-if="fishingPhaseIds.length === 0" class="mt-1.5 text-2xs text-dimmed">
|
|
1127
1128
|
{{ t('board.addTask.bugFishingFields.angles.allSelected') }}
|
|
@@ -197,8 +197,9 @@ async function retry() {
|
|
|
197
197
|
/>
|
|
198
198
|
|
|
199
199
|
<div class="mt-2 flex flex-wrap items-center gap-2">
|
|
200
|
-
<
|
|
201
|
-
|
|
200
|
+
<UButton
|
|
201
|
+
color="neutral"
|
|
202
|
+
variant="ghost"
|
|
202
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"
|
|
203
204
|
:class="compact ? 'px-2 py-0.5 text-3xs' : 'px-2 py-1 text-2xs'"
|
|
204
205
|
:disabled="retrying || !access.canExecuteRuns.value"
|
|
@@ -211,11 +212,12 @@ async function retry() {
|
|
|
211
212
|
:class="[compact ? 'h-3 w-3' : 'h-3.5 w-3.5', { 'animate-spin': retrying }]"
|
|
212
213
|
/>
|
|
213
214
|
{{ retrying ? t('board.failure.retrying') : compact ? t('common.retry') : retryLabel }}
|
|
214
|
-
</
|
|
215
|
+
</UButton>
|
|
215
216
|
|
|
216
|
-
<
|
|
217
|
+
<UButton
|
|
218
|
+
color="neutral"
|
|
219
|
+
variant="ghost"
|
|
217
220
|
v-if="run.kind === 'bootstrap'"
|
|
218
|
-
type="button"
|
|
219
221
|
class="nodrag flex items-center gap-1 rounded-md bg-app-error-900/20 text-app-error-300 hover:bg-app-error-900/50"
|
|
220
222
|
:class="compact ? 'px-2 py-0.5 text-3xs' : 'px-2 py-1 text-2xs'"
|
|
221
223
|
data-testid="agent-failure-inspect"
|
|
@@ -223,16 +225,17 @@ async function retry() {
|
|
|
223
225
|
>
|
|
224
226
|
<UIcon name="i-lucide-activity" :class="compact ? 'h-3 w-3' : 'h-3.5 w-3.5'" />
|
|
225
227
|
{{ t('observability.modelActivity') }}
|
|
226
|
-
</
|
|
228
|
+
</UButton>
|
|
227
229
|
|
|
228
230
|
<!-- Environment provisioning failures are almost always a deploy-backend / provider-config
|
|
229
231
|
issue, so link straight to where it's set up rather than leaving the user to hunt. The
|
|
230
232
|
destination + label follow the cause: a `deploy_runner_unwired` failure needs the runner
|
|
231
233
|
pool (Agent containers tab), every other cause needs the environment provider (Test
|
|
232
234
|
environments tab) — see `routesToRunnerPool`. -->
|
|
233
|
-
<
|
|
235
|
+
<UButton
|
|
236
|
+
color="neutral"
|
|
237
|
+
variant="ghost"
|
|
234
238
|
v-if="isEnvironmentFailure"
|
|
235
|
-
type="button"
|
|
236
239
|
class="nodrag flex items-center gap-1 rounded-md bg-app-error-900/20 text-app-error-300 hover:bg-app-error-900/50"
|
|
237
240
|
:class="compact ? 'px-2 py-0.5 text-3xs' : 'px-2 py-1 text-2xs'"
|
|
238
241
|
data-testid="agent-failure-configure-environment"
|
|
@@ -240,7 +243,7 @@ async function retry() {
|
|
|
240
243
|
>
|
|
241
244
|
<UIcon name="i-lucide-settings" :class="compact ? 'h-3 w-3' : 'h-3.5 w-3.5'" />
|
|
242
245
|
{{ failureSetupLabel }}
|
|
243
|
-
</
|
|
246
|
+
</UButton>
|
|
244
247
|
</div>
|
|
245
248
|
</div>
|
|
246
249
|
</template>
|
|
@@ -13,18 +13,27 @@ const { t } = useI18n()
|
|
|
13
13
|
</script>
|
|
14
14
|
|
|
15
15
|
<template>
|
|
16
|
-
<
|
|
16
|
+
<UCollapsible
|
|
17
17
|
v-if="failures.length"
|
|
18
18
|
class="nodrag rounded-lg border border-muted/60 bg-default/40 px-3 py-2"
|
|
19
19
|
data-testid="agent-failure-history"
|
|
20
20
|
>
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
<template #default="{ open }">
|
|
22
|
+
<UButton
|
|
23
|
+
variant="link"
|
|
24
|
+
color="neutral"
|
|
25
|
+
size="xs"
|
|
26
|
+
icon="i-lucide-history"
|
|
27
|
+
:trailing-icon="open ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
|
|
28
|
+
:label="
|
|
29
|
+
t('board.failure.history.previousErrors', { count: failures.length }, failures.length)
|
|
30
|
+
"
|
|
31
|
+
:ui="{ base: 'w-full justify-start gap-1.5 p-0 text-2xs text-muted hover:text-default' }"
|
|
32
|
+
/>
|
|
33
|
+
</template>
|
|
27
34
|
|
|
28
|
-
<
|
|
29
|
-
|
|
35
|
+
<template #content>
|
|
36
|
+
<FailureHistoryList :failures="props.failures" class="mt-2" />
|
|
37
|
+
</template>
|
|
38
|
+
</UCollapsible>
|
|
30
39
|
</template>
|
|
@@ -193,16 +193,17 @@ async function create() {
|
|
|
193
193
|
<div v-if="presets.length > 1" class="space-y-1.5">
|
|
194
194
|
<span class="text-xs font-medium text-toned">{{ t('initiative.create.preset') }}</span>
|
|
195
195
|
<div class="grid gap-2" data-testid="initiative-preset-picker">
|
|
196
|
-
<
|
|
196
|
+
<UButton
|
|
197
|
+
color="neutral"
|
|
198
|
+
variant="ghost"
|
|
197
199
|
v-for="p in presets"
|
|
198
200
|
:key="p.id"
|
|
199
|
-
type="button"
|
|
200
201
|
:data-testid="`initiative-preset-option-${p.id}`"
|
|
201
202
|
:aria-pressed="p.id === selectedPresetId"
|
|
202
203
|
class="flex items-start gap-3 rounded-md border px-3 py-2 text-left transition"
|
|
203
204
|
:class="
|
|
204
205
|
p.id === selectedPresetId
|
|
205
|
-
? 'border-primary bg-primary/10'
|
|
206
|
+
? 'border-primary bg-primary/10 hover:bg-primary/10 focus-visible:bg-primary/10 disabled:bg-primary/10'
|
|
206
207
|
: 'border-muted hover:border-app-600'
|
|
207
208
|
"
|
|
208
209
|
@click="selectPreset(p.id)"
|
|
@@ -220,7 +221,7 @@ async function create() {
|
|
|
220
221
|
{{ p.presentation.description }}
|
|
221
222
|
</span>
|
|
222
223
|
</span>
|
|
223
|
-
</
|
|
224
|
+
</UButton>
|
|
224
225
|
</div>
|
|
225
226
|
</div>
|
|
226
227
|
|
|
@@ -18,18 +18,28 @@ const { t } = useI18n()
|
|
|
18
18
|
</script>
|
|
19
19
|
|
|
20
20
|
<template>
|
|
21
|
-
<
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
<UCollapsible v-if="detail && detail !== message" class="mt-1">
|
|
22
|
+
<template #default="{ open }">
|
|
23
|
+
<UButton
|
|
24
|
+
variant="link"
|
|
25
|
+
color="neutral"
|
|
26
|
+
size="xs"
|
|
27
|
+
:icon="open ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
|
|
28
|
+
:label="t('board.failure.showDetail')"
|
|
29
|
+
:class="summaryClass"
|
|
30
|
+
:ui="{ base: 'w-full justify-start gap-1 p-0' }"
|
|
31
|
+
/>
|
|
32
|
+
</template>
|
|
25
33
|
<!-- The stack trace / extended detail: the first thing a user does with it is copy it, so
|
|
26
34
|
offer a copy affordance floated over the scroll box (UX-39). -->
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
<template #content>
|
|
36
|
+
<div class="relative mt-1">
|
|
37
|
+
<CopyButton :text="detail" class="absolute end-1 top-1 z-10" />
|
|
38
|
+
<pre
|
|
39
|
+
class="max-h-32 overflow-auto whitespace-pre-wrap rounded-sm p-1.5 pe-9"
|
|
40
|
+
:class="preClass"
|
|
41
|
+
>{{ detail }}</pre>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
44
|
+
</UCollapsible>
|
|
35
45
|
</template>
|
|
@@ -90,8 +90,9 @@ function createAnyway() {
|
|
|
90
90
|
</SectionLabel>
|
|
91
91
|
<ul class="space-y-1">
|
|
92
92
|
<li v-for="item in ctx.debt" :key="item.blockId">
|
|
93
|
-
<
|
|
94
|
-
|
|
93
|
+
<UButton
|
|
94
|
+
color="neutral"
|
|
95
|
+
variant="ghost"
|
|
95
96
|
class="flex w-full items-center justify-between gap-3 rounded-md px-2 py-1.5 text-left text-sm hover:bg-elevated/60 disabled:opacity-50"
|
|
96
97
|
:disabled="pending"
|
|
97
98
|
@click="goToBlock(item.blockId)"
|
|
@@ -102,7 +103,7 @@ function createAnyway() {
|
|
|
102
103
|
<span class="shrink-0 text-xs text-dimmed">
|
|
103
104
|
{{ t('errors.reviewFriction.waiting', { minutes: item.waitingMinutes }) }}
|
|
104
105
|
</span>
|
|
105
|
-
</
|
|
106
|
+
</UButton>
|
|
106
107
|
</li>
|
|
107
108
|
</ul>
|
|
108
109
|
</div>
|