@cat-factory/app 0.70.1 → 0.71.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/app/components/auth/LoginScreen.vue +3 -2
- package/app/components/board/AddTaskModal.vue +7 -1
- package/app/components/board/RecurringPipelineModal.vue +6 -1
- package/app/components/bootstrap/BootstrapModal.vue +3 -0
- package/app/components/documents/DocumentSourceConnectModal.vue +4 -3
- package/app/components/focus/BlockFocusView.vue +12 -7
- package/app/components/humanTest/HumanTestWindow.vue +15 -1
- package/app/components/layout/AccountDeploymentSettings.vue +4 -0
- package/app/components/layout/AccountTeamSettings.vue +8 -2
- package/app/components/layout/AiProvidersBanner.vue +3 -1
- package/app/components/layout/BoardToolbar.vue +12 -2
- package/app/components/layout/InfraSetupBanner.vue +192 -0
- package/app/components/layout/ProviderConfigBanner.vue +3 -1
- package/app/components/panels/InspectorPanel.vue +13 -7
- package/app/components/panels/inspector/FrontendConfig.vue +19 -4
- package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +4 -0
- package/app/components/panels/inspector/TaskRunSettings.vue +8 -1
- package/app/components/providers/ApiKeysSection.vue +4 -0
- package/app/components/settings/CustomManifestTypeEditor.vue +3 -0
- package/app/components/settings/InfraHandlersConfigurator.vue +72 -12
- package/app/components/settings/KubernetesEngineForm.vue +41 -5
- package/app/components/settings/ObservabilityConnectionPanel.vue +7 -0
- package/app/components/settings/ProviderConnectionTab.vue +2 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +10 -0
- package/app/components/tasks/TaskSourceConnectModal.vue +4 -3
- package/app/composables/useConfirmAction.ts +96 -0
- package/app/composables/usePipelineErrorToast.ts +2 -0
- package/app/composables/useWorkspaceStream.ts +26 -6
- package/app/pages/index.vue +20 -4
- package/app/stores/agentRuns.spec.ts +20 -2
- package/app/stores/agentRuns.ts +21 -11
- package/app/stores/ui.ts +17 -1
- package/app/stores/workspace.ts +10 -2
- package/app/types/domain.ts +3 -0
- package/app/utils/pipeline.ts +22 -0
- package/i18n/locales/en.json +74 -10
- package/i18n/locales/es.json +71 -10
- package/i18n/locales/fr.json +71 -10
- package/i18n/locales/he.json +71 -10
- package/i18n/locales/ja.json +71 -10
- package/i18n/locales/pl.json +71 -10
- package/i18n/locales/tr.json +71 -10
- package/i18n/locales/uk.json +71 -10
- package/package.json +3 -2
|
@@ -129,8 +129,9 @@ const showOAuthDivider = computed(
|
|
|
129
129
|
|
|
130
130
|
// Hosted (remote node) PAT login: the user pastes their OWN source-control PAT, which the
|
|
131
131
|
// server resolves to an account and holds to its login/org/domain allowlist. The available
|
|
132
|
-
// providers come from the server (`auth.patProviders`)
|
|
133
|
-
//
|
|
132
|
+
// providers come from the server (`auth.patProviders`) — GitHub always, GitLab when configured,
|
|
133
|
+
// on both hosted facades (Node + Worker); empty in local mode (which uses the configured-token
|
|
134
|
+
// flow above).
|
|
134
135
|
const remotePatProviders = computed<PatProvider[]>(() =>
|
|
135
136
|
isLocalMode.value ? [] : (auth.patProviders as PatProvider[]),
|
|
136
137
|
)
|
|
@@ -16,6 +16,7 @@ import { DOC_KINDS } from '~/types/domain'
|
|
|
16
16
|
import ContextDocumentPicker from '~/components/documents/ContextDocumentPicker.vue'
|
|
17
17
|
import ContextIssuePicker from '~/components/tasks/ContextIssuePicker.vue'
|
|
18
18
|
import { mergePresetOptionLabel, mergePresetThresholds } from '~/utils/mergePreset'
|
|
19
|
+
import { pipelineAllowedForFrame } from '~/utils/pipeline'
|
|
19
20
|
|
|
20
21
|
const ui = useUiStore()
|
|
21
22
|
const board = useBoardStore()
|
|
@@ -195,6 +196,11 @@ const selectedModelPresetLabel = computed(() => {
|
|
|
195
196
|
)
|
|
196
197
|
})
|
|
197
198
|
|
|
199
|
+
// Hide UI-testing pipelines (`tester-ui` / `visual-confirmation`) when the target frame has no
|
|
200
|
+
// UI to exercise — they'd be refused server-side (see utils/pipeline + the backend gate).
|
|
201
|
+
const selectablePipelines = computed(() =>
|
|
202
|
+
pipelines.pipelines.filter((p) => pipelineAllowedForFrame(p, frame.value, board.blocks)),
|
|
203
|
+
)
|
|
198
204
|
const pipelineMenu = computed(() => [
|
|
199
205
|
[
|
|
200
206
|
{
|
|
@@ -202,7 +208,7 @@ const pipelineMenu = computed(() => [
|
|
|
202
208
|
icon: 'i-lucide-rotate-ccw',
|
|
203
209
|
onSelect: () => (pipelineId.value = ''),
|
|
204
210
|
},
|
|
205
|
-
...
|
|
211
|
+
...selectablePipelines.value.map((p) => ({
|
|
206
212
|
label: p.name,
|
|
207
213
|
icon: 'i-lucide-workflow',
|
|
208
214
|
onSelect: () => (pipelineId.value = p.id),
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// pipeline is picked, the workspace issue-tracker choice is surfaced inline (it is
|
|
7
7
|
// where that pipeline files its ticket) and saved alongside.
|
|
8
8
|
import type { Recurrence, ScheduleTemplate } from '~/types/recurring'
|
|
9
|
+
import { pipelineAllowedForFrame } from '~/utils/pipeline'
|
|
9
10
|
|
|
10
11
|
const ui = useUiStore()
|
|
11
12
|
const board = useBoardStore()
|
|
@@ -47,8 +48,12 @@ function defaultRecurrence(): Recurrence {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
// Hide UI-testing pipelines when the frame has no UI to exercise — they'd be refused at run start.
|
|
52
|
+
const selectablePipelines = computed(() =>
|
|
53
|
+
pipelines.pipelines.filter((p) => pipelineAllowedForFrame(p, frame.value, board.blocks)),
|
|
54
|
+
)
|
|
50
55
|
const pipelineMenu = computed(() => [
|
|
51
|
-
|
|
56
|
+
selectablePipelines.value.map((p) => ({
|
|
52
57
|
label: p.name,
|
|
53
58
|
icon: 'i-lucide-workflow',
|
|
54
59
|
onSelect: () => (pipelineId.value = p.id),
|
|
@@ -15,6 +15,7 @@ const agentRuns = useAgentRunsStore()
|
|
|
15
15
|
const github = useGitHubStore()
|
|
16
16
|
const toast = useToast()
|
|
17
17
|
const { t } = useI18n()
|
|
18
|
+
const { confirmAction, toastDone } = useConfirmAction()
|
|
18
19
|
|
|
19
20
|
const open = computed({
|
|
20
21
|
get: () => ui.bootstrapOpen,
|
|
@@ -349,9 +350,11 @@ async function saveArch() {
|
|
|
349
350
|
}
|
|
350
351
|
|
|
351
352
|
async function removeArch(a: ReferenceArchitecture) {
|
|
353
|
+
if (!(await confirmAction('remove', a.name))) return
|
|
352
354
|
try {
|
|
353
355
|
await bootstrap.deleteArchitecture(a.id)
|
|
354
356
|
if (selectedArchId.value === a.id) selectedArchId.value = undefined
|
|
357
|
+
toastDone('remove', a.name)
|
|
355
358
|
} catch (e) {
|
|
356
359
|
toast.add({
|
|
357
360
|
title: t('bootstrap.toast.deleteFailed'),
|
|
@@ -10,6 +10,7 @@ const { t } = useI18n()
|
|
|
10
10
|
const ui = useUiStore()
|
|
11
11
|
const documents = useDocumentsStore()
|
|
12
12
|
const toast = useToast()
|
|
13
|
+
const { confirmAction } = useConfirmAction()
|
|
13
14
|
|
|
14
15
|
const source = computed(() => ui.documentConnect?.source ?? null)
|
|
15
16
|
const descriptor = computed(() =>
|
|
@@ -70,11 +71,11 @@ async function submit() {
|
|
|
70
71
|
|
|
71
72
|
async function disconnect() {
|
|
72
73
|
if (!source.value) return
|
|
74
|
+
const label = descriptor.value?.label ?? t('documents.connect.sourceFallback')
|
|
75
|
+
if (!(await confirmAction('disconnect', label))) return
|
|
73
76
|
await documents.disconnect(source.value)
|
|
74
77
|
toast.add({
|
|
75
|
-
title: t('documents.connect.disconnected', {
|
|
76
|
-
source: descriptor.value?.label ?? t('documents.connect.sourceFallback'),
|
|
77
|
-
}),
|
|
78
|
+
title: t('documents.connect.disconnected', { source: label }),
|
|
78
79
|
icon: 'i-lucide-unplug',
|
|
79
80
|
})
|
|
80
81
|
ui.closeDocumentConnect()
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { onKeyStroke } from '@vueuse/core'
|
|
3
3
|
import type { Block } from '~/types/domain'
|
|
4
4
|
import { blockTypeMeta, STATUS_META } from '~/utils/catalog'
|
|
5
|
+
import { pipelineAllowedForFrame } from '~/utils/pipeline'
|
|
5
6
|
import PipelineProgress from '~/components/pipeline/PipelineProgress.vue'
|
|
6
7
|
|
|
7
8
|
const board = useBoardStore()
|
|
@@ -26,13 +27,17 @@ const deps = computed(() =>
|
|
|
26
27
|
(block.value?.dependsOn ?? []).map((id) => board.getBlock(id)).filter((b): b is Block => !!b),
|
|
27
28
|
)
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
// Hide UI-testing pipelines when this block's frame has no UI to exercise (see the backend gate).
|
|
31
|
+
const runMenu = computed(() => {
|
|
32
|
+
const frame = block.value ? board.serviceOf(block.value) : undefined
|
|
33
|
+
return pipelines.pipelines
|
|
34
|
+
.filter((p) => pipelineAllowedForFrame(p, frame, board.blocks))
|
|
35
|
+
.map((p) => ({
|
|
36
|
+
label: p.name,
|
|
37
|
+
icon: 'i-lucide-play',
|
|
38
|
+
onSelect: () => block.value && execution.start(block.value.id, p),
|
|
39
|
+
}))
|
|
40
|
+
})
|
|
36
41
|
|
|
37
42
|
function close() {
|
|
38
43
|
ui.focus(null)
|
|
@@ -19,6 +19,8 @@ const board = useBoardStore()
|
|
|
19
19
|
const execution = useExecutionStore()
|
|
20
20
|
const humanTest = useHumanTestStore()
|
|
21
21
|
const { t, d } = useI18n()
|
|
22
|
+
const toast = useToast()
|
|
23
|
+
const { confirmAction, toastDone } = useConfirmAction()
|
|
22
24
|
|
|
23
25
|
// Shared seam contract (open/blockId/close + Escape). No `onOpen` loader: the gate state
|
|
24
26
|
// rides on the execution step, pushed over the stream.
|
|
@@ -102,7 +104,19 @@ async function recreate() {
|
|
|
102
104
|
}
|
|
103
105
|
async function destroy() {
|
|
104
106
|
if (!blockId.value) return
|
|
105
|
-
|
|
107
|
+
const noun = t('humanTest.envNoun')
|
|
108
|
+
if (!(await confirmAction('destroy', noun))) return
|
|
109
|
+
try {
|
|
110
|
+
await humanTest.destroyEnv(blockId.value)
|
|
111
|
+
toastDone('destroy', noun)
|
|
112
|
+
} catch (e) {
|
|
113
|
+
toast.add({
|
|
114
|
+
title: t('humanTest.destroyFailed'),
|
|
115
|
+
description: e instanceof Error ? e.message : String(e),
|
|
116
|
+
icon: 'i-lucide-triangle-alert',
|
|
117
|
+
color: 'error',
|
|
118
|
+
})
|
|
119
|
+
}
|
|
106
120
|
}
|
|
107
121
|
|
|
108
122
|
/** Env actions need a provider (an env is/was present, or it's provisioning) — disabled in degraded mode. */
|
|
@@ -14,6 +14,7 @@ const store = useAccountSettingsStore()
|
|
|
14
14
|
const ui = useUiStore()
|
|
15
15
|
const toast = useToast()
|
|
16
16
|
const { t } = useI18n()
|
|
17
|
+
const { confirmAction } = useConfirmAction()
|
|
17
18
|
|
|
18
19
|
// Deep-link anchor: the pipeline-start "configure storage" prompt opens this tab with the
|
|
19
20
|
// ui store's scroll target set to `content-storage`, so we bring the storage section (which
|
|
@@ -207,6 +208,7 @@ async function saveSlack() {
|
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
async function clearSlack() {
|
|
211
|
+
if (!(await confirmAction('clear', 'Slack'))) return
|
|
210
212
|
savingSlack.value = true
|
|
211
213
|
try {
|
|
212
214
|
await store.save(props.accountId, { secrets: { slackOAuth: null } })
|
|
@@ -262,6 +264,7 @@ async function saveLinear() {
|
|
|
262
264
|
}
|
|
263
265
|
|
|
264
266
|
async function clearLinear() {
|
|
267
|
+
if (!(await confirmAction('clear', 'Linear'))) return
|
|
265
268
|
savingLinear.value = true
|
|
266
269
|
try {
|
|
267
270
|
await store.save(props.accountId, { secrets: { linearOAuth: null } })
|
|
@@ -319,6 +322,7 @@ async function saveWeb() {
|
|
|
319
322
|
}
|
|
320
323
|
|
|
321
324
|
async function clearWeb() {
|
|
325
|
+
if (!(await confirmAction('clear', t('layout.accountDeployment.web.title')))) return
|
|
322
326
|
savingWeb.value = true
|
|
323
327
|
try {
|
|
324
328
|
await store.save(props.accountId, { secrets: { webSearch: null } })
|
|
@@ -14,6 +14,7 @@ const props = defineProps<{ accountId: string }>()
|
|
|
14
14
|
const accounts = useAccountsStore()
|
|
15
15
|
const toast = useToast()
|
|
16
16
|
const { t, te } = useI18n()
|
|
17
|
+
const { confirmAction, toastDone } = useConfirmAction()
|
|
17
18
|
const busy = ref(false)
|
|
18
19
|
|
|
19
20
|
const ROLE_ITEMS = computed<{ label: string; value: AccountRole }[]>(() => [
|
|
@@ -132,9 +133,11 @@ async function sendInvite() {
|
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
async function revoke(id: string) {
|
|
136
|
+
async function revoke(id: string, email: string) {
|
|
137
|
+
if (!(await confirmAction('revoke', email))) return
|
|
136
138
|
try {
|
|
137
139
|
await accounts.revokeInvite(props.accountId, id)
|
|
140
|
+
toastDone('revoke', email)
|
|
138
141
|
} catch (e) {
|
|
139
142
|
notifyError(t('layout.accountTeam.errors.revokeInvite'), e)
|
|
140
143
|
}
|
|
@@ -164,9 +167,12 @@ async function connectEmail() {
|
|
|
164
167
|
}
|
|
165
168
|
|
|
166
169
|
async function disconnectEmail() {
|
|
170
|
+
const noun = t('layout.accountTeam.emailNoun')
|
|
171
|
+
if (!(await confirmAction('disconnect', noun))) return
|
|
167
172
|
busy.value = true
|
|
168
173
|
try {
|
|
169
174
|
await accounts.disconnectEmail(props.accountId)
|
|
175
|
+
toastDone('disconnect', noun)
|
|
170
176
|
} catch (e) {
|
|
171
177
|
notifyError(t('layout.accountTeam.errors.disconnectEmail'), e)
|
|
172
178
|
} finally {
|
|
@@ -258,7 +264,7 @@ async function disconnectEmail() {
|
|
|
258
264
|
variant="ghost"
|
|
259
265
|
icon="i-lucide-x"
|
|
260
266
|
:aria-label="t('layout.accountTeam.invite.revoke')"
|
|
261
|
-
@click="revoke(inv.id)"
|
|
267
|
+
@click="revoke(inv.id, inv.email)"
|
|
262
268
|
/>
|
|
263
269
|
</span>
|
|
264
270
|
</li>
|
|
@@ -18,7 +18,9 @@ const show = computed(() => showSetup.value || showPreset.value)
|
|
|
18
18
|
|
|
19
19
|
<template>
|
|
20
20
|
<Transition name="fade">
|
|
21
|
-
|
|
21
|
+
<!-- Positioning/stacking is owned by the shared banner column in `pages/index.vue`; this
|
|
22
|
+
renders only its card and re-enables pointer events on it. -->
|
|
23
|
+
<div v-if="show" class="pointer-events-auto w-full max-w-3xl">
|
|
22
24
|
<!-- (1) No usable AI source -->
|
|
23
25
|
<div
|
|
24
26
|
v-if="showSetup"
|
|
@@ -6,6 +6,7 @@ const ui = useUiStore()
|
|
|
6
6
|
const board = useBoardStore()
|
|
7
7
|
const execution = useExecutionStore()
|
|
8
8
|
const workspace = useWorkspaceStore()
|
|
9
|
+
const workspaceSettings = useWorkspaceSettingsStore()
|
|
9
10
|
const services = useServicesStore()
|
|
10
11
|
const toast = useToast()
|
|
11
12
|
const { t, n } = useI18n()
|
|
@@ -54,9 +55,18 @@ const LOD_LABEL_KEYS = {
|
|
|
54
55
|
} as const
|
|
55
56
|
const lodLabel = computed(() => t(LOD_LABEL_KEYS[ui.lod]))
|
|
56
57
|
|
|
57
|
-
// Live spend indicator: shown once
|
|
58
|
+
// Live spend indicator: shown once the workspace has an explicit budget configured
|
|
59
|
+
// (so the meter appears the moment a budget is set, at zero spend), or once any tokens
|
|
60
|
+
// have been metered this period (the built-in default budget still applies unconfigured).
|
|
58
61
|
const spend = computed(() => workspace.spend)
|
|
59
|
-
const
|
|
62
|
+
const budgetConfigured = computed(
|
|
63
|
+
() =>
|
|
64
|
+
workspaceSettings.settings.spendMonthlyLimit != null ||
|
|
65
|
+
workspaceSettings.settings.spendCurrency != null,
|
|
66
|
+
)
|
|
67
|
+
const showSpend = computed(
|
|
68
|
+
() => !!spend.value && (budgetConfigured.value || spend.value.costSpent > 0),
|
|
69
|
+
)
|
|
60
70
|
const spendLabel = computed(() => {
|
|
61
71
|
const s = spend.value
|
|
62
72
|
if (!s) return ''
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Loud prompt that this deployment needs a piece of infrastructure the operator hasn't set up
|
|
3
|
+
// yet, so a whole class of agents can't run. Driven off the server-computed `infraSetup`
|
|
4
|
+
// snapshot projection (`not_defined` per area) — so it only fires on a runtime that actually
|
|
5
|
+
// requires the piece (the runner-pool executor matters on remote Node; binary storage on any
|
|
6
|
+
// runtime whose account picked no backend — incl. Cloudflare without an ARTIFACT_BUCKET binding;
|
|
7
|
+
// ephemeral test environments on any runtime that wires the integration).
|
|
8
|
+
//
|
|
9
|
+
// Positioning/stacking against the sibling advisory banners (AI-readiness, provider-config) is
|
|
10
|
+
// owned by the shared, click-through banner column in `pages/index.vue` — so concurrent prompts
|
|
11
|
+
// stack vertically instead of drawing on top of each other. This component only stacks its OWN
|
|
12
|
+
// (up to three) area cards; each card re-enables pointer events while the column stays inert.
|
|
13
|
+
//
|
|
14
|
+
// Dismissal offers the two choices the product asks for: hide for THIS SESSION (a ui-store flag,
|
|
15
|
+
// cleared on workspace switch, re-nags next load) or "I'm OK with the limitations, don't notify
|
|
16
|
+
// me again" — a PERMANENT, per-USER dismissal persisted in localStorage keyed by the signed-in
|
|
17
|
+
// user id (so it's this-user-only and survives reloads).
|
|
18
|
+
//
|
|
19
|
+
// Scope note: the permanent dismissal is per-USER and DEPLOYMENT-wide, not per-account. That is
|
|
20
|
+
// exact for `agentExecutor`/`ephemeralEnvironments` (deployment-level wiring). `binaryStorage` is
|
|
21
|
+
// per-account, so a user who permanently silences it on one account won't be re-nagged on another
|
|
22
|
+
// account that also has no storage — an accepted trade-off (the setting stays reachable from
|
|
23
|
+
// account settings, and the SESSION dismissal re-nags on the next load regardless).
|
|
24
|
+
//
|
|
25
|
+
// Freshness note: `infraSetup` is a server projection recomputed only on snapshot (re)load, so a
|
|
26
|
+
// banner clears on the next board load after the operator configures the area via the deep-link,
|
|
27
|
+
// not the instant the config panel saves.
|
|
28
|
+
import { useLocalStorage } from '@vueuse/core'
|
|
29
|
+
import { computed } from 'vue'
|
|
30
|
+
// The localStorage key holding the permanent per-user dismissals lives in `@cat-factory/contracts`
|
|
31
|
+
// (a dependency-free package the SPA and the e2e suite both import), so the key + shape can't drift
|
|
32
|
+
// between this component and the e2e seed in `backend/internal/e2e/tests/helpers.ts` (`pinWorkspace`).
|
|
33
|
+
import { INFRA_SETUP_DISMISSED_STORAGE_KEY } from '@cat-factory/contracts'
|
|
34
|
+
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
35
|
+
import type { InfraSetupArea } from '~/types/domain'
|
|
36
|
+
|
|
37
|
+
const { t } = useI18n()
|
|
38
|
+
const ui = useUiStore()
|
|
39
|
+
const auth = useAuthStore()
|
|
40
|
+
const workspace = useWorkspaceStore()
|
|
41
|
+
|
|
42
|
+
// Severity order: no executor blocks EVERY agent, so it leads; a missing test environment blocks
|
|
43
|
+
// only testing agents; missing storage only degrades the UI-tester's screenshots.
|
|
44
|
+
const AREAS: InfraSetupArea[] = ['agentExecutor', 'ephemeralEnvironments', 'binaryStorage']
|
|
45
|
+
|
|
46
|
+
// Exhaustive per-area presentation (an exhaustive `Record<InfraSetupArea, …>`, so adding an area
|
|
47
|
+
// without a meta entry fails typecheck — the tier-2 guard). The i18n keys are resolved
|
|
48
|
+
// dynamically (`t(AREA_META[area].titleKey)`), so tier-1 typed-key checking doesn't cover them;
|
|
49
|
+
// the `i18n:check` drift guard (tier 3) catches any that are absent from the catalog. `action`
|
|
50
|
+
// deep-links into the relevant setup surface.
|
|
51
|
+
const AREA_META: Record<
|
|
52
|
+
InfraSetupArea,
|
|
53
|
+
{ icon: string; titleKey: string; bodyKey: string; actionKey: string; onConfigure: () => void }
|
|
54
|
+
> = {
|
|
55
|
+
agentExecutor: {
|
|
56
|
+
icon: 'i-lucide-server-cog',
|
|
57
|
+
titleKey: 'layout.infraSetupBanner.agentExecutor.title',
|
|
58
|
+
bodyKey: 'layout.infraSetupBanner.agentExecutor.body',
|
|
59
|
+
actionKey: 'layout.infraSetupBanner.agentExecutor.action',
|
|
60
|
+
onConfigure: () => ui.openProviderConnection('runner-pool'),
|
|
61
|
+
},
|
|
62
|
+
ephemeralEnvironments: {
|
|
63
|
+
icon: 'i-lucide-flask-conical',
|
|
64
|
+
titleKey: 'layout.infraSetupBanner.ephemeralEnvironments.title',
|
|
65
|
+
bodyKey: 'layout.infraSetupBanner.ephemeralEnvironments.body',
|
|
66
|
+
actionKey: 'layout.infraSetupBanner.ephemeralEnvironments.action',
|
|
67
|
+
onConfigure: () => ui.openProviderConnection('environment'),
|
|
68
|
+
},
|
|
69
|
+
binaryStorage: {
|
|
70
|
+
icon: 'i-lucide-hard-drive',
|
|
71
|
+
titleKey: 'layout.infraSetupBanner.binaryStorage.title',
|
|
72
|
+
bodyKey: 'layout.infraSetupBanner.binaryStorage.body',
|
|
73
|
+
actionKey: 'layout.infraSetupBanner.binaryStorage.action',
|
|
74
|
+
onConfigure: () => ui.openContentStorageSettings(),
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Permanent, per-user dismissals: one shared localStorage record keyed BY user id (so it's
|
|
79
|
+
// scoped to the signed-in user and doesn't leak across accounts on a shared browser). No
|
|
80
|
+
// signed-in user (local/auth-off single-user mode) ⇒ the `local` bucket.
|
|
81
|
+
const permanentDismissed = useLocalStorage<Record<string, InfraSetupArea[]>>(
|
|
82
|
+
INFRA_SETUP_DISMISSED_STORAGE_KEY,
|
|
83
|
+
{},
|
|
84
|
+
)
|
|
85
|
+
const userKey = computed(() => auth.user?.id ?? 'local')
|
|
86
|
+
const dismissedForUser = computed(() => permanentDismissed.value[userKey.value] ?? [])
|
|
87
|
+
function dismissPermanently(area: InfraSetupArea) {
|
|
88
|
+
const current = permanentDismissed.value[userKey.value] ?? []
|
|
89
|
+
if (!current.includes(area)) {
|
|
90
|
+
permanentDismissed.value = {
|
|
91
|
+
...permanentDismissed.value,
|
|
92
|
+
[userKey.value]: [...current, area],
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const visible = computed<InfraSetupArea[]>(() => {
|
|
98
|
+
const status = workspace.infraSetup
|
|
99
|
+
if (!status) return []
|
|
100
|
+
return AREAS.filter(
|
|
101
|
+
(area) =>
|
|
102
|
+
status[area] === 'not_defined' &&
|
|
103
|
+
!ui.infraSetupSessionDismissed.includes(area) &&
|
|
104
|
+
!dismissedForUser.value.includes(area),
|
|
105
|
+
)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// The dismiss dropdown: the product wants the user asked WHICH kind of dismissal on close.
|
|
109
|
+
function dismissMenu(area: InfraSetupArea): DropdownMenuItem[][] {
|
|
110
|
+
return [
|
|
111
|
+
[
|
|
112
|
+
{
|
|
113
|
+
label: t('layout.infraSetupBanner.dismiss.session'),
|
|
114
|
+
icon: 'i-lucide-clock',
|
|
115
|
+
onSelect: () => ui.dismissInfraSetupForSession(area),
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
label: t('layout.infraSetupBanner.dismiss.permanent'),
|
|
119
|
+
icon: 'i-lucide-bell-off',
|
|
120
|
+
onSelect: () => dismissPermanently(area),
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
<template>
|
|
128
|
+
<Transition name="fade">
|
|
129
|
+
<!-- One polite live region for ALL area cards (plus the sibling AI/provider banners) rather
|
|
130
|
+
than an assertive `role="alert"` per card — an advisory setup nag shouldn't interrupt a
|
|
131
|
+
screen reader, and up to three stacked alerts would spam it. -->
|
|
132
|
+
<div
|
|
133
|
+
v-if="visible.length > 0"
|
|
134
|
+
class="flex w-full flex-col items-center gap-2"
|
|
135
|
+
role="status"
|
|
136
|
+
aria-live="polite"
|
|
137
|
+
>
|
|
138
|
+
<div
|
|
139
|
+
v-for="area in visible"
|
|
140
|
+
:key="area"
|
|
141
|
+
class="pointer-events-auto w-full max-w-3xl rounded-2xl border-2 border-amber-500/70 bg-amber-950/95 p-5 shadow-2xl backdrop-blur"
|
|
142
|
+
:data-testid="`infra-setup-banner-${area}`"
|
|
143
|
+
>
|
|
144
|
+
<div class="flex items-start gap-4">
|
|
145
|
+
<UIcon :name="AREA_META[area].icon" class="mt-0.5 h-9 w-9 shrink-0 text-amber-400" />
|
|
146
|
+
<div class="min-w-0 flex-1">
|
|
147
|
+
<div class="flex items-start justify-between gap-3">
|
|
148
|
+
<h2 class="text-lg font-semibold text-amber-100">
|
|
149
|
+
{{ t(AREA_META[area].titleKey) }}
|
|
150
|
+
</h2>
|
|
151
|
+
<UDropdownMenu :items="dismissMenu(area)" :content="{ align: 'end' }">
|
|
152
|
+
<UButton
|
|
153
|
+
color="neutral"
|
|
154
|
+
variant="ghost"
|
|
155
|
+
size="xs"
|
|
156
|
+
icon="i-lucide-x"
|
|
157
|
+
:aria-label="t('common.close')"
|
|
158
|
+
:data-testid="`infra-setup-dismiss-${area}`"
|
|
159
|
+
/>
|
|
160
|
+
</UDropdownMenu>
|
|
161
|
+
</div>
|
|
162
|
+
<p class="mt-1 text-sm text-amber-200/90">
|
|
163
|
+
{{ t(AREA_META[area].bodyKey) }}
|
|
164
|
+
</p>
|
|
165
|
+
<div class="mt-4">
|
|
166
|
+
<UButton
|
|
167
|
+
color="warning"
|
|
168
|
+
variant="solid"
|
|
169
|
+
icon="i-lucide-settings"
|
|
170
|
+
:data-testid="`infra-setup-configure-${area}`"
|
|
171
|
+
@click="AREA_META[area].onConfigure()"
|
|
172
|
+
>
|
|
173
|
+
{{ t(AREA_META[area].actionKey) }}
|
|
174
|
+
</UButton>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
</Transition>
|
|
181
|
+
</template>
|
|
182
|
+
|
|
183
|
+
<style scoped>
|
|
184
|
+
.fade-enter-active,
|
|
185
|
+
.fade-leave-active {
|
|
186
|
+
transition: opacity 0.2s ease;
|
|
187
|
+
}
|
|
188
|
+
.fade-enter-from,
|
|
189
|
+
.fade-leave-to {
|
|
190
|
+
opacity: 0;
|
|
191
|
+
}
|
|
192
|
+
</style>
|
|
@@ -38,7 +38,9 @@ const show = computed(() => pending.value.length > 0 && !dismissed.value)
|
|
|
38
38
|
|
|
39
39
|
<template>
|
|
40
40
|
<Transition name="fade">
|
|
41
|
-
|
|
41
|
+
<!-- Positioning/stacking is owned by the shared banner column in `pages/index.vue`; this
|
|
42
|
+
renders only its card and re-enables pointer events on it. -->
|
|
43
|
+
<div v-if="show" class="pointer-events-auto w-full max-w-3xl">
|
|
42
44
|
<div
|
|
43
45
|
class="w-full max-w-3xl rounded-2xl border-2 border-amber-500/70 bg-amber-950/95 p-5 shadow-2xl backdrop-blur"
|
|
44
46
|
role="alert"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { Block, BlockStatus } from '~/types/domain'
|
|
3
3
|
import { blockTypeMeta, STATUS_META } from '~/utils/catalog'
|
|
4
|
+
import { pipelineAllowedForFrame } from '~/utils/pipeline'
|
|
4
5
|
import TaskContextDocs from '~/components/documents/TaskContextDocs.vue'
|
|
5
6
|
import TaskContextIssues from '~/components/tasks/TaskContextIssues.vue'
|
|
6
7
|
import TaskAgentConfig from '~/components/panels/inspector/TaskAgentConfig.vue'
|
|
@@ -137,13 +138,18 @@ const taskBranchUrl = computed(() => {
|
|
|
137
138
|
return base ? `${base}/tree/${pr.branch}` : null
|
|
138
139
|
})
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
)
|
|
141
|
+
// Hide UI-testing pipelines when this block's frame has no UI to exercise — they'd be refused
|
|
142
|
+
// at run start (see utils/pipeline + the backend gate).
|
|
143
|
+
const runMenu = computed(() => {
|
|
144
|
+
const frame = block.value ? board.serviceOf(block.value) : undefined
|
|
145
|
+
return pipelines.pipelines
|
|
146
|
+
.filter((p) => pipelineAllowedForFrame(p, frame, board.blocks))
|
|
147
|
+
.map((p) => ({
|
|
148
|
+
label: p.name,
|
|
149
|
+
icon: 'i-lucide-play',
|
|
150
|
+
onSelect: () => block.value && execution.start(block.value.id, p),
|
|
151
|
+
}))
|
|
152
|
+
})
|
|
147
153
|
|
|
148
154
|
// Delegate to the shared confirm-gated deletion so the button and the keyboard shortcut
|
|
149
155
|
// (Delete/Backspace) follow the exact same prompt + optimistic-delete + rollback path.
|
|
@@ -18,8 +18,15 @@ import type {
|
|
|
18
18
|
const props = defineProps<{ block: Block }>()
|
|
19
19
|
|
|
20
20
|
const board = useBoardStore()
|
|
21
|
+
const auth = useAuthStore()
|
|
21
22
|
const { t } = useI18n()
|
|
22
23
|
|
|
24
|
+
// A browsable preview needs a long-lived host serve, so it is a local/node runtime capability
|
|
25
|
+
// the deployment advertises (`infrastructure.frontendPreview.supported`); the Worker reports it
|
|
26
|
+
// unsupported. Default to true until the auth handshake resolves so the toggle isn't briefly
|
|
27
|
+
// disabled on a runtime that does support it.
|
|
28
|
+
const previewSupported = computed(() => auth.infrastructure?.frontendPreview?.supported !== false)
|
|
29
|
+
|
|
23
30
|
const config = computed<FrontendConfig>(() => props.block.frontendConfig ?? { backendBindings: [] })
|
|
24
31
|
const bindings = computed(() => config.value.backendBindings ?? [])
|
|
25
32
|
|
|
@@ -226,7 +233,7 @@ function removeBinding(index: number) {
|
|
|
226
233
|
step="1"
|
|
227
234
|
size="xs"
|
|
228
235
|
class="font-mono"
|
|
229
|
-
placeholder="
|
|
236
|
+
placeholder="4173"
|
|
230
237
|
@blur="(e: FocusEvent) => saveServePort((e.target as HTMLInputElement).value)"
|
|
231
238
|
/>
|
|
232
239
|
</div>
|
|
@@ -247,6 +254,9 @@ function removeBinding(index: number) {
|
|
|
247
254
|
(e: KeyboardEvent) => saveText('mockMappingsPath', (e.target as HTMLInputElement).value)
|
|
248
255
|
"
|
|
249
256
|
/>
|
|
257
|
+
<p class="col-span-2 text-[11px] leading-snug text-slate-500">
|
|
258
|
+
{{ t('inspector.frontendConfig.mockMappingsHint') }}
|
|
259
|
+
</p>
|
|
250
260
|
</div>
|
|
251
261
|
</div>
|
|
252
262
|
|
|
@@ -331,18 +341,23 @@ function removeBinding(index: number) {
|
|
|
331
341
|
</div>
|
|
332
342
|
</div>
|
|
333
343
|
|
|
334
|
-
<!-- Browsable preview (local/node only). -->
|
|
344
|
+
<!-- Browsable preview (local/node only; the Worker reports it unsupported). -->
|
|
335
345
|
<div class="border-t border-slate-800 pt-2">
|
|
336
346
|
<UCheckbox
|
|
337
|
-
:model-value="config.previewEnabled === true"
|
|
347
|
+
:model-value="previewSupported && config.previewEnabled === true"
|
|
338
348
|
:label="t('inspector.frontendConfig.previewEnabled')"
|
|
349
|
+
:disabled="!previewSupported"
|
|
339
350
|
size="xs"
|
|
340
351
|
@update:model-value="
|
|
341
352
|
(v: boolean | 'indeterminate') => save({ previewEnabled: v === true ? true : undefined })
|
|
342
353
|
"
|
|
343
354
|
/>
|
|
344
355
|
<p class="mt-1 text-[11px] leading-snug text-slate-500">
|
|
345
|
-
{{
|
|
356
|
+
{{
|
|
357
|
+
previewSupported
|
|
358
|
+
? t('inspector.frontendConfig.previewHint')
|
|
359
|
+
: t('inspector.frontendConfig.previewUnsupported')
|
|
360
|
+
}}
|
|
346
361
|
</p>
|
|
347
362
|
</div>
|
|
348
363
|
</div>
|
|
@@ -12,6 +12,7 @@ const store = useReleaseHealthStore()
|
|
|
12
12
|
const ui = useUiStore()
|
|
13
13
|
const toast = useToast()
|
|
14
14
|
const { t } = useI18n()
|
|
15
|
+
const { confirmAction, toastDone } = useConfirmAction()
|
|
15
16
|
|
|
16
17
|
const busy = ref(false)
|
|
17
18
|
const draft = reactive({ monitorIds: '', sloIds: '', envTag: '' })
|
|
@@ -70,12 +71,15 @@ async function save() {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
async function clear() {
|
|
74
|
+
const noun = t('inspector.releaseHealth.configNoun')
|
|
75
|
+
if (!(await confirmAction('clear', noun))) return
|
|
73
76
|
busy.value = true
|
|
74
77
|
try {
|
|
75
78
|
await store.removeConfig(props.block.id)
|
|
76
79
|
draft.monitorIds = ''
|
|
77
80
|
draft.sloIds = ''
|
|
78
81
|
draft.envTag = ''
|
|
82
|
+
toastDone('clear', noun)
|
|
79
83
|
} catch (e) {
|
|
80
84
|
notifyError(t('inspector.releaseHealth.clearFailed'), e)
|
|
81
85
|
} finally {
|
|
@@ -3,6 +3,7 @@ import { computed, onMounted } from 'vue'
|
|
|
3
3
|
import type { Block } from '~/types/domain'
|
|
4
4
|
import type { WritebackOverride } from '~/types/tracker'
|
|
5
5
|
import { mergePresetOptionLabel, mergePresetThresholds } from '~/utils/mergePreset'
|
|
6
|
+
import { pipelineAllowedForFrame } from '~/utils/pipeline'
|
|
6
7
|
|
|
7
8
|
const props = defineProps<{ block: Block }>()
|
|
8
9
|
|
|
@@ -129,6 +130,12 @@ function setModelPreset(id: string) {
|
|
|
129
130
|
const selectedPipeline = computed(() =>
|
|
130
131
|
props.block.pipelineId ? pipelines.getPipeline(props.block.pipelineId) : undefined,
|
|
131
132
|
)
|
|
133
|
+
// Hide UI-testing pipelines when this task's frame has no UI to exercise — they'd be refused at
|
|
134
|
+
// run start (see utils/pipeline + the backend gate).
|
|
135
|
+
const taskFrame = computed(() => board.serviceOf(props.block))
|
|
136
|
+
const selectablePipelines = computed(() =>
|
|
137
|
+
pipelines.pipelines.filter((p) => pipelineAllowedForFrame(p, taskFrame.value, board.blocks)),
|
|
138
|
+
)
|
|
132
139
|
const pipelineMenu = computed(() => [
|
|
133
140
|
[
|
|
134
141
|
{
|
|
@@ -136,7 +143,7 @@ const pipelineMenu = computed(() => [
|
|
|
136
143
|
icon: 'i-lucide-rotate-ccw',
|
|
137
144
|
onSelect: () => setPipeline(''),
|
|
138
145
|
},
|
|
139
|
-
...
|
|
146
|
+
...selectablePipelines.value.map((p) => ({
|
|
140
147
|
label: p.name,
|
|
141
148
|
icon: 'i-lucide-workflow',
|
|
142
149
|
onSelect: () => setPipeline(p.id),
|