@cat-factory/app 0.87.1 → 0.87.3
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/FailureDetail.vue +12 -3
- package/app/components/clarity/ClarityReviewWindow.vue +4 -1
- package/app/components/common/CopyButton.vue +30 -0
- package/app/components/consensus/ConsensusSessionWindow.vue +12 -4
- package/app/components/gates/GateResultView.vue +15 -8
- package/app/components/panels/AgentStepDetail.vue +2 -1
- package/app/components/panels/StepContainerStatus.vue +1 -14
- package/app/components/panels/StepMetadataCard.vue +2 -1
- package/app/components/panels/StepRunMeta.vue +2 -1
- package/app/components/requirements/RequirementsReviewWindow.vue +4 -1
- package/app/components/settings/KubernetesEngineForm.vue +2 -1
- package/app/composables/useCopyToClipboard.ts +29 -0
- package/package.json +1 -1
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// message. Tone (summary/pre classes) is passed by the host so it blends into each surface
|
|
6
6
|
// (rose banner vs slate history) while the guard + the `showDetail` key + the whitespace-
|
|
7
7
|
// preserving `<pre>` structure live in one place.
|
|
8
|
+
import CopyButton from '~/components/common/CopyButton.vue'
|
|
9
|
+
|
|
8
10
|
defineProps<{
|
|
9
11
|
detail: string | null
|
|
10
12
|
message: string
|
|
@@ -20,8 +22,15 @@ const { t } = useI18n()
|
|
|
20
22
|
<summary class="cursor-pointer" :class="summaryClass">
|
|
21
23
|
{{ t('board.failure.showDetail') }}
|
|
22
24
|
</summary>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
<!-- The stack trace / extended detail: the first thing a user does with it is copy it, so
|
|
26
|
+
offer a copy affordance floated over the scroll box (UX-39). -->
|
|
27
|
+
<div class="relative mt-1">
|
|
28
|
+
<CopyButton :text="detail" class="absolute end-1 top-1 z-10" />
|
|
29
|
+
<pre
|
|
30
|
+
class="max-h-32 overflow-auto whitespace-pre-wrap rounded p-1.5 pe-9"
|
|
31
|
+
:class="preClass"
|
|
32
|
+
>{{ detail }}</pre
|
|
33
|
+
>
|
|
34
|
+
</div>
|
|
26
35
|
</details>
|
|
27
36
|
</template>
|
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
|
|
22
22
|
const board = useBoardStore()
|
|
23
23
|
const clarity = useClarityStore()
|
|
24
|
+
const models = useModelsStore()
|
|
24
25
|
const toast = useToast()
|
|
25
26
|
const { t } = useI18n()
|
|
26
27
|
|
|
@@ -493,7 +494,9 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
493
494
|
</div>
|
|
494
495
|
<div v-if="review.model" class="flex items-center justify-between">
|
|
495
496
|
<span>{{ t('clarity.rail.model') }}</span>
|
|
496
|
-
<span class="truncate ps-2 text-slate-500">{{
|
|
497
|
+
<span class="truncate ps-2 text-slate-500">{{
|
|
498
|
+
models.labelForRef(review.model) ?? review.model
|
|
499
|
+
}}</span>
|
|
497
500
|
</div>
|
|
498
501
|
</div>
|
|
499
502
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Standard icon-only "copy to clipboard" button with confirmation feedback. Routes through
|
|
3
|
+
// `useCopyToClipboard` so success/failure is always toasted (UX-38), and carries both a
|
|
4
|
+
// `title` and an `aria-label` so it's named for pointer tooltips and screen readers alike.
|
|
5
|
+
// Used to make error/detail surfaces copyable (UX-39) — the first thing a user does with a
|
|
6
|
+
// stack trace or failure summary is copy it.
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
/** The text to place on the clipboard. */
|
|
9
|
+
text: string
|
|
10
|
+
/** Accessible name + tooltip; defaults to the generic "Copy". */
|
|
11
|
+
label?: string
|
|
12
|
+
size?: 'xs' | 'sm' | 'md'
|
|
13
|
+
}>()
|
|
14
|
+
|
|
15
|
+
const { t } = useI18n()
|
|
16
|
+
const { copy } = useCopyToClipboard()
|
|
17
|
+
const label = computed(() => props.label ?? t('common.copy'))
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<UButton
|
|
22
|
+
icon="i-lucide-copy"
|
|
23
|
+
color="neutral"
|
|
24
|
+
variant="ghost"
|
|
25
|
+
:size="size ?? 'xs'"
|
|
26
|
+
:title="label"
|
|
27
|
+
:aria-label="label"
|
|
28
|
+
@click.stop="copy(text)"
|
|
29
|
+
/>
|
|
30
|
+
</template>
|
|
@@ -8,11 +8,14 @@
|
|
|
8
8
|
// as `consensus` stream events arrive.
|
|
9
9
|
import { computed } from 'vue'
|
|
10
10
|
import type { ConsensusContribution, ConsensusSession } from '~/types/consensus'
|
|
11
|
+
import CopyButton from '~/components/common/CopyButton.vue'
|
|
12
|
+
import { agentKindMeta } from '~/utils/catalog'
|
|
11
13
|
|
|
12
14
|
const { t, n } = useI18n()
|
|
13
15
|
|
|
14
16
|
const board = useBoardStore()
|
|
15
17
|
const consensus = useConsensusStore()
|
|
18
|
+
const models = useModelsStore()
|
|
16
19
|
|
|
17
20
|
const { open, blockId, close } = useResultView('consensus-session', {
|
|
18
21
|
onOpen: (id) => {
|
|
@@ -113,7 +116,7 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
|
|
|
113
116
|
<span v-if="block" class="font-normal text-slate-400">— {{ block.title }}</span>
|
|
114
117
|
</h2>
|
|
115
118
|
<p v-if="session" class="text-xs text-slate-500">
|
|
116
|
-
{{ session.agentKind }} ·
|
|
119
|
+
{{ agentKindMeta(session.agentKind).label }} ·
|
|
117
120
|
{{
|
|
118
121
|
t(
|
|
119
122
|
'consensus.participantCount',
|
|
@@ -149,9 +152,12 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
|
|
|
149
152
|
<!-- failure -->
|
|
150
153
|
<div
|
|
151
154
|
v-if="session.status === 'failed'"
|
|
152
|
-
class="mb-5 rounded-lg border border-rose-800/60 bg-rose-950/40 px-4 py-3 text-sm text-rose-200"
|
|
155
|
+
class="mb-5 flex items-start gap-2 rounded-lg border border-rose-800/60 bg-rose-950/40 px-4 py-3 text-sm text-rose-200"
|
|
153
156
|
>
|
|
154
|
-
|
|
157
|
+
<span class="min-w-0 flex-1">{{
|
|
158
|
+
t('consensus.failed', { error: session.error ?? t('consensus.unknownError') })
|
|
159
|
+
}}</span>
|
|
160
|
+
<CopyButton v-if="session.error" :text="session.error" class="-me-1 shrink-0" />
|
|
155
161
|
</div>
|
|
156
162
|
|
|
157
163
|
<!-- synthesized result -->
|
|
@@ -197,7 +203,9 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
|
|
|
197
203
|
t('consensus.expert', { letter: String.fromCharCode(65 + i) })
|
|
198
204
|
}}</span>
|
|
199
205
|
<span class="text-slate-400"> · {{ p.role }}</span>
|
|
200
|
-
<span v-if="p.modelId" class="ms-1 text-slate-500"
|
|
206
|
+
<span v-if="p.modelId" class="ms-1 text-slate-500"
|
|
207
|
+
>({{ models.labelForRef(p.modelId) ?? p.modelId }})</span
|
|
208
|
+
>
|
|
201
209
|
</div>
|
|
202
210
|
</div>
|
|
203
211
|
</section>
|
|
@@ -12,6 +12,7 @@ import StepRestartControl from '~/components/panels/StepRestartControl.vue'
|
|
|
12
12
|
import StepRunMeta from '~/components/panels/StepRunMeta.vue'
|
|
13
13
|
import AttemptEntryHeader from '~/components/panels/AttemptEntryHeader.vue'
|
|
14
14
|
import GateFailingCheckList from '~/components/gates/GateFailingCheckList.vue'
|
|
15
|
+
import CopyButton from '~/components/common/CopyButton.vue'
|
|
15
16
|
|
|
16
17
|
const board = useBoardStore()
|
|
17
18
|
const execution = useExecutionStore()
|
|
@@ -252,12 +253,15 @@ const conflictVerdict = computed(() => {
|
|
|
252
253
|
<template v-else> {{ t('gates.humanReview.suffixAwaiting') }}</template>
|
|
253
254
|
</span>
|
|
254
255
|
</div>
|
|
255
|
-
<
|
|
256
|
+
<div
|
|
256
257
|
v-if="gate.lastFailureSummary"
|
|
257
|
-
class="mt-2
|
|
258
|
+
class="relative mt-2 rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
|
|
258
259
|
>
|
|
259
|
-
|
|
260
|
-
|
|
260
|
+
<CopyButton :text="gate.lastFailureSummary" class="absolute end-1 top-1" />
|
|
261
|
+
<p class="whitespace-pre-wrap pe-8 text-[12px] leading-relaxed text-slate-300">
|
|
262
|
+
{{ gate.lastFailureSummary }}
|
|
263
|
+
</p>
|
|
264
|
+
</div>
|
|
261
265
|
<a
|
|
262
266
|
v-if="prUrl"
|
|
263
267
|
:href="prUrl"
|
|
@@ -330,12 +334,15 @@ const conflictVerdict = computed(() => {
|
|
|
330
334
|
<!-- GitHub's API reports mergeability as a single bit (no file list), but the
|
|
331
335
|
conflict resolver discovers the conflicting files in the container and
|
|
332
336
|
reports them back — surface that account here. -->
|
|
333
|
-
<
|
|
337
|
+
<div
|
|
334
338
|
v-if="gate.lastFailureSummary"
|
|
335
|
-
class="mt-2
|
|
339
|
+
class="relative mt-2 rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
|
|
336
340
|
>
|
|
337
|
-
|
|
338
|
-
|
|
341
|
+
<CopyButton :text="gate.lastFailureSummary" class="absolute end-1 top-1" />
|
|
342
|
+
<p class="whitespace-pre-wrap pe-8 text-[12px] leading-relaxed text-slate-300">
|
|
343
|
+
{{ gate.lastFailureSummary }}
|
|
344
|
+
</p>
|
|
345
|
+
</div>
|
|
339
346
|
<a
|
|
340
347
|
v-if="prUrl"
|
|
341
348
|
:href="prUrl"
|
|
@@ -195,8 +195,9 @@ onKeyStroke('Escape', () => {
|
|
|
195
195
|
if (open.value) close()
|
|
196
196
|
})
|
|
197
197
|
|
|
198
|
+
const { copy } = useCopyToClipboard()
|
|
198
199
|
async function copyOutput() {
|
|
199
|
-
if (step.value?.output) await
|
|
200
|
+
if (step.value?.output) await copy(step.value.output)
|
|
200
201
|
}
|
|
201
202
|
</script>
|
|
202
203
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue'
|
|
3
|
-
import { useClipboard } from '@vueuse/core'
|
|
4
3
|
import type { PipelineStep, RunContainerStatus } from '~/types/execution'
|
|
5
4
|
import { containerPhaseLabel } from '~/utils/pipelineRender'
|
|
6
5
|
|
|
@@ -65,19 +64,7 @@ const phaseLabel = computed(() => containerPhaseLabel(props.step.container?.phas
|
|
|
65
64
|
|
|
66
65
|
// Make the container id / URL one-click copyable (they're long and used to be
|
|
67
66
|
// select-and-copy-by-hand), with a toast confirming the copy landed.
|
|
68
|
-
const
|
|
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
|
-
}
|
|
67
|
+
const { copy: copyText } = useCopyToClipboard()
|
|
81
68
|
</script>
|
|
82
69
|
|
|
83
70
|
<template>
|
|
@@ -70,9 +70,10 @@ function formatClock(ms?: number | null): string | null {
|
|
|
70
70
|
return ms ? d(new Date(ms), 'long') : null
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
const { copy } = useCopyToClipboard()
|
|
73
74
|
async function copyRunId() {
|
|
74
75
|
const id = props.step.runId ?? props.instanceId
|
|
75
|
-
if (id) await
|
|
76
|
+
if (id) await copy(id)
|
|
76
77
|
}
|
|
77
78
|
</script>
|
|
78
79
|
|
|
@@ -40,8 +40,9 @@ function formatClock(ms?: number | null): string | null {
|
|
|
40
40
|
return ms ? d(new Date(ms), 'long') : null
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
const { copy } = useCopyToClipboard()
|
|
43
44
|
async function copyRunId() {
|
|
44
|
-
if (runId.value) await
|
|
45
|
+
if (runId.value) await copy(runId.value)
|
|
45
46
|
}
|
|
46
47
|
</script>
|
|
47
48
|
|
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
|
|
22
22
|
const board = useBoardStore()
|
|
23
23
|
const requirements = useRequirementsStore()
|
|
24
|
+
const models = useModelsStore()
|
|
24
25
|
const toast = useToast()
|
|
25
26
|
const { t } = useI18n()
|
|
26
27
|
|
|
@@ -881,7 +882,9 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
881
882
|
</template>
|
|
882
883
|
<div v-if="review.model" class="flex items-center justify-between">
|
|
883
884
|
<span>{{ t('requirements.stats.model') }}</span>
|
|
884
|
-
<span class="truncate ps-2 text-slate-500">{{
|
|
885
|
+
<span class="truncate ps-2 text-slate-500">{{
|
|
886
|
+
models.labelForRef(review.model) ?? review.model
|
|
887
|
+
}}</span>
|
|
885
888
|
</div>
|
|
886
889
|
</div>
|
|
887
890
|
|
|
@@ -269,8 +269,9 @@ function optional(label: string): string {
|
|
|
269
269
|
// example (not prose), so it stays inline rather than in the i18n catalog — mirroring the format
|
|
270
270
|
// examples the i18n rules keep out of message bodies.
|
|
271
271
|
const AUTO_SETUP_COMMAND = 'cat-factory k3s'
|
|
272
|
+
const { copy } = useCopyToClipboard()
|
|
272
273
|
async function copyAutoSetupCommand() {
|
|
273
|
-
await
|
|
274
|
+
await copy(AUTO_SETUP_COMMAND)
|
|
274
275
|
}
|
|
275
276
|
</script>
|
|
276
277
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Shared clipboard-with-feedback primitive. Wraps VueUse's `useClipboard` with a toast that
|
|
2
|
+
// confirms the copy actually landed (or reports failure) — the pattern first written inline in
|
|
3
|
+
// `StepContainerStatus.vue`, extracted here so every copy affordance behaves the same.
|
|
4
|
+
//
|
|
5
|
+
// UX-38: several copy handlers called `navigator.clipboard?.writeText(...)` directly with no
|
|
6
|
+
// feedback and no catch, so in an insecure context or when permission was denied the copy was a
|
|
7
|
+
// silent no-op the user couldn't tell apart from success. Routing every copy through this seam
|
|
8
|
+
// makes the outcome always visible.
|
|
9
|
+
import { useClipboard } from '@vueuse/core'
|
|
10
|
+
|
|
11
|
+
export function useCopyToClipboard() {
|
|
12
|
+
const { t } = useI18n()
|
|
13
|
+
const toast = useToast()
|
|
14
|
+
const { copy: writeClipboard, isSupported } = useClipboard()
|
|
15
|
+
|
|
16
|
+
async function copy(text: string) {
|
|
17
|
+
// Only claim success once the write actually landed — a failed/unsupported clipboard
|
|
18
|
+
// (insecure context, denied permission) must not show a misleading "Copied" toast.
|
|
19
|
+
try {
|
|
20
|
+
if (!isSupported.value) throw new Error('clipboard unsupported')
|
|
21
|
+
await writeClipboard(text)
|
|
22
|
+
toast.add({ title: t('common.copied'), color: 'success', icon: 'i-lucide-check' })
|
|
23
|
+
} catch {
|
|
24
|
+
toast.add({ title: t('common.copyFailed'), color: 'error', icon: 'i-lucide-x' })
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { copy, isSupported }
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.87.
|
|
3
|
+
"version": "0.87.3",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|