@cat-factory/app 0.70.0 → 0.70.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/board/AgentFailureCard.vue +3 -0
- package/app/components/board/nodes/BlockNode.vue +10 -2
- package/app/components/board/nodes/TaskCard.vue +12 -1
- 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/fragments/FragmentLibraryManager.vue +10 -0
- package/app/components/github/GitHubPanel.vue +9 -0
- package/app/components/layout/BoardSwitcher.vue +11 -0
- package/app/components/layout/CommandBar.vue +8 -0
- package/app/components/layout/NotificationsInbox.vue +11 -0
- package/app/components/panels/InspectorPanel.vue +5 -13
- package/app/components/panels/StepContainerStatus.vue +37 -0
- package/app/components/panels/inspector/TaskDependencies.vue +18 -4
- package/app/components/panels/inspector/TaskExecution.vue +24 -0
- package/app/components/pipeline/PipelineBuilder.vue +13 -1
- package/app/components/providers/PersonalSubscriptionSection.vue +9 -0
- package/app/components/providers/VendorCredentialsModal.vue +12 -3
- 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/UserSecretsSection.vue +11 -0
- package/app/components/slack/SlackPanel.vue +9 -0
- package/app/components/tasks/ContextIssuePicker.vue +8 -4
- package/app/composables/useBlockDeletion.ts +63 -0
- package/app/composables/useConfirm.ts +63 -0
- package/app/composables/useKeyboardShortcuts.ts +76 -0
- package/app/pages/index.vue +8 -0
- package/app/stores/agentRuns.spec.ts +56 -0
- package/app/stores/agentRuns.ts +22 -4
- package/app/stores/ui.ts +16 -0
- package/i18n/locales/en.json +103 -5
- package/i18n/locales/es.json +107 -9
- package/i18n/locales/fr.json +107 -9
- package/i18n/locales/he.json +107 -9
- package/i18n/locales/ja.json +107 -9
- package/i18n/locales/pl.json +107 -9
- package/i18n/locales/tr.json +107 -9
- package/i18n/locales/uk.json +107 -9
- package/package.json +1 -1
|
@@ -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
|
|
@@ -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() {
|
|
@@ -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>
|
|
@@ -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' })
|
|
@@ -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
|
})
|
|
@@ -12,6 +12,7 @@ const { t, te } = useI18n()
|
|
|
12
12
|
const notifications = useNotificationsStore()
|
|
13
13
|
const ui = useUiStore()
|
|
14
14
|
const execution = useExecutionStore()
|
|
15
|
+
const toast = useToast()
|
|
15
16
|
|
|
16
17
|
const busy = ref<string | null>(null)
|
|
17
18
|
|
|
@@ -92,6 +93,11 @@ async function act(n: Notification) {
|
|
|
92
93
|
busy.value = n.id
|
|
93
94
|
try {
|
|
94
95
|
await notifications.act(n.id)
|
|
96
|
+
toast.add({
|
|
97
|
+
title: t('layout.notifications.toast.acted'),
|
|
98
|
+
color: 'success',
|
|
99
|
+
icon: 'i-lucide-check',
|
|
100
|
+
})
|
|
95
101
|
} finally {
|
|
96
102
|
busy.value = null
|
|
97
103
|
}
|
|
@@ -101,6 +107,11 @@ async function dismiss(n: Notification) {
|
|
|
101
107
|
busy.value = n.id
|
|
102
108
|
try {
|
|
103
109
|
await notifications.dismiss(n.id)
|
|
110
|
+
toast.add({
|
|
111
|
+
title: t('layout.notifications.toast.dismissed'),
|
|
112
|
+
color: 'neutral',
|
|
113
|
+
icon: 'i-lucide-check',
|
|
114
|
+
})
|
|
104
115
|
} finally {
|
|
105
116
|
busy.value = null
|
|
106
117
|
}
|
|
@@ -145,20 +145,11 @@ const runMenu = computed(() =>
|
|
|
145
145
|
})),
|
|
146
146
|
)
|
|
147
147
|
|
|
148
|
+
// Delegate to the shared confirm-gated deletion so the button and the keyboard shortcut
|
|
149
|
+
// (Delete/Backspace) follow the exact same prompt + optimistic-delete + rollback path.
|
|
150
|
+
const { deleteBlock } = useBlockDeletion()
|
|
148
151
|
function remove() {
|
|
149
|
-
|
|
150
|
-
const id = block.value.id
|
|
151
|
-
// Close the inspector right away; the stores hide the block optimistically and
|
|
152
|
-
// restore it (with a toast) only if the backend delete fails.
|
|
153
|
-
ui.select(null)
|
|
154
|
-
// A schedule owns its reused block + run history — removing the schedule deletes
|
|
155
|
-
// them server-side, so delete the schedule rather than orphaning it.
|
|
156
|
-
if (schedule.value) {
|
|
157
|
-
void recurring.remove(schedule.value.id)
|
|
158
|
-
return
|
|
159
|
-
}
|
|
160
|
-
execution.cancel(id)
|
|
161
|
-
void board.removeBlock(id)
|
|
152
|
+
void deleteBlock(block.value)
|
|
162
153
|
}
|
|
163
154
|
|
|
164
155
|
// ---- failed agent run (bootstrap or execution) ------------------------------
|
|
@@ -492,6 +483,7 @@ const showOriginalDescription = ref(false)
|
|
|
492
483
|
size="sm"
|
|
493
484
|
icon="i-lucide-trash-2"
|
|
494
485
|
class="ms-auto"
|
|
486
|
+
data-testid="inspector-delete"
|
|
495
487
|
:title="deleteLabel"
|
|
496
488
|
@click="remove"
|
|
497
489
|
>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue'
|
|
3
|
+
import { useClipboard } from '@vueuse/core'
|
|
3
4
|
import type { PipelineStep, RunContainerStatus } from '~/types/execution'
|
|
4
5
|
import { containerPhaseLabel } from '~/utils/pipelineRender'
|
|
5
6
|
|
|
@@ -61,6 +62,22 @@ const CONTAINER_STATUS_META: Record<
|
|
|
61
62
|
|
|
62
63
|
// The friendly phase label (clone → "Preparing workspace", …); only meaningful while up.
|
|
63
64
|
const phaseLabel = computed(() => containerPhaseLabel(props.step.container?.phase, { t, te }))
|
|
65
|
+
|
|
66
|
+
// Make the container id / URL one-click copyable (they're long and used to be
|
|
67
|
+
// select-and-copy-by-hand), with a toast confirming the copy landed.
|
|
68
|
+
const toast = useToast()
|
|
69
|
+
const { copy, isSupported } = useClipboard()
|
|
70
|
+
async function copyText(text: string) {
|
|
71
|
+
// Only claim success once the write actually landed — a failed/unsupported clipboard
|
|
72
|
+
// (insecure context, denied permission) must not show a misleading "Copied" toast.
|
|
73
|
+
try {
|
|
74
|
+
if (!isSupported.value) throw new Error('clipboard unsupported')
|
|
75
|
+
await copy(text)
|
|
76
|
+
toast.add({ title: t('common.copied'), color: 'success', icon: 'i-lucide-check' })
|
|
77
|
+
} catch {
|
|
78
|
+
toast.add({ title: t('common.copyFailed'), color: 'error', icon: 'i-lucide-x' })
|
|
79
|
+
}
|
|
80
|
+
}
|
|
64
81
|
</script>
|
|
65
82
|
|
|
66
83
|
<template>
|
|
@@ -94,6 +111,16 @@ const phaseLabel = computed(() => containerPhaseLabel(props.step.container?.phas
|
|
|
94
111
|
<dd class="truncate font-mono text-[11px] text-slate-300" :title="step.container.id">
|
|
95
112
|
{{ step.container.id }}
|
|
96
113
|
</dd>
|
|
114
|
+
<UButton
|
|
115
|
+
icon="i-lucide-copy"
|
|
116
|
+
color="neutral"
|
|
117
|
+
variant="ghost"
|
|
118
|
+
size="xs"
|
|
119
|
+
class="ms-auto shrink-0"
|
|
120
|
+
:title="t('panels.stepMeta.container.copyId')"
|
|
121
|
+
:aria-label="t('panels.stepMeta.container.copyId')"
|
|
122
|
+
@click="copyText(step.container.id)"
|
|
123
|
+
/>
|
|
97
124
|
</div>
|
|
98
125
|
<div v-if="step.container?.url" class="flex items-center gap-2">
|
|
99
126
|
<dt class="shrink-0 text-[11px] uppercase tracking-wide text-slate-500">
|
|
@@ -109,6 +136,16 @@ const phaseLabel = computed(() => containerPhaseLabel(props.step.container?.phas
|
|
|
109
136
|
{{ step.container.url }}
|
|
110
137
|
</a>
|
|
111
138
|
</dd>
|
|
139
|
+
<UButton
|
|
140
|
+
icon="i-lucide-copy"
|
|
141
|
+
color="neutral"
|
|
142
|
+
variant="ghost"
|
|
143
|
+
size="xs"
|
|
144
|
+
class="ms-auto shrink-0"
|
|
145
|
+
:title="t('panels.stepMeta.container.copyUrl')"
|
|
146
|
+
:aria-label="t('panels.stepMeta.container.copyUrl')"
|
|
147
|
+
@click="copyText(step.container.url)"
|
|
148
|
+
/>
|
|
112
149
|
</div>
|
|
113
150
|
</dl>
|
|
114
151
|
</div>
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { Block } from '~/types/domain'
|
|
3
|
+
import EmptyState from '~/components/common/EmptyState.vue'
|
|
3
4
|
|
|
4
5
|
const props = defineProps<{ block: Block }>()
|
|
5
6
|
|
|
6
7
|
const board = useBoardStore()
|
|
7
8
|
const { depLabel } = useDepLabels()
|
|
9
|
+
const { confirm } = useConfirm()
|
|
8
10
|
const { t } = useI18n()
|
|
9
11
|
|
|
10
12
|
const deps = computed(() =>
|
|
@@ -27,8 +29,15 @@ const depMenu = computed(() => {
|
|
|
27
29
|
}))
|
|
28
30
|
})
|
|
29
31
|
|
|
30
|
-
function removeDep(
|
|
31
|
-
|
|
32
|
+
async function removeDep(dep: Block) {
|
|
33
|
+
const ok = await confirm({
|
|
34
|
+
title: t('inspector.dependencies.confirmRemove.title'),
|
|
35
|
+
description: t('inspector.dependencies.confirmRemove.body', { name: label(dep) }),
|
|
36
|
+
variant: 'destructive',
|
|
37
|
+
confirmLabel: t('common.remove'),
|
|
38
|
+
icon: 'i-lucide-unlink',
|
|
39
|
+
})
|
|
40
|
+
if (ok) board.removeDependency(props.block.id, dep.id)
|
|
32
41
|
}
|
|
33
42
|
</script>
|
|
34
43
|
|
|
@@ -61,13 +70,18 @@ function removeDep(depId: string) {
|
|
|
61
70
|
? t('inspector.dependencies.merged')
|
|
62
71
|
: t('inspector.dependencies.notMerged')
|
|
63
72
|
"
|
|
64
|
-
@click="removeDep(d
|
|
73
|
+
@click="removeDep(d)"
|
|
65
74
|
>
|
|
66
75
|
{{ label(d) }}
|
|
67
76
|
<UIcon name="i-lucide-x" class="ms-0.5 h-3 w-3" />
|
|
68
77
|
</UBadge>
|
|
69
78
|
</div>
|
|
70
|
-
<
|
|
79
|
+
<EmptyState
|
|
80
|
+
v-else
|
|
81
|
+
compact
|
|
82
|
+
icon="i-lucide-git-branch"
|
|
83
|
+
:title="t('inspector.dependencies.empty')"
|
|
84
|
+
/>
|
|
71
85
|
<div v-if="!runnable" class="mt-1 text-[10px] text-amber-400">
|
|
72
86
|
{{ t('inspector.dependencies.blocked') }}
|
|
73
87
|
</div>
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
containerPhaseLabel,
|
|
9
9
|
} from '~/utils/pipelineRender'
|
|
10
10
|
import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
|
|
11
|
+
import EmptyState from '~/components/common/EmptyState.vue'
|
|
11
12
|
|
|
12
13
|
const props = defineProps<{ block: Block }>()
|
|
13
14
|
|
|
@@ -34,6 +35,16 @@ const reviewStageLabel = computed(() =>
|
|
|
34
35
|
)
|
|
35
36
|
|
|
36
37
|
const instance = computed(() => execution.getInstance(props.block.executionId))
|
|
38
|
+
|
|
39
|
+
// Nothing to show yet: no run, no failed run, no PR, and not awaiting a merge — render an
|
|
40
|
+
// empty state instead of a blank gap so the section reads as "no runs yet" rather than broken.
|
|
41
|
+
const isEmpty = computed(
|
|
42
|
+
() =>
|
|
43
|
+
!instance.value &&
|
|
44
|
+
!failedRun.value &&
|
|
45
|
+
!props.block.pullRequest &&
|
|
46
|
+
props.block.status !== 'pr_ready',
|
|
47
|
+
)
|
|
37
48
|
// A failed run is no longer executing: a step left mid-flight must stop showing
|
|
38
49
|
// its live "Spinning up…" phase (the shared failure banner renders below).
|
|
39
50
|
const runFailed = computed(() => instance.value?.status === 'failed')
|
|
@@ -184,6 +195,9 @@ async function resetRun() {
|
|
|
184
195
|
:key="i"
|
|
185
196
|
class="rounded-md px-2 py-1"
|
|
186
197
|
:class="i === instance.currentStep ? 'bg-slate-800/70' : ''"
|
|
198
|
+
data-testid="run-step"
|
|
199
|
+
:data-step-kind="s.agentKind"
|
|
200
|
+
:data-step-state="s.state"
|
|
187
201
|
>
|
|
188
202
|
<div class="flex items-center gap-2">
|
|
189
203
|
<!-- Every agent is clickable: it opens the step-detail overlay (timing,
|
|
@@ -221,6 +235,7 @@ async function resetRun() {
|
|
|
221
235
|
<span
|
|
222
236
|
v-if="s.subtasks && s.subtasks.total > 0"
|
|
223
237
|
class="ms-auto font-mono text-[10px] tabular-nums text-slate-300"
|
|
238
|
+
data-testid="run-subtasks"
|
|
224
239
|
:title="
|
|
225
240
|
s.subtasks.inProgress > 0
|
|
226
241
|
? t('inspector.execution.subtasksProgress', {
|
|
@@ -399,6 +414,15 @@ async function resetRun() {
|
|
|
399
414
|
</p>
|
|
400
415
|
</div>
|
|
401
416
|
|
|
417
|
+
<!-- No run yet: read as "nothing here" rather than a blank gap. -->
|
|
418
|
+
<EmptyState
|
|
419
|
+
v-if="isEmpty"
|
|
420
|
+
compact
|
|
421
|
+
icon="i-lucide-play-circle"
|
|
422
|
+
:title="t('inspector.execution.empty.title')"
|
|
423
|
+
:description="t('inspector.execution.empty.body')"
|
|
424
|
+
/>
|
|
425
|
+
|
|
402
426
|
<!-- PR ready: merge -->
|
|
403
427
|
<UButton
|
|
404
428
|
v-if="block.status === 'pr_ready'"
|
|
@@ -198,6 +198,18 @@ function edit(p: Pipeline) {
|
|
|
198
198
|
pipelines.loadForEdit(p)
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
const { confirm } = useConfirm()
|
|
202
|
+
async function removePipeline(p: Pipeline) {
|
|
203
|
+
const ok = await confirm({
|
|
204
|
+
title: t('pipeline.builder.confirmDeletePipeline.title'),
|
|
205
|
+
description: t('pipeline.builder.confirmDeletePipeline.body', { name: p.name }),
|
|
206
|
+
variant: 'destructive',
|
|
207
|
+
confirmLabel: t('common.delete'),
|
|
208
|
+
icon: 'i-lucide-trash-2',
|
|
209
|
+
})
|
|
210
|
+
if (ok) void pipelines.removePipeline(p.id)
|
|
211
|
+
}
|
|
212
|
+
|
|
201
213
|
/** Clone any pipeline (incl. a read-only built-in) into an editable copy, then edit it. */
|
|
202
214
|
async function clone(p: Pipeline) {
|
|
203
215
|
try {
|
|
@@ -776,7 +788,7 @@ async function clone(p: Pipeline) {
|
|
|
776
788
|
variant="ghost"
|
|
777
789
|
size="xs"
|
|
778
790
|
:title="t('pipeline.builder.delete')"
|
|
779
|
-
@click="
|
|
791
|
+
@click="removePipeline(p)"
|
|
780
792
|
/>
|
|
781
793
|
</div>
|
|
782
794
|
</div>
|
|
@@ -13,6 +13,7 @@ const workspace = useWorkspaceStore()
|
|
|
13
13
|
const models = useModelsStore()
|
|
14
14
|
const toast = useToast()
|
|
15
15
|
const { t, d } = useI18n()
|
|
16
|
+
const { confirm } = useConfirm()
|
|
16
17
|
|
|
17
18
|
// Personal subscriptions are stored per-user, so they need a signed-in user. When there
|
|
18
19
|
// isn't one (a deployment running without sign-in), block the form so the user doesn't
|
|
@@ -141,6 +142,14 @@ async function connect() {
|
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
async function disconnect(v: SubscriptionVendor) {
|
|
145
|
+
const ok = await confirm({
|
|
146
|
+
title: t('personalSubscriptions.confirmDisconnect.title'),
|
|
147
|
+
description: t('personalSubscriptions.confirmDisconnect.body', { vendor: vendorLabel(v) }),
|
|
148
|
+
variant: 'destructive',
|
|
149
|
+
confirmLabel: t('common.disconnect'),
|
|
150
|
+
icon: 'i-lucide-unplug',
|
|
151
|
+
})
|
|
152
|
+
if (!ok) return
|
|
144
153
|
try {
|
|
145
154
|
await personal.remove(v)
|
|
146
155
|
// Removing the subscription may drop the workspace's last usable model — refresh so the
|
|
@@ -14,6 +14,7 @@ const ui = useUiStore()
|
|
|
14
14
|
const workspace = useWorkspaceStore()
|
|
15
15
|
const creds = useVendorCredentialsStore()
|
|
16
16
|
const toast = useToast()
|
|
17
|
+
const { confirm } = useConfirm()
|
|
17
18
|
|
|
18
19
|
const open = computed({
|
|
19
20
|
get: () => ui.vendorCredentialsOpen,
|
|
@@ -139,9 +140,17 @@ async function add() {
|
|
|
139
140
|
}
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
async function remove(id: string) {
|
|
143
|
+
async function remove(cred: { id: string; label: string }) {
|
|
144
|
+
const ok = await confirm({
|
|
145
|
+
title: t('providers.vendorCredentials.confirmRemove.title'),
|
|
146
|
+
description: t('providers.vendorCredentials.confirmRemove.body', { name: cred.label }),
|
|
147
|
+
variant: 'destructive',
|
|
148
|
+
confirmLabel: t('common.remove'),
|
|
149
|
+
icon: 'i-lucide-trash-2',
|
|
150
|
+
})
|
|
151
|
+
if (!ok) return
|
|
143
152
|
try {
|
|
144
|
-
await creds.remove(id)
|
|
153
|
+
await creds.remove(cred.id)
|
|
145
154
|
} catch (e) {
|
|
146
155
|
toast.add({
|
|
147
156
|
title: t('providers.vendorCredentials.toast.removeFailed'),
|
|
@@ -258,7 +267,7 @@ function vendorLabel(v: SubscriptionVendor): string {
|
|
|
258
267
|
color="error"
|
|
259
268
|
variant="ghost"
|
|
260
269
|
size="xs"
|
|
261
|
-
@click="remove(c
|
|
270
|
+
@click="remove(c)"
|
|
262
271
|
/>
|
|
263
272
|
</div>
|
|
264
273
|
</div>
|