@cat-factory/app 0.70.0 → 0.71.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/app/components/auth/LoginScreen.vue +3 -2
- package/app/components/board/AddTaskModal.vue +7 -1
- package/app/components/board/AgentFailureCard.vue +3 -0
- package/app/components/board/RecurringPipelineModal.vue +6 -1
- package/app/components/board/nodes/BlockNode.vue +10 -2
- package/app/components/board/nodes/TaskCard.vue +12 -1
- package/app/components/bootstrap/BootstrapModal.vue +3 -0
- package/app/components/common/ConfirmDialog.vue +59 -0
- package/app/components/common/EmptyState.vue +35 -0
- package/app/components/common/KeyboardShortcutsHelp.vue +48 -0
- package/app/components/documents/ContextDocumentPicker.vue +8 -4
- package/app/components/documents/DocumentSourceConnectModal.vue +4 -3
- package/app/components/focus/BlockFocusView.vue +12 -7
- package/app/components/fragments/FragmentLibraryManager.vue +10 -0
- package/app/components/github/GitHubPanel.vue +9 -0
- 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/BoardSwitcher.vue +11 -0
- package/app/components/layout/CommandBar.vue +8 -0
- package/app/components/layout/InfraSetupBanner.vue +192 -0
- package/app/components/layout/NotificationsInbox.vue +11 -0
- package/app/components/layout/ProviderConfigBanner.vue +3 -1
- package/app/components/panels/InspectorPanel.vue +18 -20
- package/app/components/panels/StepContainerStatus.vue +37 -0
- package/app/components/panels/inspector/FrontendConfig.vue +19 -4
- package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +4 -0
- package/app/components/panels/inspector/TaskDependencies.vue +18 -4
- package/app/components/panels/inspector/TaskExecution.vue +24 -0
- package/app/components/panels/inspector/TaskRunSettings.vue +8 -1
- package/app/components/pipeline/PipelineBuilder.vue +13 -1
- package/app/components/providers/ApiKeysSection.vue +4 -0
- package/app/components/providers/PersonalSubscriptionSection.vue +9 -0
- package/app/components/providers/VendorCredentialsModal.vue +12 -3
- 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/LocalModelEndpointsPanel.vue +11 -0
- package/app/components/settings/MergeThresholdsPanel.vue +9 -0
- package/app/components/settings/ModelConfigurationPanel.vue +9 -0
- package/app/components/settings/ObservabilityConnectionPanel.vue +7 -0
- package/app/components/settings/ProviderConnectionTab.vue +2 -0
- package/app/components/settings/UserSecretsSection.vue +11 -0
- package/app/components/slack/SlackPanel.vue +9 -0
- package/app/components/tasks/ContextIssuePicker.vue +8 -4
- package/app/components/tasks/TaskSourceConnectModal.vue +4 -3
- package/app/composables/useBlockDeletion.ts +63 -0
- package/app/composables/useConfirm.ts +63 -0
- package/app/composables/useConfirmAction.ts +96 -0
- package/app/composables/useKeyboardShortcuts.ts +76 -0
- package/app/composables/usePipelineErrorToast.ts +2 -0
- package/app/composables/useWorkspaceStream.ts +26 -6
- package/app/pages/index.vue +28 -4
- package/app/stores/agentRuns.spec.ts +74 -0
- package/app/stores/agentRuns.ts +33 -5
- package/app/stores/ui.ts +33 -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 +174 -12
- package/i18n/locales/es.json +176 -17
- package/i18n/locales/fr.json +176 -17
- package/i18n/locales/he.json +176 -17
- package/i18n/locales/ja.json +176 -17
- package/i18n/locales/pl.json +176 -17
- package/i18n/locales/tr.json +176 -17
- package/i18n/locales/uk.json +176 -17
- 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),
|
|
@@ -53,6 +53,8 @@ async function retry() {
|
|
|
53
53
|
<div
|
|
54
54
|
class="nodrag rounded-lg border border-rose-900/60 bg-rose-950/40"
|
|
55
55
|
:class="compact ? 'px-3 py-2' : 'px-3 py-2.5'"
|
|
56
|
+
data-testid="agent-failure-banner"
|
|
57
|
+
:data-run-kind="run.kind"
|
|
56
58
|
>
|
|
57
59
|
<div class="flex items-center gap-1.5" :class="compact ? 'text-[11px]' : 'text-xs'">
|
|
58
60
|
<UIcon
|
|
@@ -95,6 +97,7 @@ async function retry() {
|
|
|
95
97
|
class="nodrag mt-2 flex items-center gap-1 rounded-md bg-rose-900/40 text-rose-200 hover:bg-rose-900/70 disabled:opacity-60"
|
|
96
98
|
:class="compact ? 'px-2 py-0.5 text-[10px]' : 'px-2 py-1 text-[11px]'"
|
|
97
99
|
:disabled="retrying"
|
|
100
|
+
data-testid="agent-failure-retry"
|
|
98
101
|
@click.stop="retry"
|
|
99
102
|
>
|
|
100
103
|
<UIcon
|
|
@@ -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),
|
|
@@ -256,7 +256,11 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
256
256
|
>
|
|
257
257
|
<div class="h-1.5 w-full" :style="{ backgroundColor: accent }" />
|
|
258
258
|
<!-- bootstrap-in-progress banner -->
|
|
259
|
-
<div
|
|
259
|
+
<div
|
|
260
|
+
v-if="bootstrapping"
|
|
261
|
+
class="border-b border-amber-900/50 bg-amber-950/30 px-3 py-2"
|
|
262
|
+
data-testid="bootstrap-progress"
|
|
263
|
+
>
|
|
260
264
|
<div class="flex items-center gap-1.5 text-[11px]">
|
|
261
265
|
<UIcon
|
|
262
266
|
name="i-lucide-loader-circle"
|
|
@@ -334,7 +338,11 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
334
338
|
>
|
|
335
339
|
<div class="h-1.5 w-full rounded-t-2xl" :style="{ backgroundColor: accent }" />
|
|
336
340
|
<!-- bootstrap-in-progress banner -->
|
|
337
|
-
<div
|
|
341
|
+
<div
|
|
342
|
+
v-if="bootstrapping"
|
|
343
|
+
class="border-b border-amber-900/50 bg-amber-950/30 px-4 py-2"
|
|
344
|
+
data-testid="bootstrap-progress"
|
|
345
|
+
>
|
|
338
346
|
<div class="flex items-center gap-1.5 text-xs">
|
|
339
347
|
<UIcon
|
|
340
348
|
name="i-lucide-loader-circle"
|
|
@@ -87,7 +87,18 @@ async function run() {
|
|
|
87
87
|
// the start was refused — the store surfaces the actionable toast itself). Revert the
|
|
88
88
|
// optimistic state; on success the button unmounts once the stream pushes in_progress.
|
|
89
89
|
const started = await execution.start(props.taskId, pipeline)
|
|
90
|
-
if (
|
|
90
|
+
if (started) {
|
|
91
|
+
// Confirm the (optimistic) start landed — the button unmounts once the stream pushes
|
|
92
|
+
// in_progress, so without this the successful action gives no feedback.
|
|
93
|
+
toast.add({
|
|
94
|
+
title: t('board.task.startedToast.title'),
|
|
95
|
+
description: t('board.task.startedToast.body', { name: pipeline.name }),
|
|
96
|
+
color: 'success',
|
|
97
|
+
icon: 'i-lucide-play',
|
|
98
|
+
})
|
|
99
|
+
} else {
|
|
100
|
+
starting.value = false
|
|
101
|
+
}
|
|
91
102
|
}
|
|
92
103
|
|
|
93
104
|
function review() {
|
|
@@ -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'),
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The single, app-wide confirmation dialog. Mounted once in `pages/index.vue` and driven
|
|
3
|
+
// entirely by the `useConfirm()` singleton, so any caller can `await confirm({...})` without
|
|
4
|
+
// rendering its own modal. UModal already provides the focus trap, Escape-to-close and
|
|
5
|
+
// backdrop, so this component adds none of that — it only resolves the pending promise and,
|
|
6
|
+
// crucially, resolves `false` whenever the modal closes without an explicit choice.
|
|
7
|
+
|
|
8
|
+
const { t } = useI18n()
|
|
9
|
+
const { open, current, accept, cancel, dismissed } = useConfirm()
|
|
10
|
+
|
|
11
|
+
const model = computed({
|
|
12
|
+
get: () => open.value,
|
|
13
|
+
set: (v: boolean) => {
|
|
14
|
+
// The user dismissed via backdrop / Escape — treat as cancel so the promise settles.
|
|
15
|
+
if (!v) dismissed()
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const isDestructive = computed(() => current.value?.variant === 'destructive')
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<UModal
|
|
24
|
+
v-model:open="model"
|
|
25
|
+
:title="current?.title ?? t('common.confirm.defaultTitle')"
|
|
26
|
+
:ui="{ content: 'max-w-md' }"
|
|
27
|
+
>
|
|
28
|
+
<template #body>
|
|
29
|
+
<div class="flex items-start gap-3" data-testid="confirm-dialog">
|
|
30
|
+
<div
|
|
31
|
+
v-if="current?.icon"
|
|
32
|
+
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg"
|
|
33
|
+
:class="isDestructive ? 'bg-red-500/10 text-red-400' : 'bg-slate-700/40 text-slate-300'"
|
|
34
|
+
>
|
|
35
|
+
<UIcon :name="current.icon" class="h-5 w-5" />
|
|
36
|
+
</div>
|
|
37
|
+
<p v-if="current?.description" class="text-sm text-slate-300">
|
|
38
|
+
{{ current.description }}
|
|
39
|
+
</p>
|
|
40
|
+
</div>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<template #footer>
|
|
44
|
+
<div class="flex w-full justify-end gap-2">
|
|
45
|
+
<UButton color="neutral" variant="ghost" data-testid="confirm-cancel" @click="cancel">
|
|
46
|
+
{{ current?.cancelLabel ?? t('common.cancel') }}
|
|
47
|
+
</UButton>
|
|
48
|
+
<UButton
|
|
49
|
+
:color="isDestructive ? 'error' : 'primary'"
|
|
50
|
+
data-testid="confirm-accept"
|
|
51
|
+
autofocus
|
|
52
|
+
@click="accept"
|
|
53
|
+
>
|
|
54
|
+
{{ current?.confirmLabel ?? t('common.confirm.confirm') }}
|
|
55
|
+
</UButton>
|
|
56
|
+
</div>
|
|
57
|
+
</template>
|
|
58
|
+
</UModal>
|
|
59
|
+
</template>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// A small, presentational "nothing here yet" placeholder — the reusable version of the
|
|
3
|
+
// hand-coded empty renders scattered across the app (context pickers, dependency lists,
|
|
4
|
+
// execution history, preset lists). Dumb by design: it takes already-resolved copy and an
|
|
5
|
+
// icon, and exposes a default slot for an optional call-to-action button. `compact` shrinks
|
|
6
|
+
// it for inline use inside a narrow panel section.
|
|
7
|
+
withDefaults(
|
|
8
|
+
defineProps<{
|
|
9
|
+
icon?: string
|
|
10
|
+
title: string
|
|
11
|
+
description?: string
|
|
12
|
+
compact?: boolean
|
|
13
|
+
}>(),
|
|
14
|
+
{ icon: 'i-lucide-inbox', compact: false },
|
|
15
|
+
)
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<div
|
|
20
|
+
class="flex flex-col items-center justify-center text-center text-slate-500"
|
|
21
|
+
:class="compact ? 'gap-1 py-4' : 'gap-2 py-10'"
|
|
22
|
+
data-testid="empty-state"
|
|
23
|
+
>
|
|
24
|
+
<UIcon :name="icon" :class="compact ? 'h-5 w-5' : 'h-8 w-8'" class="text-slate-600" />
|
|
25
|
+
<p :class="compact ? 'text-xs' : 'text-sm'" class="font-medium text-slate-400">
|
|
26
|
+
{{ title }}
|
|
27
|
+
</p>
|
|
28
|
+
<p v-if="description" :class="compact ? 'text-[11px]' : 'text-xs'" class="max-w-xs">
|
|
29
|
+
{{ description }}
|
|
30
|
+
</p>
|
|
31
|
+
<div v-if="$slots.default" :class="compact ? 'mt-1' : 'mt-2'">
|
|
32
|
+
<slot />
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The keyboard-shortcuts cheatsheet, opened with "?" (or from the command bar). A plain
|
|
3
|
+
// UModal listing every global shortcut with its keys rendered as UKbd chips. Driven by the
|
|
4
|
+
// ui store so "?" anywhere toggles it.
|
|
5
|
+
const { t } = useI18n()
|
|
6
|
+
const ui = useUiStore()
|
|
7
|
+
|
|
8
|
+
const open = computed({
|
|
9
|
+
get: () => ui.shortcutsHelpOpen,
|
|
10
|
+
set: (v: boolean) => (v ? ui.openShortcutsHelp() : ui.closeShortcutsHelp()),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
// Each row: the display keys + the action label. `mod` renders as ⌘ on Apple, Ctrl elsewhere.
|
|
14
|
+
// `navigator.platform` is deprecated; prefer the userAgentData hint, fall back to userAgent.
|
|
15
|
+
const nav = typeof navigator !== 'undefined' ? navigator : undefined
|
|
16
|
+
const platformHint =
|
|
17
|
+
(nav as { userAgentData?: { platform?: string } } | undefined)?.userAgentData?.platform ??
|
|
18
|
+
nav?.userAgent ??
|
|
19
|
+
''
|
|
20
|
+
const isApple = /Mac|iPhone|iPad/.test(platformHint)
|
|
21
|
+
const modKey = isApple ? '⌘' : 'Ctrl'
|
|
22
|
+
|
|
23
|
+
const shortcuts = computed(() => [
|
|
24
|
+
{ keys: [modKey, 'K'], label: t('layout.shortcuts.commandBar') },
|
|
25
|
+
{ keys: ['Esc'], label: t('layout.shortcuts.deselect') },
|
|
26
|
+
{ keys: ['Del'], label: t('layout.shortcuts.deleteBlock') },
|
|
27
|
+
{ keys: ['?'], label: t('layout.shortcuts.help') },
|
|
28
|
+
])
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<UModal v-model:open="open" :title="t('layout.shortcuts.title')" :ui="{ content: 'max-w-md' }">
|
|
33
|
+
<template #body>
|
|
34
|
+
<dl class="space-y-2" data-testid="shortcuts-help">
|
|
35
|
+
<div
|
|
36
|
+
v-for="(s, i) in shortcuts"
|
|
37
|
+
:key="i"
|
|
38
|
+
class="flex items-center justify-between gap-4 rounded-md px-1 py-1.5"
|
|
39
|
+
>
|
|
40
|
+
<dt class="text-sm text-slate-300">{{ s.label }}</dt>
|
|
41
|
+
<dd class="flex shrink-0 items-center gap-1">
|
|
42
|
+
<UKbd v-for="k in s.keys" :key="k" :value="k" />
|
|
43
|
+
</dd>
|
|
44
|
+
</div>
|
|
45
|
+
</dl>
|
|
46
|
+
</template>
|
|
47
|
+
</UModal>
|
|
48
|
+
</template>
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// useContextLinking). A search hit / pasted ref carries `needsImport: true` so
|
|
10
10
|
// it's fetched + persisted before linking. Mirrors ContextIssuePicker.
|
|
11
11
|
import type { DocumentSearchResult, DocumentSourceKind } from '~/types/domain'
|
|
12
|
+
import EmptyState from '~/components/common/EmptyState.vue'
|
|
12
13
|
|
|
13
14
|
const props = defineProps<{
|
|
14
15
|
/** contextKeys already staged by the caller, so they're filtered out / not re-offered. */
|
|
@@ -238,15 +239,18 @@ onMounted(() => {
|
|
|
238
239
|
</span>
|
|
239
240
|
</button>
|
|
240
241
|
|
|
241
|
-
<
|
|
242
|
-
|
|
242
|
+
<EmptyState
|
|
243
|
+
v-if="empty"
|
|
244
|
+
compact
|
|
245
|
+
icon="i-lucide-file-search"
|
|
246
|
+
:title="
|
|
243
247
|
query.trim()
|
|
244
248
|
? t('documents.picker.noMatches')
|
|
245
249
|
: searchable
|
|
246
250
|
? t('documents.picker.emptySearchable')
|
|
247
251
|
: t('documents.picker.emptyRefOnly')
|
|
248
|
-
|
|
249
|
-
|
|
252
|
+
"
|
|
253
|
+
/>
|
|
250
254
|
</div>
|
|
251
255
|
</div>
|
|
252
256
|
</template>
|
|
@@ -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)
|
|
@@ -31,6 +31,7 @@ const library =
|
|
|
31
31
|
const documents = useDocumentsStore()
|
|
32
32
|
const toast = useToast()
|
|
33
33
|
const { t, d } = useI18n()
|
|
34
|
+
const { confirm } = useConfirm()
|
|
34
35
|
|
|
35
36
|
const isWorkspace = props.kind === 'workspace'
|
|
36
37
|
/** Linking a document at the account scope needs a workspace connection to fetch through. */
|
|
@@ -121,6 +122,15 @@ async function createFragment() {
|
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
async function removeFragment(id: string) {
|
|
125
|
+
const name = library.fragments.find((f) => f.id === id)?.title ?? ''
|
|
126
|
+
const ok = await confirm({
|
|
127
|
+
title: t('fragments.confirmRemove.title'),
|
|
128
|
+
description: t('fragments.confirmRemove.body', { name }),
|
|
129
|
+
variant: 'destructive',
|
|
130
|
+
confirmLabel: t('common.delete'),
|
|
131
|
+
icon: 'i-lucide-trash-2',
|
|
132
|
+
})
|
|
133
|
+
if (!ok) return
|
|
124
134
|
try {
|
|
125
135
|
await library.remove(id)
|
|
126
136
|
toast.add({ title: t('fragments.toast.removed'), icon: 'i-lucide-trash-2' })
|
|
@@ -16,6 +16,7 @@ const { t } = useI18n()
|
|
|
16
16
|
const ui = useUiStore()
|
|
17
17
|
const github = useGitHubStore()
|
|
18
18
|
const toast = useToast()
|
|
19
|
+
const { confirm } = useConfirm()
|
|
19
20
|
|
|
20
21
|
const open = computed({
|
|
21
22
|
get: () => ui.githubOpen,
|
|
@@ -46,6 +47,14 @@ function notifyError(title: string, e: unknown) {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
async function disconnect() {
|
|
50
|
+
const ok = await confirm({
|
|
51
|
+
title: t('github.panel.confirmDisconnect.title'),
|
|
52
|
+
description: t('github.panel.confirmDisconnect.body'),
|
|
53
|
+
variant: 'destructive',
|
|
54
|
+
confirmLabel: t('common.disconnect'),
|
|
55
|
+
icon: 'i-lucide-unplug',
|
|
56
|
+
})
|
|
57
|
+
if (!ok) return
|
|
49
58
|
try {
|
|
50
59
|
await github.disconnect()
|
|
51
60
|
toast.add({ title: t('github.panel.toast.disconnected'), icon: 'i-lucide-unplug' })
|
|
@@ -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"
|
|
@@ -12,6 +12,7 @@ const accounts = useAccountsStore()
|
|
|
12
12
|
const workspace = useWorkspaceStore()
|
|
13
13
|
const ui = useUiStore()
|
|
14
14
|
const toast = useToast()
|
|
15
|
+
const { confirm } = useConfirm()
|
|
15
16
|
|
|
16
17
|
const busy = ref(false)
|
|
17
18
|
|
|
@@ -142,6 +143,16 @@ async function switchBoard(id: string) {
|
|
|
142
143
|
async function removeBoard() {
|
|
143
144
|
const id = workspace.workspaceId
|
|
144
145
|
if (!id) return
|
|
146
|
+
const ok = await confirm({
|
|
147
|
+
title: t('layout.boardSwitcher.confirmDelete.title'),
|
|
148
|
+
description: t('layout.boardSwitcher.confirmDelete.body', {
|
|
149
|
+
name: workspace.activeWorkspace?.name ?? t('layout.boardSwitcher.boardFallback'),
|
|
150
|
+
}),
|
|
151
|
+
variant: 'destructive',
|
|
152
|
+
confirmLabel: t('common.delete'),
|
|
153
|
+
icon: 'i-lucide-trash-2',
|
|
154
|
+
})
|
|
155
|
+
if (!ok) return
|
|
145
156
|
busy.value = true
|
|
146
157
|
try {
|
|
147
158
|
await workspace.remove(id)
|
|
@@ -212,6 +212,14 @@ const commands = computed<Command[]>(() => {
|
|
|
212
212
|
keywords: t('layout.commandBar.keywords.sandbox'),
|
|
213
213
|
run: () => ui.openSandbox(),
|
|
214
214
|
})
|
|
215
|
+
list.push({
|
|
216
|
+
id: 'keyboard-shortcuts',
|
|
217
|
+
label: t('layout.commandBar.cmd.shortcuts'),
|
|
218
|
+
group: groupWorkspace,
|
|
219
|
+
icon: 'i-lucide-keyboard',
|
|
220
|
+
keywords: t('layout.commandBar.keywords.shortcuts'),
|
|
221
|
+
run: () => ui.openShortcutsHelp(),
|
|
222
|
+
})
|
|
215
223
|
|
|
216
224
|
return list
|
|
217
225
|
})
|