@cat-factory/app 0.115.1 → 0.115.2
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/AgentStopButton.vue +11 -0
- package/app/components/panels/InspectorPanel.vue +29 -0
- package/app/components/panels/inspector/TaskExecution.vue +28 -0
- package/app/components/pipeline/PipelineProgress.vue +23 -3
- package/app/composables/useStepTimer.spec.ts +80 -0
- package/app/composables/useStepTimer.ts +64 -28
- package/app/stores/consensus.spec.ts +76 -0
- package/app/stores/consensus.ts +11 -1
- package/app/stores/docInterview.spec.ts +64 -0
- package/app/stores/kaizen.spec.ts +134 -0
- package/app/stores/kaizen.ts +50 -3
- package/i18n/locales/de.json +12 -4
- package/i18n/locales/en.json +14 -3
- package/i18n/locales/es.json +12 -4
- package/i18n/locales/fr.json +12 -4
- package/i18n/locales/he.json +12 -4
- package/i18n/locales/it.json +12 -4
- package/i18n/locales/ja.json +12 -4
- package/i18n/locales/pl.json +12 -4
- package/i18n/locales/tr.json +12 -4
- package/i18n/locales/uk.json +12 -4
- package/package.json +1 -1
|
@@ -22,12 +22,23 @@ const props = withDefaults(
|
|
|
22
22
|
const { t } = useI18n()
|
|
23
23
|
const agentRuns = useAgentRunsStore()
|
|
24
24
|
const toast = useToast()
|
|
25
|
+
const { confirm } = useConfirm()
|
|
25
26
|
const stopping = ref(false)
|
|
26
27
|
|
|
27
28
|
const displayLabel = computed(() => props.label ?? t('board.stop.label'))
|
|
28
29
|
|
|
29
30
|
async function stop() {
|
|
30
31
|
if (stopping.value) return
|
|
32
|
+
// Killing a running container discards its in-flight work — gate it behind a confirm,
|
|
33
|
+
// matching the confirm-then-mutate contract the task reset path uses.
|
|
34
|
+
const ok = await confirm({
|
|
35
|
+
title: t('board.stop.confirm.title'),
|
|
36
|
+
description: t('board.stop.confirm.body'),
|
|
37
|
+
confirmLabel: t('board.stop.confirm.confirm'),
|
|
38
|
+
variant: 'destructive',
|
|
39
|
+
icon: 'i-lucide-circle-stop',
|
|
40
|
+
})
|
|
41
|
+
if (!ok) return
|
|
31
42
|
stopping.value = true
|
|
32
43
|
try {
|
|
33
44
|
const kind = await agentRuns.stop(props.runId)
|
|
@@ -100,6 +100,21 @@ const statusLabel = computed(() =>
|
|
|
100
100
|
|
|
101
101
|
const runnable = computed(() => (block.value ? board.isRunnable(block.value.id) : false))
|
|
102
102
|
|
|
103
|
+
// A task runs only once every dependency has merged. When the Run trigger is locked
|
|
104
|
+
// it must say WHY — name the unfinished dependencies rather than showing a bare lock.
|
|
105
|
+
const unmetDepTitles = computed(() =>
|
|
106
|
+
block.value && isTask.value ? board.unmetDeps(block.value.id).map((b) => b.title) : [],
|
|
107
|
+
)
|
|
108
|
+
const runBlockedReason = computed(() =>
|
|
109
|
+
unmetDepTitles.value.length
|
|
110
|
+
? t(
|
|
111
|
+
'panels.inspector.runBlocked',
|
|
112
|
+
{ count: unmetDepTitles.value.length, names: unmetDepTitles.value.join(', ') },
|
|
113
|
+
unmetDepTitles.value.length,
|
|
114
|
+
)
|
|
115
|
+
: null,
|
|
116
|
+
)
|
|
117
|
+
|
|
103
118
|
// The delete control names what it removes, so selecting a task and deleting it
|
|
104
119
|
// reads as "Delete task" rather than ambiguously removing the whole service.
|
|
105
120
|
const deleteLabel = computed(() =>
|
|
@@ -532,6 +547,19 @@ const showOriginalDescription = ref(false)
|
|
|
532
547
|
<!-- initiative: status + goal, run-planning + tracker controls -->
|
|
533
548
|
<InitiativeInspector v-else-if="isInitiative" :block="block" />
|
|
534
549
|
|
|
550
|
+
<!-- Locked-run explanation: a disabled task Run button reads as a dead lock unless
|
|
551
|
+
it says what's holding it. Named here (and on the button title) so the blocking
|
|
552
|
+
dependencies are visible to pointer, keyboard, and touch alike — a native title
|
|
553
|
+
on a disabled button doesn't fire hover events. -->
|
|
554
|
+
<p
|
|
555
|
+
v-if="isTask && runBlockedReason"
|
|
556
|
+
class="flex items-start gap-1.5 text-[11px] text-amber-300/90"
|
|
557
|
+
data-testid="run-blocked-reason"
|
|
558
|
+
>
|
|
559
|
+
<UIcon name="i-lucide-lock" class="mt-px h-3 w-3 shrink-0" />
|
|
560
|
+
<span>{{ runBlockedReason }}</span>
|
|
561
|
+
</p>
|
|
562
|
+
|
|
535
563
|
<!-- actions -->
|
|
536
564
|
<div class="flex items-center gap-2">
|
|
537
565
|
<UDropdownMenu v-if="isTask" :items="runMenu">
|
|
@@ -542,6 +570,7 @@ const showOriginalDescription = ref(false)
|
|
|
542
570
|
:icon="runnable ? 'i-lucide-play' : 'i-lucide-lock'"
|
|
543
571
|
trailing-icon="i-lucide-chevron-down"
|
|
544
572
|
:disabled="!runnable"
|
|
573
|
+
:title="runBlockedReason ?? undefined"
|
|
545
574
|
>
|
|
546
575
|
{{ instance ? t('panels.inspector.reRun') : t('panels.inspector.run') }}
|
|
547
576
|
</UButton>
|
|
@@ -11,6 +11,8 @@ import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
|
|
|
11
11
|
import AgentFailureHistory from '~/components/board/AgentFailureHistory.vue'
|
|
12
12
|
import EmptyState from '~/components/common/EmptyState.vue'
|
|
13
13
|
import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
|
|
14
|
+
import { useNowTick, stepDurationLabel } from '~/composables/useStepTimer'
|
|
15
|
+
import type { PipelineStep } from '~/types/execution'
|
|
14
16
|
|
|
15
17
|
const props = defineProps<{ block: Block }>()
|
|
16
18
|
|
|
@@ -85,6 +87,13 @@ function stepFailed(s: { state: string }) {
|
|
|
85
87
|
return runFailed.value && s.state === 'working'
|
|
86
88
|
}
|
|
87
89
|
|
|
90
|
+
// A shared 1s tick drives every step's live elapsed clock, so a running step that
|
|
91
|
+
// hasn't yet emitted subtask counts reads as progressing rather than hung.
|
|
92
|
+
const nowTick = useNowTick()
|
|
93
|
+
function stepElapsed(s: PipelineStep): string | null {
|
|
94
|
+
return stepDurationLabel(s, nowTick.value, runFailed.value, instance.value?.failure?.occurredAt)
|
|
95
|
+
}
|
|
96
|
+
|
|
88
97
|
/** A gated step parked for approval reads "Needs approval", not "Needs decision". */
|
|
89
98
|
function labelForStep(s: {
|
|
90
99
|
state: string
|
|
@@ -148,6 +157,17 @@ function openForkFor(i: number) {
|
|
|
148
157
|
const stopping = ref(false)
|
|
149
158
|
async function stopRun() {
|
|
150
159
|
if (!instance.value || stopping.value) return
|
|
160
|
+
// Killing the running container discards its in-flight work — gate it behind the same
|
|
161
|
+
// confirm the board card's stop uses (via `AgentStopButton`), so every stop surface for
|
|
162
|
+
// a run is confirm-gated identically.
|
|
163
|
+
const ok = await confirm({
|
|
164
|
+
title: t('board.stop.confirm.title'),
|
|
165
|
+
description: t('board.stop.confirm.body'),
|
|
166
|
+
confirmLabel: t('board.stop.confirm.confirm'),
|
|
167
|
+
variant: 'destructive',
|
|
168
|
+
icon: 'i-lucide-circle-stop',
|
|
169
|
+
})
|
|
170
|
+
if (!ok) return
|
|
151
171
|
stopping.value = true
|
|
152
172
|
try {
|
|
153
173
|
await execution.stop(instance.value.id)
|
|
@@ -306,6 +326,14 @@ async function mergePr() {
|
|
|
306
326
|
>
|
|
307
327
|
<UIcon v-if="stepFailed(s)" name="i-lucide-circle-x" class="h-3 w-3 shrink-0" />
|
|
308
328
|
{{ labelForStep(s) }}
|
|
329
|
+
<!-- live elapsed clock: a running step counts up, a finished one shows total -->
|
|
330
|
+
<span
|
|
331
|
+
v-if="stepElapsed(s)"
|
|
332
|
+
class="inline-flex items-center gap-0.5 font-mono tabular-nums text-slate-500"
|
|
333
|
+
:title="t('inspector.execution.elapsedTooltip')"
|
|
334
|
+
>
|
|
335
|
+
· {{ stepElapsed(s) }}
|
|
336
|
+
</span>
|
|
309
337
|
</span>
|
|
310
338
|
<UButton
|
|
311
339
|
v-if="s.decision && !s.decision.chosen"
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
containerPhaseLabel,
|
|
13
13
|
} from '~/utils/pipelineRender'
|
|
14
14
|
import StepMetricsBar from '~/components/observability/StepMetricsBar.vue'
|
|
15
|
+
import { useNowTick, stepDurationLabel } from '~/composables/useStepTimer'
|
|
15
16
|
|
|
16
17
|
const props = defineProps<{ instance: ExecutionInstance }>()
|
|
17
18
|
const emit = defineEmits<{
|
|
@@ -136,6 +137,13 @@ const STATUS_META = computed<Record<ExecutionInstance['status'], { label: string
|
|
|
136
137
|
const steps = computed(() => props.instance.steps)
|
|
137
138
|
const total = computed(() => steps.value.length)
|
|
138
139
|
|
|
140
|
+
// A shared 1s tick drives every step's live elapsed clock, so a step that hasn't yet
|
|
141
|
+
// emitted subtask counts still shows it is progressing rather than reading as hung.
|
|
142
|
+
const nowTick = useNowTick()
|
|
143
|
+
function stepElapsed(s: PipelineStep): string | null {
|
|
144
|
+
return stepDurationLabel(s, nowTick.value, runFailed.value, props.instance.failure?.occurredAt)
|
|
145
|
+
}
|
|
146
|
+
|
|
139
147
|
// The conditionally-run companion (e.g. the Tester's `fixer`) each step drives, with
|
|
140
148
|
// its possible/running/completed/skipped state — rendered as a distinct sub-node so a
|
|
141
149
|
// human can see at a glance whether the fixer ran or was skipped.
|
|
@@ -322,8 +330,20 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
322
330
|
{{ t('pipeline.progress.companion') }}
|
|
323
331
|
</span>
|
|
324
332
|
</div>
|
|
325
|
-
<div
|
|
326
|
-
|
|
333
|
+
<div
|
|
334
|
+
class="flex items-center gap-1.5 text-[10px] uppercase tracking-wide text-slate-500"
|
|
335
|
+
>
|
|
336
|
+
<span>{{ t('pipeline.progress.stepOf', { current: i + 1, total }) }}</span>
|
|
337
|
+
<!-- live elapsed clock: a running step counts up (so no-subtask steps
|
|
338
|
+
don't read as hung), a finished step shows its total duration -->
|
|
339
|
+
<span
|
|
340
|
+
v-if="stepElapsed(s)"
|
|
341
|
+
class="inline-flex items-center gap-0.5 font-mono normal-case tabular-nums text-slate-400"
|
|
342
|
+
:title="t('pipeline.progress.elapsedTooltip')"
|
|
343
|
+
>
|
|
344
|
+
<UIcon name="i-lucide-clock" class="h-2.5 w-2.5 shrink-0" />
|
|
345
|
+
{{ stepElapsed(s) }}
|
|
346
|
+
</span>
|
|
327
347
|
</div>
|
|
328
348
|
</div>
|
|
329
349
|
<span
|
|
@@ -343,7 +363,7 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
343
363
|
color="neutral"
|
|
344
364
|
variant="ghost"
|
|
345
365
|
size="xs"
|
|
346
|
-
class="shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
|
|
366
|
+
class="shrink-0 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100"
|
|
347
367
|
:title="t('pipeline.progress.restartTooltip')"
|
|
348
368
|
@click.stop="
|
|
349
369
|
() => {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { PipelineStep } from '~/types/execution'
|
|
3
|
+
import { stepDurationLabel, stepDurationMs, stepIsRunning } from '~/composables/useStepTimer'
|
|
4
|
+
|
|
5
|
+
// The pure helpers encode one freeze rule shared by the list surfaces (pipeline timeline,
|
|
6
|
+
// inspector run list) and the single-step overlay, so pin the precedence here:
|
|
7
|
+
// finishedAt > (runFailed ? failureAt ?? startedAt) > pausedAt > live now.
|
|
8
|
+
function step(overrides: Partial<PipelineStep> = {}): PipelineStep {
|
|
9
|
+
return { agentKind: 'coder', state: 'working', ...overrides } as PipelineStep
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const NOW = 10_000
|
|
13
|
+
|
|
14
|
+
describe('stepIsRunning', () => {
|
|
15
|
+
it('is false for a null step', () => {
|
|
16
|
+
expect(stepIsRunning(null, false)).toBe(false)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('is false until the step has started', () => {
|
|
20
|
+
expect(stepIsRunning(step({ startedAt: undefined }), false)).toBe(false)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('is true for a started, unfinished, unparked step on a live run', () => {
|
|
24
|
+
expect(stepIsRunning(step({ startedAt: 1000 }), false)).toBe(true)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('is false once finished, parked, or the run failed', () => {
|
|
28
|
+
expect(stepIsRunning(step({ startedAt: 1000, finishedAt: 2000 }), false)).toBe(false)
|
|
29
|
+
expect(stepIsRunning(step({ startedAt: 1000, pausedAt: 1500 }), false)).toBe(false)
|
|
30
|
+
expect(stepIsRunning(step({ startedAt: 1000 }), true)).toBe(false)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('stepDurationMs', () => {
|
|
35
|
+
it('is null until the step has started', () => {
|
|
36
|
+
expect(stepDurationMs(step({ startedAt: undefined }), NOW, false, null)).toBeNull()
|
|
37
|
+
expect(stepDurationMs(null, NOW, false, null)).toBeNull()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('counts up to now while live', () => {
|
|
41
|
+
expect(stepDurationMs(step({ startedAt: 4000 }), NOW, false, null)).toBe(6000)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('freezes at finishedAt once finished (ignoring now)', () => {
|
|
45
|
+
expect(stepDurationMs(step({ startedAt: 4000, finishedAt: 7000 }), NOW, false, null)).toBe(3000)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('freezes at the run failure time when the run failed', () => {
|
|
49
|
+
expect(stepDurationMs(step({ startedAt: 4000 }), NOW, true, 6000)).toBe(2000)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('falls back to startedAt (zero) when the run failed with no failure time', () => {
|
|
53
|
+
expect(stepDurationMs(step({ startedAt: 4000 }), NOW, true, null)).toBe(0)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('freezes at the park time when parked on a human', () => {
|
|
57
|
+
expect(stepDurationMs(step({ startedAt: 4000, pausedAt: 5500 }), NOW, false, null)).toBe(1500)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('prefers finishedAt over the failure time', () => {
|
|
61
|
+
expect(stepDurationMs(step({ startedAt: 4000, finishedAt: 5000 }), NOW, true, 6000)).toBe(1000)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('never returns a negative duration', () => {
|
|
65
|
+
expect(stepDurationMs(step({ startedAt: 8000 }), NOW, true, 6000)).toBe(0)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('stepDurationLabel', () => {
|
|
70
|
+
it('is null until the step has started', () => {
|
|
71
|
+
expect(stepDurationLabel(step({ startedAt: undefined }), NOW, false, null)).toBeNull()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('formats seconds and minutes', () => {
|
|
75
|
+
expect(stepDurationLabel(step({ startedAt: 4000 }), NOW, false, null)).toBe('6s')
|
|
76
|
+
expect(stepDurationLabel(step({ startedAt: 0, finishedAt: 90_000 }), NOW, false, null)).toBe(
|
|
77
|
+
'1m 30s',
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
2
2
|
import type { PipelineStep } from '~/types/execution'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Whether a step is actively executing: it has started, hasn't finished, isn't
|
|
6
|
+
* parked on a human (`pausedAt`), and the run itself hasn't failed. A step in any
|
|
7
|
+
* of those states is not ticking — no spinner, no counting-up clock.
|
|
8
|
+
*/
|
|
9
|
+
export function stepIsRunning(step: PipelineStep | null, runFailed: boolean): boolean {
|
|
10
|
+
return !!step?.startedAt && !step?.finishedAt && step?.pausedAt == null && !runFailed
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Elapsed/total execution time in ms for a step at wall-clock `nowMs`, or null until
|
|
15
|
+
* the step has started. The clock freezes once the step stops working: at its finish,
|
|
16
|
+
* else at the run's failure time once the run has failed, else at the moment it parked
|
|
17
|
+
* on a human (`pausedAt`). Otherwise it is live, counting up to `nowMs`.
|
|
18
|
+
*/
|
|
19
|
+
export function stepDurationMs(
|
|
20
|
+
step: PipelineStep | null,
|
|
21
|
+
nowMs: number,
|
|
22
|
+
runFailed: boolean,
|
|
23
|
+
failureAt: number | null | undefined,
|
|
24
|
+
): number | null {
|
|
25
|
+
if (step?.startedAt == null) return null
|
|
26
|
+
const end =
|
|
27
|
+
step.finishedAt ?? (runFailed ? (failureAt ?? step.startedAt) : (step.pausedAt ?? nowMs))
|
|
28
|
+
return Math.max(0, end - step.startedAt)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Human-friendly elapsed label for a step at `nowMs`, or null until it has started. */
|
|
32
|
+
export function stepDurationLabel(
|
|
33
|
+
step: PipelineStep | null,
|
|
34
|
+
nowMs: number,
|
|
35
|
+
runFailed: boolean,
|
|
36
|
+
failureAt: number | null | undefined,
|
|
37
|
+
): string | null {
|
|
38
|
+
const ms = stepDurationMs(step, nowMs, runFailed, failureAt)
|
|
39
|
+
return ms == null ? null : formatDuration(ms)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A shared 1s wall-clock tick for surfaces that render many steps' live durations
|
|
44
|
+
* at once (the pipeline timeline, the inspector run list). One interval drives every
|
|
45
|
+
* step's elapsed label instead of a per-step timer. Stays `0` until mounted so the
|
|
46
|
+
* first paint never reads a stale time.
|
|
47
|
+
*/
|
|
48
|
+
export function useNowTick(intervalMs = 1000) {
|
|
49
|
+
const now = ref(0)
|
|
50
|
+
let timer: ReturnType<typeof setInterval> | undefined
|
|
51
|
+
onMounted(() => {
|
|
52
|
+
now.value = Date.now()
|
|
53
|
+
timer = setInterval(() => (now.value = Date.now()), intervalMs)
|
|
54
|
+
})
|
|
55
|
+
onUnmounted(() => {
|
|
56
|
+
if (timer) clearInterval(timer)
|
|
57
|
+
})
|
|
58
|
+
return now
|
|
59
|
+
}
|
|
60
|
+
|
|
4
61
|
/**
|
|
5
62
|
* Live elapsed-time clock for a single pipeline step. A 1s tick drives the
|
|
6
63
|
* counting-up duration while the step is actively running; the clock freezes at
|
|
@@ -12,37 +69,16 @@ export function useStepTimer(opts: {
|
|
|
12
69
|
runFailed: () => boolean
|
|
13
70
|
failureAt: () => number | null | undefined
|
|
14
71
|
}) {
|
|
15
|
-
|
|
16
|
-
const nowTick = ref(0)
|
|
17
|
-
let timer: ReturnType<typeof setInterval> | undefined
|
|
18
|
-
onMounted(() => {
|
|
19
|
-
nowTick.value = Date.now()
|
|
20
|
-
timer = setInterval(() => (nowTick.value = Date.now()), 1000)
|
|
21
|
-
})
|
|
22
|
-
onUnmounted(() => {
|
|
23
|
-
if (timer) clearInterval(timer)
|
|
24
|
-
})
|
|
72
|
+
const nowTick = useNowTick()
|
|
25
73
|
|
|
26
74
|
// A step that is finished, failed, or parked on a human is not actively
|
|
27
|
-
// executing — no ticking clock or spinner. `pausedAt` is the "waiting on input"
|
|
28
|
-
|
|
29
|
-
const isRunning = computed(() => {
|
|
30
|
-
const s = opts.step()
|
|
31
|
-
return !!s?.startedAt && !s?.finishedAt && s?.pausedAt == null && !opts.runFailed()
|
|
32
|
-
})
|
|
75
|
+
// executing — no ticking clock or spinner. `pausedAt` is the "waiting on input" freeze.
|
|
76
|
+
const isRunning = computed(() => stepIsRunning(opts.step(), opts.runFailed()))
|
|
33
77
|
|
|
34
78
|
/** Elapsed/total execution time in ms — null until the step has started. */
|
|
35
|
-
const durationMs = computed(() =>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// Freeze the clock once the step stops working: at its finish, else at the
|
|
39
|
-
// failure time once the run has failed, else at the moment it parked on a
|
|
40
|
-
// human (`pausedAt`). Otherwise it is live, so count up to the current tick.
|
|
41
|
-
const end =
|
|
42
|
-
s.finishedAt ??
|
|
43
|
-
(opts.runFailed() ? (opts.failureAt() ?? s.startedAt) : (s.pausedAt ?? nowTick.value))
|
|
44
|
-
return Math.max(0, end - s.startedAt)
|
|
45
|
-
})
|
|
79
|
+
const durationMs = computed(() =>
|
|
80
|
+
stepDurationMs(opts.step(), nowTick.value, opts.runFailed(), opts.failureAt()),
|
|
81
|
+
)
|
|
46
82
|
|
|
47
83
|
const durationLabel = computed(() =>
|
|
48
84
|
durationMs.value == null ? null : formatDuration(durationMs.value),
|
|
@@ -51,7 +87,7 @@ export function useStepTimer(opts: {
|
|
|
51
87
|
return { isRunning, durationMs, durationLabel }
|
|
52
88
|
}
|
|
53
89
|
|
|
54
|
-
function formatDuration(ms: number): string {
|
|
90
|
+
export function formatDuration(ms: number): string {
|
|
55
91
|
const totalSec = Math.round(ms / 1000)
|
|
56
92
|
if (totalSec < 60) return `${totalSec}s`
|
|
57
93
|
const m = Math.floor(totalSec / 60)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import { useConsensusStore } from '~/stores/consensus'
|
|
3
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
4
|
+
import type { ConsensusSession } from '~/types/consensus'
|
|
5
|
+
|
|
6
|
+
/** Minimal session factory — only the fields the store reconciles/reads. */
|
|
7
|
+
function session(over: Partial<ConsensusSession> = {}): ConsensusSession {
|
|
8
|
+
return {
|
|
9
|
+
id: 's1',
|
|
10
|
+
blockId: 'blk1',
|
|
11
|
+
executionId: null,
|
|
12
|
+
stepIndex: 0,
|
|
13
|
+
agentKind: 'architect',
|
|
14
|
+
strategy: 'panel',
|
|
15
|
+
status: 'complete',
|
|
16
|
+
participants: [],
|
|
17
|
+
rounds: [],
|
|
18
|
+
synthesis: null,
|
|
19
|
+
createdAt: 1,
|
|
20
|
+
updatedAt: 1,
|
|
21
|
+
...over,
|
|
22
|
+
} as ConsensusSession
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('consensus store — load vs live-push reconcile', () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
useWorkspaceStore().workspaceId = 'ws1'
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('load stores the fetched session', async () => {
|
|
31
|
+
vi.stubGlobal('useApi', () => ({
|
|
32
|
+
getConsensusSession: () => Promise.resolve({ session: session() }),
|
|
33
|
+
}))
|
|
34
|
+
const store = useConsensusStore()
|
|
35
|
+
await store.load('blk1')
|
|
36
|
+
expect(store.sessionFor('blk1')?.id).toBe('s1')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('a stale load never regresses a fresher live-pushed session', async () => {
|
|
40
|
+
// A live `consensus` push delivers the newest transcript; a `load` that started earlier
|
|
41
|
+
// resolves later with a staler snapshot. It must NOT overwrite the fresher one.
|
|
42
|
+
let resolveFetch!: (r: { session: ConsensusSession | null }) => void
|
|
43
|
+
const pending = new Promise<{ session: ConsensusSession | null }>((res) => {
|
|
44
|
+
resolveFetch = res
|
|
45
|
+
})
|
|
46
|
+
vi.stubGlobal('useApi', () => ({ getConsensusSession: () => pending }))
|
|
47
|
+
const store = useConsensusStore()
|
|
48
|
+
|
|
49
|
+
const load = store.load('blk1')
|
|
50
|
+
store.upsert(session({ updatedAt: 10, synthesis: 'fresh' }))
|
|
51
|
+
resolveFetch({ session: session({ updatedAt: 2, synthesis: 'stale' }) })
|
|
52
|
+
await load
|
|
53
|
+
|
|
54
|
+
expect(store.sessionFor('blk1')?.synthesis).toBe('fresh')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('a load returning "none" never clobbers an existing live session', async () => {
|
|
58
|
+
vi.stubGlobal('useApi', () => ({
|
|
59
|
+
getConsensusSession: () => Promise.resolve({ session: null }),
|
|
60
|
+
}))
|
|
61
|
+
const store = useConsensusStore()
|
|
62
|
+
store.upsert(session({ synthesis: 'live' }))
|
|
63
|
+
await store.load('blk1')
|
|
64
|
+
expect(store.sessionFor('blk1')?.synthesis).toBe('live')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('a load returning "none" records the fetched-empty state when nothing is cached', async () => {
|
|
68
|
+
vi.stubGlobal('useApi', () => ({
|
|
69
|
+
getConsensusSession: () => Promise.resolve({ session: null }),
|
|
70
|
+
}))
|
|
71
|
+
const store = useConsensusStore()
|
|
72
|
+
await store.load('blk1')
|
|
73
|
+
expect('blk1' in store.sessions).toBe(true)
|
|
74
|
+
expect(store.sessionFor('blk1')).toBeNull()
|
|
75
|
+
})
|
|
76
|
+
})
|
package/app/stores/consensus.ts
CHANGED
|
@@ -46,7 +46,17 @@ export const useConsensusStore = defineStore('consensus', () => {
|
|
|
46
46
|
loading.value = new Set(loading.value).add(blockId)
|
|
47
47
|
try {
|
|
48
48
|
const { session } = await api.getConsensusSession(wsId, blockId)
|
|
49
|
-
|
|
49
|
+
// Reconcile rather than blind-replace: a `load` resolving AFTER a fresher live
|
|
50
|
+
// `consensus` push (or after a newer concurrent load) must not regress the transcript —
|
|
51
|
+
// the out-of-order-overwrite hazard the CLAUDE.md live-push rules warn about. Keep
|
|
52
|
+
// whichever session is newer by `updatedAt` (any id), and never overwrite an existing
|
|
53
|
+
// (possibly live-pushed) session with a raced "none".
|
|
54
|
+
const existing = sessions.value[blockId]
|
|
55
|
+
if (session) {
|
|
56
|
+
if (!existing || session.updatedAt >= existing.updatedAt) store(session)
|
|
57
|
+
} else if (existing === undefined) {
|
|
58
|
+
sessions.value = { ...sessions.value, [blockId]: null }
|
|
59
|
+
}
|
|
50
60
|
} catch {
|
|
51
61
|
// Consensus off / no session — leave the cache as-is; the window shows its empty state.
|
|
52
62
|
} finally {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import { useDocInterviewStore } from '~/stores/docInterview'
|
|
3
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
4
|
+
import type { DocInterviewSession } from '~/types/domain'
|
|
5
|
+
|
|
6
|
+
/** Minimal session factory — only the fields the store reconciles/reads. */
|
|
7
|
+
function session(over: Partial<DocInterviewSession> = {}): DocInterviewSession {
|
|
8
|
+
return {
|
|
9
|
+
id: 'd1',
|
|
10
|
+
blockId: 'blk1',
|
|
11
|
+
status: 'awaiting_answers',
|
|
12
|
+
round: 1,
|
|
13
|
+
maxRounds: 3,
|
|
14
|
+
qa: [],
|
|
15
|
+
brief: null,
|
|
16
|
+
model: null,
|
|
17
|
+
createdAt: 1,
|
|
18
|
+
updatedAt: 1,
|
|
19
|
+
...over,
|
|
20
|
+
} as DocInterviewSession
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// docInterview already routes its `load` through `upsert`'s newest-wins (`updatedAt`) guard —
|
|
24
|
+
// these specs pin that so a future refactor can't reintroduce a blind-replace clobber.
|
|
25
|
+
describe('docInterview store — load vs live-push reconcile', () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
useWorkspaceStore().workspaceId = 'ws1'
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('load stores the fetched session', async () => {
|
|
31
|
+
vi.stubGlobal('useApi', () => ({
|
|
32
|
+
getDocInterview: () => Promise.resolve(session()),
|
|
33
|
+
}))
|
|
34
|
+
const store = useDocInterviewStore()
|
|
35
|
+
await store.load('blk1')
|
|
36
|
+
expect(store.forBlock('blk1')?.id).toBe('d1')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('a stale load never regresses a fresher live-pushed session', async () => {
|
|
40
|
+
let resolveFetch!: (r: DocInterviewSession | null) => void
|
|
41
|
+
const pending = new Promise<DocInterviewSession | null>((res) => {
|
|
42
|
+
resolveFetch = res
|
|
43
|
+
})
|
|
44
|
+
vi.stubGlobal('useApi', () => ({ getDocInterview: () => pending }))
|
|
45
|
+
const store = useDocInterviewStore()
|
|
46
|
+
|
|
47
|
+
const load = store.load('blk1')
|
|
48
|
+
store.upsert(session({ updatedAt: 10, round: 2 }))
|
|
49
|
+
resolveFetch(session({ updatedAt: 2, round: 1 }))
|
|
50
|
+
await load
|
|
51
|
+
|
|
52
|
+
expect(store.forBlock('blk1')?.round).toBe(2)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('a load returning "none" leaves the cache untouched', async () => {
|
|
56
|
+
vi.stubGlobal('useApi', () => ({
|
|
57
|
+
getDocInterview: () => Promise.resolve(null),
|
|
58
|
+
}))
|
|
59
|
+
const store = useDocInterviewStore()
|
|
60
|
+
store.upsert(session({ round: 2 }))
|
|
61
|
+
await store.load('blk1')
|
|
62
|
+
expect(store.forBlock('blk1')?.round).toBe(2)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import { useKaizenStore } from '~/stores/kaizen'
|
|
3
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
4
|
+
import type { KaizenGrading } from '~/types/domain'
|
|
5
|
+
|
|
6
|
+
/** Minimal grading factory — only the fields the store reconciles/reads. */
|
|
7
|
+
function grading(over: Partial<KaizenGrading> = {}): KaizenGrading {
|
|
8
|
+
return {
|
|
9
|
+
id: 'g1',
|
|
10
|
+
executionId: 'exec1',
|
|
11
|
+
blockId: 'blk1',
|
|
12
|
+
stepIndex: 0,
|
|
13
|
+
agentKind: 'coder',
|
|
14
|
+
model: 'm',
|
|
15
|
+
promptVersion: 1,
|
|
16
|
+
comboKey: 'coder|m|1',
|
|
17
|
+
status: 'complete',
|
|
18
|
+
grade: 5,
|
|
19
|
+
summary: '',
|
|
20
|
+
recommendations: [],
|
|
21
|
+
graderModel: null,
|
|
22
|
+
error: null,
|
|
23
|
+
createdAt: 1,
|
|
24
|
+
updatedAt: 1,
|
|
25
|
+
...over,
|
|
26
|
+
} as KaizenGrading
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('kaizen store — live-push clobber guards', () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
useWorkspaceStore().workspaceId = 'ws1'
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('loadForExecution stores the fetched gradings', async () => {
|
|
35
|
+
vi.stubGlobal('useApi', () => ({
|
|
36
|
+
getKaizenForExecution: () => Promise.resolve({ gradings: [grading()] }),
|
|
37
|
+
}))
|
|
38
|
+
const store = useKaizenStore()
|
|
39
|
+
await store.loadForExecution('exec1')
|
|
40
|
+
expect(store.byExecution.exec1).toHaveLength(1)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('a slower stale loadForExecution never clobbers a newer one (monotonic guard)', async () => {
|
|
44
|
+
// Two loads race for the same execution: the FIRST-issued resolves LAST with a stale list.
|
|
45
|
+
// Without the ticket guard its REPLACE would overwrite the fresher second result.
|
|
46
|
+
const deferred: Array<(r: { gradings: KaizenGrading[] }) => void> = []
|
|
47
|
+
vi.stubGlobal('useApi', () => ({
|
|
48
|
+
getKaizenForExecution: () =>
|
|
49
|
+
new Promise<{ gradings: KaizenGrading[] }>((res) => deferred.push(res)),
|
|
50
|
+
}))
|
|
51
|
+
const store = useKaizenStore()
|
|
52
|
+
const first = store.loadForExecution('exec1') // issued #1 (stale)
|
|
53
|
+
const second = store.loadForExecution('exec1') // issued #2 (fresh)
|
|
54
|
+
|
|
55
|
+
deferred[1]!({ gradings: [grading({ id: 'fresh' })] })
|
|
56
|
+
deferred[0]!({ gradings: [grading({ id: 'stale' })] })
|
|
57
|
+
await Promise.all([first, second])
|
|
58
|
+
|
|
59
|
+
expect(store.byExecution.exec1).toHaveLength(1)
|
|
60
|
+
expect(store.byExecution.exec1![0]!.id).toBe('fresh')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('a grading pushed live mid-load survives the load (merge, not blind-replace)', async () => {
|
|
64
|
+
// A load is in flight (server response predates the newest grading); a live `upsert` lands
|
|
65
|
+
// its grading; then the load resolves. A blind replace would drop the live-only grading.
|
|
66
|
+
let resolveFetch!: (r: { gradings: KaizenGrading[] }) => void
|
|
67
|
+
const pending = new Promise<{ gradings: KaizenGrading[] }>((res) => {
|
|
68
|
+
resolveFetch = res
|
|
69
|
+
})
|
|
70
|
+
vi.stubGlobal('useApi', () => ({ getKaizenForExecution: () => pending }))
|
|
71
|
+
const store = useKaizenStore()
|
|
72
|
+
|
|
73
|
+
const load = store.loadForExecution('exec1')
|
|
74
|
+
// A live stream event arrives while the fetch is in flight.
|
|
75
|
+
store.upsert(grading({ id: 'live', stepIndex: 1 }))
|
|
76
|
+
// The load's (staler) response comes back with only the earlier grading.
|
|
77
|
+
resolveFetch({ gradings: [grading({ id: 'g1', stepIndex: 0 })] })
|
|
78
|
+
await load
|
|
79
|
+
|
|
80
|
+
const ids = store.byExecution.exec1!.map((g) => g.id).sort()
|
|
81
|
+
expect(ids).toEqual(['g1', 'live'])
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('a shared-id load keeps whichever updatedAt is newer', async () => {
|
|
85
|
+
// Stub before the store is created — it captures `useApi()` at instantiation.
|
|
86
|
+
vi.stubGlobal('useApi', () => ({
|
|
87
|
+
getKaizenForExecution: () =>
|
|
88
|
+
Promise.resolve({ gradings: [grading({ id: 'g1', updatedAt: 2, summary: 'stale' })] }),
|
|
89
|
+
}))
|
|
90
|
+
const store = useKaizenStore()
|
|
91
|
+
// Seed a fresher live grading, then a load returns a staler copy of the SAME id.
|
|
92
|
+
store.upsert(grading({ id: 'g1', updatedAt: 5, summary: 'live' }))
|
|
93
|
+
await store.loadForExecution('exec1')
|
|
94
|
+
expect(store.byExecution.exec1![0]!.summary).toBe('live')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('loadOverview preserves a live-pushed grading in history (merge, newest-first)', async () => {
|
|
98
|
+
vi.stubGlobal('useApi', () => ({
|
|
99
|
+
getKaizenOverview: () =>
|
|
100
|
+
Promise.resolve({
|
|
101
|
+
gradings: [grading({ id: 'old', createdAt: 1, updatedAt: 1 })],
|
|
102
|
+
verified: [],
|
|
103
|
+
}),
|
|
104
|
+
}))
|
|
105
|
+
const store = useKaizenStore()
|
|
106
|
+
// A grading arrives live before the overview list is fetched.
|
|
107
|
+
store.upsert(grading({ id: 'live', createdAt: 9, updatedAt: 9 }))
|
|
108
|
+
await store.loadOverview()
|
|
109
|
+
|
|
110
|
+
const ids = store.history.map((g) => g.id)
|
|
111
|
+
expect(ids).toContain('live')
|
|
112
|
+
expect(ids).toContain('old')
|
|
113
|
+
// The live (newest) grading stays at the front of the newest-first list.
|
|
114
|
+
expect(ids[0]).toBe('live')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('a slower stale loadOverview never clobbers a newer one', async () => {
|
|
118
|
+
const deferred: Array<(r: { gradings: KaizenGrading[]; verified: [] }) => void> = []
|
|
119
|
+
vi.stubGlobal('useApi', () => ({
|
|
120
|
+
getKaizenOverview: () =>
|
|
121
|
+
new Promise<{ gradings: KaizenGrading[]; verified: [] }>((res) => deferred.push(res)),
|
|
122
|
+
}))
|
|
123
|
+
const store = useKaizenStore()
|
|
124
|
+
const first = store.loadOverview() // stale
|
|
125
|
+
const second = store.loadOverview() // fresh
|
|
126
|
+
|
|
127
|
+
deferred[1]!({ gradings: [grading({ id: 'fresh' })], verified: [] })
|
|
128
|
+
deferred[0]!({ gradings: [grading({ id: 'stale' })], verified: [] })
|
|
129
|
+
await Promise.all([first, second])
|
|
130
|
+
|
|
131
|
+
expect(store.history).toHaveLength(1)
|
|
132
|
+
expect(store.history[0]!.id).toBe('fresh')
|
|
133
|
+
})
|
|
134
|
+
})
|
package/app/stores/kaizen.ts
CHANGED
|
@@ -23,6 +23,36 @@ export const useKaizenStore = defineStore('kaizen', () => {
|
|
|
23
23
|
/** 503 ⇒ the Kaizen feature isn't configured on this deployment. */
|
|
24
24
|
const available = ref<boolean | null>(null)
|
|
25
25
|
|
|
26
|
+
// Monotonic load-ordering guard. Both loads REPLACE state that also arrives live over the
|
|
27
|
+
// stream (`upsert`), so a slower/staler fetch resolving AFTER a newer one — or after a live
|
|
28
|
+
// push — would clobber the fresher gradings (the CLAUDE.md live-push out-of-order hazard,
|
|
29
|
+
// the same one `stores/provisioningLogs.ts` guards). Each load takes a ticket; only the
|
|
30
|
+
// newest-issued one commits. NOT reactive — pure bookkeeping the UI never reads.
|
|
31
|
+
let loadTicket = 0
|
|
32
|
+
let latestOverviewLoad = 0
|
|
33
|
+
const latestExecLoad = new Map<string, number>()
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Fold a freshly-loaded grading list into the live cache WITHOUT dropping live-only rows:
|
|
37
|
+
* a grading pushed via `upsert` while the load was in flight may not be in the server's
|
|
38
|
+
* response yet, and a blind replace would silently drop it. Loaded rows are authoritative
|
|
39
|
+
* for the ids they carry (keeping whichever `updatedAt` is greater on a shared id), and any
|
|
40
|
+
* live-only rows the response hasn't caught up to are preserved. Gradings are append/update-
|
|
41
|
+
* only (never deleted), so preserving an unmatched live row can't resurrect stale state.
|
|
42
|
+
* Returns the reconciled loaded rows and the surviving live-only rows separately so each
|
|
43
|
+
* caller can splice them in its own order (execution cache appends; screen history, which is
|
|
44
|
+
* newest-first, prepends).
|
|
45
|
+
*/
|
|
46
|
+
function reconcileWithLive(loaded: KaizenGrading[], existing: KaizenGrading[]) {
|
|
47
|
+
const loadedIds = new Set(loaded.map((g) => g.id))
|
|
48
|
+
const reconciled = loaded.map((l) => {
|
|
49
|
+
const live = existing.find((e) => e.id === l.id)
|
|
50
|
+
return live && live.updatedAt > l.updatedAt ? live : l
|
|
51
|
+
})
|
|
52
|
+
const liveOnly = existing.filter((e) => !loadedIds.has(e.id))
|
|
53
|
+
return { reconciled, liveOnly }
|
|
54
|
+
}
|
|
55
|
+
|
|
26
56
|
function gradingsFor(executionId: string): KaizenGrading[] {
|
|
27
57
|
return byExecution.value[executionId] ?? []
|
|
28
58
|
}
|
|
@@ -35,11 +65,18 @@ export const useKaizenStore = defineStore('kaizen', () => {
|
|
|
35
65
|
async function loadOverview() {
|
|
36
66
|
const ws = useWorkspaceStore()
|
|
37
67
|
loadingOverview.value = true
|
|
68
|
+
const seq = ++loadTicket
|
|
69
|
+
latestOverviewLoad = seq
|
|
38
70
|
try {
|
|
39
71
|
const overview = await api.getKaizenOverview(ws.requireId())
|
|
40
|
-
history.value = overview.gradings
|
|
41
|
-
verified.value = overview.verified
|
|
42
72
|
available.value = true
|
|
73
|
+
// A newer overview load superseded this one while it was in flight — discard the staler
|
|
74
|
+
// result so it can't clobber the fresher history (and any grading live-pushed since).
|
|
75
|
+
if (latestOverviewLoad !== seq) return
|
|
76
|
+
verified.value = overview.verified
|
|
77
|
+
// History is newest-first; live-pushed gradings are the newest, so prepend the survivors.
|
|
78
|
+
const { reconciled, liveOnly } = reconcileWithLive(overview.gradings, history.value)
|
|
79
|
+
history.value = [...liveOnly, ...reconciled]
|
|
43
80
|
} catch (e) {
|
|
44
81
|
if ((e as { statusCode?: number; status?: number })?.statusCode === 503)
|
|
45
82
|
available.value = false
|
|
@@ -52,10 +89,20 @@ export const useKaizenStore = defineStore('kaizen', () => {
|
|
|
52
89
|
async function loadForExecution(executionId: string) {
|
|
53
90
|
const ws = useWorkspaceStore()
|
|
54
91
|
loadingExecution.value = new Set(loadingExecution.value).add(executionId)
|
|
92
|
+
const seq = ++loadTicket
|
|
93
|
+
latestExecLoad.set(executionId, seq)
|
|
55
94
|
try {
|
|
56
95
|
const { gradings } = await api.getKaizenForExecution(ws.requireId(), executionId)
|
|
57
|
-
byExecution.value = { ...byExecution.value, [executionId]: gradings }
|
|
58
96
|
available.value = true
|
|
97
|
+
// A newer load for this execution (or a live `upsert`) may have landed while this fetch
|
|
98
|
+
// was in flight — discard a superseded load, and merge rather than blind-replace so a
|
|
99
|
+
// grading pushed live mid-flight isn't dropped.
|
|
100
|
+
if (latestExecLoad.get(executionId) !== seq) return
|
|
101
|
+
const { reconciled, liveOnly } = reconcileWithLive(
|
|
102
|
+
gradings,
|
|
103
|
+
byExecution.value[executionId] ?? [],
|
|
104
|
+
)
|
|
105
|
+
byExecution.value = { ...byExecution.value, [executionId]: [...reconciled, ...liveOnly] }
|
|
59
106
|
} catch (e) {
|
|
60
107
|
if ((e as { statusCode?: number; status?: number })?.statusCode === 503)
|
|
61
108
|
available.value = false
|
package/i18n/locales/de.json
CHANGED
|
@@ -1174,7 +1174,8 @@
|
|
|
1174
1174
|
"merged": "Gemergt",
|
|
1175
1175
|
"open": "Öffnen",
|
|
1176
1176
|
"mergePr": "PR mergen",
|
|
1177
|
-
"chooseApproach": "Ansatz wählen"
|
|
1177
|
+
"chooseApproach": "Ansatz wählen",
|
|
1178
|
+
"elapsedTooltip": "Verstrichene Zeit für diesen Schritt"
|
|
1178
1179
|
},
|
|
1179
1180
|
"structure": {
|
|
1180
1181
|
"title": "Struktur",
|
|
@@ -1466,7 +1467,8 @@
|
|
|
1466
1467
|
"confirmArchive": {
|
|
1467
1468
|
"title": "Diesen Dienst archivieren?",
|
|
1468
1469
|
"body": "„{name}“ und seine Aufgaben werden vom Board ausgeblendet. Du kannst den Dienst jederzeit wiederherstellen."
|
|
1469
|
-
}
|
|
1470
|
+
},
|
|
1471
|
+
"runBlocked": "Blockiert durch eine unerledigte Abhängigkeit: {names} | Blockiert durch {count} unerledigte Abhängigkeiten: {names}"
|
|
1470
1472
|
}
|
|
1471
1473
|
},
|
|
1472
1474
|
"layout": {
|
|
@@ -2123,7 +2125,12 @@
|
|
|
2123
2125
|
"bootstrapStopped": "Bootstrap gestoppt",
|
|
2124
2126
|
"runStopped": "Lauf gestoppt",
|
|
2125
2127
|
"stoppedDescription": "Der Container wurde beendet und der Lauf abgebrochen.",
|
|
2126
|
-
"stopFailed": "Stoppen fehlgeschlagen"
|
|
2128
|
+
"stopFailed": "Stoppen fehlgeschlagen",
|
|
2129
|
+
"confirm": {
|
|
2130
|
+
"title": "Diesen Lauf stoppen?",
|
|
2131
|
+
"body": "Der laufende Container wird beendet. Der Lauf bleibt sichtbar und kann wiederholt werden.",
|
|
2132
|
+
"confirm": "Lauf stoppen"
|
|
2133
|
+
}
|
|
2127
2134
|
},
|
|
2128
2135
|
"frame": {
|
|
2129
2136
|
"status": {
|
|
@@ -3011,7 +3018,8 @@
|
|
|
3011
3018
|
"forkDecision": {
|
|
3012
3019
|
"proposing": "Ansätze werden vorgeschlagen…",
|
|
3013
3020
|
"choose": "Ansatz wählen"
|
|
3014
|
-
}
|
|
3021
|
+
},
|
|
3022
|
+
"elapsedTooltip": "Verstrichene Zeit für diesen Schritt"
|
|
3015
3023
|
},
|
|
3016
3024
|
"health": {
|
|
3017
3025
|
"title": "Pipeline-Zustand",
|
package/i18n/locales/en.json
CHANGED
|
@@ -347,7 +347,12 @@
|
|
|
347
347
|
"bootstrapStopped": "Bootstrap stopped",
|
|
348
348
|
"runStopped": "Run stopped",
|
|
349
349
|
"stoppedDescription": "The container was killed and the run was cancelled.",
|
|
350
|
-
"stopFailed": "Stop failed"
|
|
350
|
+
"stopFailed": "Stop failed",
|
|
351
|
+
"confirm": {
|
|
352
|
+
"title": "Stop this run?",
|
|
353
|
+
"body": "The running container will be killed. The run stays visible and can be retried.",
|
|
354
|
+
"confirm": "Stop run"
|
|
355
|
+
}
|
|
351
356
|
},
|
|
352
357
|
"frame": {
|
|
353
358
|
"status": {
|
|
@@ -913,7 +918,8 @@
|
|
|
913
918
|
"merged": "Merged",
|
|
914
919
|
"open": "Open",
|
|
915
920
|
"mergePr": "Merge PR",
|
|
916
|
-
"chooseApproach": "Choose approach"
|
|
921
|
+
"chooseApproach": "Choose approach",
|
|
922
|
+
"elapsedTooltip": "Elapsed time on this step"
|
|
917
923
|
},
|
|
918
924
|
"structure": {
|
|
919
925
|
"title": "Structure",
|
|
@@ -1205,6 +1211,10 @@
|
|
|
1205
1211
|
"confirmArchive": {
|
|
1206
1212
|
"title": "Archive this service?",
|
|
1207
1213
|
"body": "\"{name}\" and its tasks will be hidden from the board. You can restore it at any time."
|
|
1214
|
+
},
|
|
1215
|
+
"runBlocked": "Blocked by an unfinished dependency: {names} | Blocked by {count} unfinished dependencies: {names}",
|
|
1216
|
+
"@runBlocked": {
|
|
1217
|
+
"description": "Count-driven plural: {count} unfinished dependencies, {names} their comma-joined titles. Languages with more than two plural forms (e.g. Polish, Ukrainian: one/few/many) need the extra pipe-separated forms."
|
|
1208
1218
|
}
|
|
1209
1219
|
}
|
|
1210
1220
|
},
|
|
@@ -3324,7 +3334,8 @@
|
|
|
3324
3334
|
"forkDecision": {
|
|
3325
3335
|
"proposing": "Proposing approaches…",
|
|
3326
3336
|
"choose": "Choose an approach"
|
|
3327
|
-
}
|
|
3337
|
+
},
|
|
3338
|
+
"elapsedTooltip": "Elapsed time on this step"
|
|
3328
3339
|
},
|
|
3329
3340
|
"health": {
|
|
3330
3341
|
"title": "Pipeline health",
|
package/i18n/locales/es.json
CHANGED
|
@@ -320,7 +320,12 @@
|
|
|
320
320
|
"bootstrapStopped": "Arranque detenido",
|
|
321
321
|
"runStopped": "Ejecución detenida",
|
|
322
322
|
"stoppedDescription": "El contenedor se cerró y la ejecución se canceló.",
|
|
323
|
-
"stopFailed": "No se pudo detener"
|
|
323
|
+
"stopFailed": "No se pudo detener",
|
|
324
|
+
"confirm": {
|
|
325
|
+
"title": "¿Detener esta ejecución?",
|
|
326
|
+
"body": "Se detendrá el contenedor en ejecución. La ejecución seguirá visible y podrá reintentarse.",
|
|
327
|
+
"confirm": "Detener ejecución"
|
|
328
|
+
}
|
|
324
329
|
},
|
|
325
330
|
"frame": {
|
|
326
331
|
"status": {
|
|
@@ -859,7 +864,8 @@
|
|
|
859
864
|
"title": "Aún no hay ejecuciones",
|
|
860
865
|
"body": "Inicia un pipeline para ver el historial de ejecución aquí."
|
|
861
866
|
},
|
|
862
|
-
"chooseApproach": "Elegir enfoque"
|
|
867
|
+
"chooseApproach": "Elegir enfoque",
|
|
868
|
+
"elapsedTooltip": "Tiempo transcurrido en este paso"
|
|
863
869
|
},
|
|
864
870
|
"structure": {
|
|
865
871
|
"title": "Estructura",
|
|
@@ -1151,7 +1157,8 @@
|
|
|
1151
1157
|
"confirmArchive": {
|
|
1152
1158
|
"title": "¿Archivar este servicio?",
|
|
1153
1159
|
"body": "«{name}» y sus tareas se ocultarán del tablero. Puedes restaurarlo en cualquier momento."
|
|
1154
|
-
}
|
|
1160
|
+
},
|
|
1161
|
+
"runBlocked": "Bloqueado por una dependencia sin terminar: {names} | Bloqueado por {count} dependencias sin terminar: {names}"
|
|
1155
1162
|
}
|
|
1156
1163
|
},
|
|
1157
1164
|
"observability": {
|
|
@@ -3237,7 +3244,8 @@
|
|
|
3237
3244
|
"forkDecision": {
|
|
3238
3245
|
"proposing": "Proponiendo enfoques…",
|
|
3239
3246
|
"choose": "Elegir un enfoque"
|
|
3240
|
-
}
|
|
3247
|
+
},
|
|
3248
|
+
"elapsedTooltip": "Tiempo transcurrido en este paso"
|
|
3241
3249
|
},
|
|
3242
3250
|
"health": {
|
|
3243
3251
|
"title": "Estado de los pipelines",
|
package/i18n/locales/fr.json
CHANGED
|
@@ -320,7 +320,12 @@
|
|
|
320
320
|
"bootstrapStopped": "Initialisation arrêtée",
|
|
321
321
|
"runStopped": "Exécution arrêtée",
|
|
322
322
|
"stoppedDescription": "Le conteneur a été tué et l’exécution annulée.",
|
|
323
|
-
"stopFailed": "Échec de l’arrêt"
|
|
323
|
+
"stopFailed": "Échec de l’arrêt",
|
|
324
|
+
"confirm": {
|
|
325
|
+
"title": "Arrêter cette exécution ?",
|
|
326
|
+
"body": "Le conteneur en cours sera arrêté. L'exécution reste visible et peut être relancée.",
|
|
327
|
+
"confirm": "Arrêter l'exécution"
|
|
328
|
+
}
|
|
324
329
|
},
|
|
325
330
|
"frame": {
|
|
326
331
|
"status": {
|
|
@@ -859,7 +864,8 @@
|
|
|
859
864
|
"title": "Aucune exécution pour l'instant",
|
|
860
865
|
"body": "Lancez un pipeline pour voir l'historique d'exécution ici."
|
|
861
866
|
},
|
|
862
|
-
"chooseApproach": "Choisir l'approche"
|
|
867
|
+
"chooseApproach": "Choisir l'approche",
|
|
868
|
+
"elapsedTooltip": "Temps écoulé sur cette étape"
|
|
863
869
|
},
|
|
864
870
|
"structure": {
|
|
865
871
|
"title": "Structure",
|
|
@@ -1151,7 +1157,8 @@
|
|
|
1151
1157
|
"confirmArchive": {
|
|
1152
1158
|
"title": "Archiver ce service ?",
|
|
1153
1159
|
"body": "« {name} » et ses tâches seront masqués du tableau. Vous pouvez le restaurer à tout moment."
|
|
1154
|
-
}
|
|
1160
|
+
},
|
|
1161
|
+
"runBlocked": "Bloqué par une dépendance non terminée : {names} | Bloqué par {count} dépendances non terminées : {names}"
|
|
1155
1162
|
}
|
|
1156
1163
|
},
|
|
1157
1164
|
"observability": {
|
|
@@ -3237,7 +3244,8 @@
|
|
|
3237
3244
|
"forkDecision": {
|
|
3238
3245
|
"proposing": "Proposition d'approches…",
|
|
3239
3246
|
"choose": "Choisir une approche"
|
|
3240
|
-
}
|
|
3247
|
+
},
|
|
3248
|
+
"elapsedTooltip": "Temps écoulé sur cette étape"
|
|
3241
3249
|
},
|
|
3242
3250
|
"health": {
|
|
3243
3251
|
"title": "État des pipelines",
|
package/i18n/locales/he.json
CHANGED
|
@@ -320,7 +320,12 @@
|
|
|
320
320
|
"bootstrapStopped": "האתחול נעצר",
|
|
321
321
|
"runStopped": "הריצה נעצרה",
|
|
322
322
|
"stoppedDescription": "הקונטיינר הופסק והריצה בוטלה.",
|
|
323
|
-
"stopFailed": "העצירה נכשלה"
|
|
323
|
+
"stopFailed": "העצירה נכשלה",
|
|
324
|
+
"confirm": {
|
|
325
|
+
"title": "לעצור את ההרצה הזו?",
|
|
326
|
+
"body": "הקונטיינר הפעיל ייעצר. ההרצה תישאר גלויה וניתן יהיה לנסות אותה שוב.",
|
|
327
|
+
"confirm": "עצור הרצה"
|
|
328
|
+
}
|
|
324
329
|
},
|
|
325
330
|
"frame": {
|
|
326
331
|
"status": {
|
|
@@ -859,7 +864,8 @@
|
|
|
859
864
|
"title": "אין הרצות עדיין",
|
|
860
865
|
"body": "התחל pipeline כדי לראות כאן את היסטוריית ההרצות."
|
|
861
866
|
},
|
|
862
|
-
"chooseApproach": "בחר גישה"
|
|
867
|
+
"chooseApproach": "בחר גישה",
|
|
868
|
+
"elapsedTooltip": "הזמן שחלף בשלב זה"
|
|
863
869
|
},
|
|
864
870
|
"structure": {
|
|
865
871
|
"title": "מבנה",
|
|
@@ -1151,7 +1157,8 @@
|
|
|
1151
1157
|
"confirmArchive": {
|
|
1152
1158
|
"title": "להעביר שירות זה לארכיון?",
|
|
1153
1159
|
"body": "\"{name}\" והמשימות שלו יוסתרו מהלוח. אפשר לשחזר אותו בכל עת."
|
|
1154
|
-
}
|
|
1160
|
+
},
|
|
1161
|
+
"runBlocked": "חסום על ידי תלות אחת שלא הושלמה: {names} | חסום על ידי {count} תלויות שלא הושלמו: {names}"
|
|
1155
1162
|
}
|
|
1156
1163
|
},
|
|
1157
1164
|
"observability": {
|
|
@@ -3248,7 +3255,8 @@
|
|
|
3248
3255
|
"forkDecision": {
|
|
3249
3256
|
"proposing": "מציע גישות…",
|
|
3250
3257
|
"choose": "בחר גישה"
|
|
3251
|
-
}
|
|
3258
|
+
},
|
|
3259
|
+
"elapsedTooltip": "הזמן שחלף בשלב זה"
|
|
3252
3260
|
},
|
|
3253
3261
|
"health": {
|
|
3254
3262
|
"title": "בריאות הצינור",
|
package/i18n/locales/it.json
CHANGED
|
@@ -1174,7 +1174,8 @@
|
|
|
1174
1174
|
"merged": "Unita",
|
|
1175
1175
|
"open": "Aperta",
|
|
1176
1176
|
"mergePr": "Unisci PR",
|
|
1177
|
-
"chooseApproach": "Scegli approccio"
|
|
1177
|
+
"chooseApproach": "Scegli approccio",
|
|
1178
|
+
"elapsedTooltip": "Tempo trascorso su questo passaggio"
|
|
1178
1179
|
},
|
|
1179
1180
|
"structure": {
|
|
1180
1181
|
"title": "Struttura",
|
|
@@ -1466,7 +1467,8 @@
|
|
|
1466
1467
|
"confirmArchive": {
|
|
1467
1468
|
"title": "Archiviare questo servizio?",
|
|
1468
1469
|
"body": "\"{name}\" e le sue attività verranno nascosti dalla board. Puoi ripristinarlo in qualsiasi momento."
|
|
1469
|
-
}
|
|
1470
|
+
},
|
|
1471
|
+
"runBlocked": "Bloccato da una dipendenza non completata: {names} | Bloccato da {count} dipendenze non completate: {names}"
|
|
1470
1472
|
}
|
|
1471
1473
|
},
|
|
1472
1474
|
"layout": {
|
|
@@ -2123,7 +2125,12 @@
|
|
|
2123
2125
|
"bootstrapStopped": "Bootstrap interrotto",
|
|
2124
2126
|
"runStopped": "Esecuzione interrotta",
|
|
2125
2127
|
"stoppedDescription": "Il container è stato terminato e l'esecuzione è stata annullata.",
|
|
2126
|
-
"stopFailed": "Interruzione fallita"
|
|
2128
|
+
"stopFailed": "Interruzione fallita",
|
|
2129
|
+
"confirm": {
|
|
2130
|
+
"title": "Interrompere questa esecuzione?",
|
|
2131
|
+
"body": "Il container in esecuzione verrà terminato. L'esecuzione resta visibile e può essere ripetuta.",
|
|
2132
|
+
"confirm": "Interrompi esecuzione"
|
|
2133
|
+
}
|
|
2127
2134
|
},
|
|
2128
2135
|
"frame": {
|
|
2129
2136
|
"status": {
|
|
@@ -3011,7 +3018,8 @@
|
|
|
3011
3018
|
"forkDecision": {
|
|
3012
3019
|
"proposing": "Proposta di approcci…",
|
|
3013
3020
|
"choose": "Scegli un approccio"
|
|
3014
|
-
}
|
|
3021
|
+
},
|
|
3022
|
+
"elapsedTooltip": "Tempo trascorso su questo passaggio"
|
|
3015
3023
|
},
|
|
3016
3024
|
"health": {
|
|
3017
3025
|
"title": "Stato delle pipeline",
|
package/i18n/locales/ja.json
CHANGED
|
@@ -320,7 +320,12 @@
|
|
|
320
320
|
"bootstrapStopped": "ブートストラップを停止しました",
|
|
321
321
|
"runStopped": "実行を停止しました",
|
|
322
322
|
"stoppedDescription": "コンテナが終了され、実行はキャンセルされました。",
|
|
323
|
-
"stopFailed": "停止に失敗しました"
|
|
323
|
+
"stopFailed": "停止に失敗しました",
|
|
324
|
+
"confirm": {
|
|
325
|
+
"title": "この実行を停止しますか?",
|
|
326
|
+
"body": "実行中のコンテナが終了します。実行は表示されたまま残り、再試行できます。",
|
|
327
|
+
"confirm": "実行を停止"
|
|
328
|
+
}
|
|
324
329
|
},
|
|
325
330
|
"frame": {
|
|
326
331
|
"status": {
|
|
@@ -859,7 +864,8 @@
|
|
|
859
864
|
"title": "実行履歴はまだありません",
|
|
860
865
|
"body": "パイプラインを開始すると、ここに実行履歴が表示されます。"
|
|
861
866
|
},
|
|
862
|
-
"chooseApproach": "アプローチを選択"
|
|
867
|
+
"chooseApproach": "アプローチを選択",
|
|
868
|
+
"elapsedTooltip": "このステップの経過時間"
|
|
863
869
|
},
|
|
864
870
|
"structure": {
|
|
865
871
|
"title": "構造",
|
|
@@ -1151,7 +1157,8 @@
|
|
|
1151
1157
|
"confirmArchive": {
|
|
1152
1158
|
"title": "このサービスをアーカイブしますか?",
|
|
1153
1159
|
"body": "「{name}」とそのタスクはボードから非表示になります。いつでも復元できます。"
|
|
1154
|
-
}
|
|
1160
|
+
},
|
|
1161
|
+
"runBlocked": "未完了の依存関係によってブロックされています: {names} | {count} 件の未完了の依存関係によってブロックされています: {names}"
|
|
1155
1162
|
}
|
|
1156
1163
|
},
|
|
1157
1164
|
"observability": {
|
|
@@ -3249,7 +3256,8 @@
|
|
|
3249
3256
|
"forkDecision": {
|
|
3250
3257
|
"proposing": "アプローチを提案中…",
|
|
3251
3258
|
"choose": "アプローチを選択"
|
|
3252
|
-
}
|
|
3259
|
+
},
|
|
3260
|
+
"elapsedTooltip": "このステップの経過時間"
|
|
3253
3261
|
},
|
|
3254
3262
|
"health": {
|
|
3255
3263
|
"title": "パイプラインの状態",
|
package/i18n/locales/pl.json
CHANGED
|
@@ -320,7 +320,12 @@
|
|
|
320
320
|
"bootstrapStopped": "Zatrzymano inicjalizację",
|
|
321
321
|
"runStopped": "Zatrzymano uruchomienie",
|
|
322
322
|
"stoppedDescription": "Kontener został zabity, a uruchomienie anulowane.",
|
|
323
|
-
"stopFailed": "Nie udało się zatrzymać"
|
|
323
|
+
"stopFailed": "Nie udało się zatrzymać",
|
|
324
|
+
"confirm": {
|
|
325
|
+
"title": "Zatrzymać to uruchomienie?",
|
|
326
|
+
"body": "Działający kontener zostanie zatrzymany. Uruchomienie pozostanie widoczne i można je ponowić.",
|
|
327
|
+
"confirm": "Zatrzymaj uruchomienie"
|
|
328
|
+
}
|
|
324
329
|
},
|
|
325
330
|
"frame": {
|
|
326
331
|
"status": {
|
|
@@ -859,7 +864,8 @@
|
|
|
859
864
|
"title": "Brak uruchomień",
|
|
860
865
|
"body": "Uruchom pipeline, aby zobaczyć tutaj historię uruchomień."
|
|
861
866
|
},
|
|
862
|
-
"chooseApproach": "Wybierz podejście"
|
|
867
|
+
"chooseApproach": "Wybierz podejście",
|
|
868
|
+
"elapsedTooltip": "Czas, który upłynął na tym kroku"
|
|
863
869
|
},
|
|
864
870
|
"structure": {
|
|
865
871
|
"title": "Struktura",
|
|
@@ -1151,7 +1157,8 @@
|
|
|
1151
1157
|
"confirmArchive": {
|
|
1152
1158
|
"title": "Zarchiwizować tę usługę?",
|
|
1153
1159
|
"body": "„{name}” i jej zadania zostaną ukryte z tablicy. Możesz przywrócić usługę w dowolnej chwili."
|
|
1154
|
-
}
|
|
1160
|
+
},
|
|
1161
|
+
"runBlocked": "Zablokowane przez niedokończoną zależność: {names} | Zablokowane przez {count} niedokończone zależności: {names} | Zablokowane przez {count} niedokończonych zależności: {names}"
|
|
1155
1162
|
}
|
|
1156
1163
|
},
|
|
1157
1164
|
"observability": {
|
|
@@ -3237,7 +3244,8 @@
|
|
|
3237
3244
|
"forkDecision": {
|
|
3238
3245
|
"proposing": "Proponowanie podejść…",
|
|
3239
3246
|
"choose": "Wybierz podejście"
|
|
3240
|
-
}
|
|
3247
|
+
},
|
|
3248
|
+
"elapsedTooltip": "Czas, który upłynął na tym kroku"
|
|
3241
3249
|
},
|
|
3242
3250
|
"health": {
|
|
3243
3251
|
"title": "Stan pipeline'ów",
|
package/i18n/locales/tr.json
CHANGED
|
@@ -320,7 +320,12 @@
|
|
|
320
320
|
"bootstrapStopped": "Bootstrap durduruldu",
|
|
321
321
|
"runStopped": "Çalıştırma durduruldu",
|
|
322
322
|
"stoppedDescription": "Konteyner sonlandırıldı ve çalıştırma iptal edildi.",
|
|
323
|
-
"stopFailed": "Durdurma başarısız oldu"
|
|
323
|
+
"stopFailed": "Durdurma başarısız oldu",
|
|
324
|
+
"confirm": {
|
|
325
|
+
"title": "Bu çalıştırma durdurulsun mu?",
|
|
326
|
+
"body": "Çalışan konteyner sonlandırılacak. Çalıştırma görünür kalır ve yeniden denenebilir.",
|
|
327
|
+
"confirm": "Çalıştırmayı durdur"
|
|
328
|
+
}
|
|
324
329
|
},
|
|
325
330
|
"frame": {
|
|
326
331
|
"status": {
|
|
@@ -859,7 +864,8 @@
|
|
|
859
864
|
"title": "Henüz çalıştırma yok",
|
|
860
865
|
"body": "Çalıştırma geçmişini burada görmek için bir pipeline başlatın."
|
|
861
866
|
},
|
|
862
|
-
"chooseApproach": "Yaklaşım seç"
|
|
867
|
+
"chooseApproach": "Yaklaşım seç",
|
|
868
|
+
"elapsedTooltip": "Bu adımda geçen süre"
|
|
863
869
|
},
|
|
864
870
|
"structure": {
|
|
865
871
|
"title": "Yapı",
|
|
@@ -1151,7 +1157,8 @@
|
|
|
1151
1157
|
"confirmArchive": {
|
|
1152
1158
|
"title": "Bu hizmet arşivlensin mi?",
|
|
1153
1159
|
"body": "\"{name}\" ve görevleri panodan gizlenecek. İstediğin zaman geri yükleyebilirsin."
|
|
1154
|
-
}
|
|
1160
|
+
},
|
|
1161
|
+
"runBlocked": "Tamamlanmamış bir bağımlılık tarafından engellendi: {names} | Tamamlanmamış {count} bağımlılık tarafından engellendi: {names}"
|
|
1155
1162
|
}
|
|
1156
1163
|
},
|
|
1157
1164
|
"observability": {
|
|
@@ -3249,7 +3256,8 @@
|
|
|
3249
3256
|
"forkDecision": {
|
|
3250
3257
|
"proposing": "Yaklaşımlar öneriliyor…",
|
|
3251
3258
|
"choose": "Bir yaklaşım seçin"
|
|
3252
|
-
}
|
|
3259
|
+
},
|
|
3260
|
+
"elapsedTooltip": "Bu adımda geçen süre"
|
|
3253
3261
|
},
|
|
3254
3262
|
"health": {
|
|
3255
3263
|
"title": "Pipeline sağlığı",
|
package/i18n/locales/uk.json
CHANGED
|
@@ -320,7 +320,12 @@
|
|
|
320
320
|
"bootstrapStopped": "Ініціалізацію зупинено",
|
|
321
321
|
"runStopped": "Запуск зупинено",
|
|
322
322
|
"stoppedDescription": "Контейнер було зупинено, а запуск скасовано.",
|
|
323
|
-
"stopFailed": "Не вдалося зупинити"
|
|
323
|
+
"stopFailed": "Не вдалося зупинити",
|
|
324
|
+
"confirm": {
|
|
325
|
+
"title": "Зупинити цей запуск?",
|
|
326
|
+
"body": "Запущений контейнер буде зупинено. Запуск залишиться видимим, і його можна повторити.",
|
|
327
|
+
"confirm": "Зупинити запуск"
|
|
328
|
+
}
|
|
324
329
|
},
|
|
325
330
|
"frame": {
|
|
326
331
|
"status": {
|
|
@@ -859,7 +864,8 @@
|
|
|
859
864
|
"title": "Ще немає запусків",
|
|
860
865
|
"body": "Запустіть pipeline, щоб побачити тут історію запусків."
|
|
861
866
|
},
|
|
862
|
-
"chooseApproach": "Обрати підхід"
|
|
867
|
+
"chooseApproach": "Обрати підхід",
|
|
868
|
+
"elapsedTooltip": "Час, що минув на цьому кроці"
|
|
863
869
|
},
|
|
864
870
|
"structure": {
|
|
865
871
|
"title": "Структура",
|
|
@@ -1151,7 +1157,8 @@
|
|
|
1151
1157
|
"confirmArchive": {
|
|
1152
1158
|
"title": "Заархівувати цей сервіс?",
|
|
1153
1159
|
"body": "«{name}» та його завдання буде приховано з дошки. Ви можете відновити його будь-коли."
|
|
1154
|
-
}
|
|
1160
|
+
},
|
|
1161
|
+
"runBlocked": "Заблоковано незавершеною залежністю: {names} | Заблоковано {count} незавершеними залежностями: {names} | Заблоковано {count} незавершеними залежностями: {names}"
|
|
1155
1162
|
}
|
|
1156
1163
|
},
|
|
1157
1164
|
"observability": {
|
|
@@ -3237,7 +3244,8 @@
|
|
|
3237
3244
|
"forkDecision": {
|
|
3238
3245
|
"proposing": "Пропонування підходів…",
|
|
3239
3246
|
"choose": "Оберіть підхід"
|
|
3240
|
-
}
|
|
3247
|
+
},
|
|
3248
|
+
"elapsedTooltip": "Час, що минув на цьому кроці"
|
|
3241
3249
|
},
|
|
3242
3250
|
"health": {
|
|
3243
3251
|
"title": "Стан пайплайнів",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.115.
|
|
3
|
+
"version": "0.115.2",
|
|
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",
|