@cat-factory/app 0.163.0 → 0.164.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/app/components/board/RecurringPipelineModal.vue +9 -22
- package/app/components/focus/BlockFocusView.vue +25 -19
- package/app/components/pipeline/PipelinePicker.vue +38 -16
- package/app/components/pipeline/PipelinePreview.vue +65 -23
- package/app/components/tasks/ContextIssuePicker.logic.spec.ts +89 -0
- package/app/components/tasks/ContextIssuePicker.logic.ts +74 -0
- package/app/components/tasks/ContextIssuePicker.vue +71 -21
- package/app/components/tasks/TaskImportModal.vue +2 -4
- package/app/utils/pipeline.spec.ts +41 -1
- package/app/utils/pipeline.ts +9 -0
- package/i18n/locales/de.json +8 -2
- package/i18n/locales/en.json +8 -2
- package/i18n/locales/es.json +8 -2
- package/i18n/locales/fr.json +8 -2
- package/i18n/locales/he.json +8 -2
- package/i18n/locales/it.json +8 -2
- package/i18n/locales/ja.json +8 -2
- package/i18n/locales/pl.json +8 -2
- package/i18n/locales/tr.json +8 -2
- package/i18n/locales/uk.json +8 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,7 +77,10 @@ example ships in [`deploy/frontend`](../../deploy/frontend) (the `acme:security`
|
|
|
77
77
|
structure, dependencies, model + fragment picker, live execution, and linked
|
|
78
78
|
docs/issues/scenarios. Decisions resolve via `DecisionModal`.
|
|
79
79
|
- **Pipeline builder** (`components/pipeline`) — assemble/edit agent chains and
|
|
80
|
-
watch `PipelineProgress`.
|
|
80
|
+
watch `PipelineProgress`. `PipelinePicker` (+ its `PipelinePreview` pane) is the
|
|
81
|
+
single way a pipeline is chosen anywhere — add-task, run settings, the recurring
|
|
82
|
+
schedule, the focus view's Run menu — so every surface explains a pipeline by the
|
|
83
|
+
ordered steps it will run rather than by its name alone.
|
|
81
84
|
- **Integrations** — modals/panels for `github` (the source-control panel, shared
|
|
82
85
|
by every VCS provider), `vcs` (the GitLab personal-access-token connect),
|
|
83
86
|
`bootstrap`, `documents`, `tasks`, `requirements` (review), `scenarios`
|
|
@@ -70,17 +70,7 @@ function defaultRecurrence(): Recurrence {
|
|
|
70
70
|
const selectablePipelines = computed(() =>
|
|
71
71
|
pipelines.pipelines.filter((p) => pipelineAllowedForSchedule(p, frame.value, board.blocks)),
|
|
72
72
|
)
|
|
73
|
-
const pipelineMenu = computed(() => [
|
|
74
|
-
selectablePipelines.value.map((p) => ({
|
|
75
|
-
label: p.name,
|
|
76
|
-
icon: 'i-lucide-workflow',
|
|
77
|
-
onSelect: () => (pipelineId.value = p.id),
|
|
78
|
-
})),
|
|
79
|
-
])
|
|
80
73
|
const selectedPipeline = computed(() => pipelines.getPipeline(pipelineId.value))
|
|
81
|
-
const selectedPipelineLabel = computed(
|
|
82
|
-
() => selectedPipeline.value?.name ?? t('board.recurring.pickPipeline'),
|
|
83
|
-
)
|
|
84
74
|
|
|
85
75
|
// Infer the template from the picked pipeline so the backend seeds the right block
|
|
86
76
|
// description (and so we know to show the tracker config).
|
|
@@ -269,19 +259,16 @@ async function add() {
|
|
|
269
259
|
/>
|
|
270
260
|
</UFormField>
|
|
271
261
|
|
|
262
|
+
<!-- The rich picker, not a name-only menu: which steps a schedule will run unattended,
|
|
263
|
+
every time it fires, is exactly what the person setting it up needs to see. A schedule
|
|
264
|
+
must name a pipeline, so there is no "none" row. -->
|
|
272
265
|
<UFormField :label="t('board.recurring.pipeline')" required>
|
|
273
|
-
<
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
trailing-icon="i-lucide-chevron-down"
|
|
280
|
-
class="w-full justify-between"
|
|
281
|
-
>
|
|
282
|
-
{{ selectedPipelineLabel }}
|
|
283
|
-
</UButton>
|
|
284
|
-
</UDropdownMenu>
|
|
266
|
+
<PipelinePicker
|
|
267
|
+
v-model="pipelineId"
|
|
268
|
+
:options="selectablePipelines"
|
|
269
|
+
:placeholder="t('board.recurring.pickPipeline')"
|
|
270
|
+
trigger-class="w-full justify-between"
|
|
271
|
+
/>
|
|
285
272
|
</UFormField>
|
|
286
273
|
|
|
287
274
|
<UFormField :label="t('board.recurring.prompt')">
|
|
@@ -31,17 +31,19 @@ const deps = computed(() =>
|
|
|
31
31
|
// Hide UI-testing pipelines when this block's frame has no UI to exercise, `'recurring'`-only
|
|
32
32
|
// pipelines (a manual run of one is refused server-side), and — for a `document` task — every
|
|
33
33
|
// non-document pipeline (per the `purpose` classifier) — see the backend gate.
|
|
34
|
-
const
|
|
34
|
+
const runOptions = computed(() => {
|
|
35
35
|
const frame = block.value ? board.serviceOf(block.value) : undefined
|
|
36
|
-
return pipelines.pipelines
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
label: p.name,
|
|
40
|
-
icon: 'i-lucide-play',
|
|
41
|
-
onSelect: () => block.value && execution.start(block.value.id, p),
|
|
42
|
-
}))
|
|
36
|
+
return pipelines.pipelines.filter((p) =>
|
|
37
|
+
pipelineAllowedForManualStart(p, frame, board.blocks, block.value?.taskType),
|
|
38
|
+
)
|
|
43
39
|
})
|
|
44
40
|
|
|
41
|
+
/** Start the picked pipeline immediately — the Run menu chooses an ACTION, it stores no default. */
|
|
42
|
+
function runPipeline(id: string) {
|
|
43
|
+
const pipeline = pipelines.getPipeline(id)
|
|
44
|
+
if (pipeline && block.value) void execution.start(block.value.id, pipeline)
|
|
45
|
+
}
|
|
46
|
+
|
|
45
47
|
function close() {
|
|
46
48
|
ui.focus(null)
|
|
47
49
|
}
|
|
@@ -97,17 +99,21 @@ function openApprovalFor(approvalId: string) {
|
|
|
97
99
|
{{ statusMeta.label }}
|
|
98
100
|
</UBadge>
|
|
99
101
|
<div class="ms-auto flex items-center gap-2">
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
102
|
+
<!-- The rich picker rather than a list of names: the run starts the moment a row is
|
|
103
|
+
clicked, so the preview is the only chance to see which agents it will run. -->
|
|
104
|
+
<PipelinePicker model-value="" :options="runOptions" @update:model-value="runPipeline">
|
|
105
|
+
<template #trigger>
|
|
106
|
+
<UButton
|
|
107
|
+
color="primary"
|
|
108
|
+
variant="soft"
|
|
109
|
+
size="sm"
|
|
110
|
+
icon="i-lucide-play"
|
|
111
|
+
trailing-icon="i-lucide-chevron-down"
|
|
112
|
+
>
|
|
113
|
+
{{ instance ? t('focus.rerunPipeline') : t('focus.runPipeline') }}
|
|
114
|
+
</UButton>
|
|
115
|
+
</template>
|
|
116
|
+
</PipelinePicker>
|
|
111
117
|
<IconButton
|
|
112
118
|
icon="i-lucide-x"
|
|
113
119
|
color="neutral"
|
|
@@ -1,25 +1,36 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// The rich pipeline picker used wherever a pipeline is chosen (add-task
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
2
|
+
// The rich pipeline picker used wherever a pipeline is chosen — as a stored default (add-task
|
|
3
|
+
// modal, inspector run settings, the recurring-schedule modal) or as an immediate action (the
|
|
4
|
+
// focus view's Run menu). A master–detail popover: the left column lists the selectable pipelines
|
|
5
|
+
// (plus an optional "none / choose at run time" row), and hovering a row reveals that pipeline's
|
|
6
|
+
// full preview — its description and every step it will run, in order — in the right column, so a
|
|
7
|
+
// user sees exactly what a pipeline does before picking it. The trigger is customizable via the
|
|
8
|
+
// `#trigger` slot (the inspector uses a bare icon button; the modal a full-width labelled one).
|
|
8
9
|
import { computed, ref } from 'vue'
|
|
9
10
|
import type { Pipeline } from '~/types/domain'
|
|
10
11
|
|
|
11
12
|
const props = withDefaults(
|
|
12
13
|
defineProps<{
|
|
13
|
-
/** Selected pipeline id, or '' for
|
|
14
|
+
/** Selected pipeline id, or '' for none. An action-mode caller passes a constant ''. */
|
|
14
15
|
modelValue: string
|
|
15
16
|
/** The pipelines offered (already filtered for the surface, e.g. manual-start allowed). */
|
|
16
17
|
options: Pipeline[]
|
|
17
|
-
/**
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Label for the "none" row (e.g. "Choose at run time" / "No default"). Omit where picking
|
|
20
|
+
* nothing is not a valid outcome — a schedule needs a pipeline, and a Run menu acts on the
|
|
21
|
+
* chosen row — and the row is left out entirely.
|
|
22
|
+
*/
|
|
23
|
+
noneLabel?: string
|
|
24
|
+
/**
|
|
25
|
+
* Default-trigger text while nothing is selected. Defaults to {@link noneLabel}, which reads
|
|
26
|
+
* correctly when the "none" row IS the empty state; a surface without that row supplies its
|
|
27
|
+
* own prompt ("Pick a pipeline").
|
|
28
|
+
*/
|
|
29
|
+
placeholder?: string
|
|
19
30
|
/** Extra classes for the default trigger button (e.g. full-width in the modal). */
|
|
20
31
|
triggerClass?: string
|
|
21
32
|
}>(),
|
|
22
|
-
{ triggerClass: '' },
|
|
33
|
+
{ noneLabel: undefined, placeholder: undefined, triggerClass: '' },
|
|
23
34
|
)
|
|
24
35
|
|
|
25
36
|
const emit = defineEmits<{ 'update:modelValue': [string] }>()
|
|
@@ -31,7 +42,9 @@ const open = ref(false)
|
|
|
31
42
|
const hoverId = ref<string | undefined>(undefined)
|
|
32
43
|
|
|
33
44
|
const selected = computed(() => props.options.find((p) => p.id === props.modelValue))
|
|
34
|
-
const triggerLabel = computed(
|
|
45
|
+
const triggerLabel = computed(
|
|
46
|
+
() => selected.value?.name ?? props.placeholder ?? props.noneLabel ?? '',
|
|
47
|
+
)
|
|
35
48
|
|
|
36
49
|
/** The pipeline the right pane previews: the hovered row, else the current selection. */
|
|
37
50
|
const previewPipeline = computed<Pipeline | null>(() => {
|
|
@@ -39,6 +52,17 @@ const previewPipeline = computed<Pipeline | null>(() => {
|
|
|
39
52
|
return id ? (props.options.find((p) => p.id === id) ?? null) : null
|
|
40
53
|
})
|
|
41
54
|
|
|
55
|
+
/**
|
|
56
|
+
* With no pipeline to preview the pane explains WHY it's empty, and the two reasons differ: the
|
|
57
|
+
* "none" row is a real choice worth describing, while a picker without one is simply waiting for
|
|
58
|
+
* a hover. Sharing one hint would tell a Run menu it has no default pipeline, which is nonsense.
|
|
59
|
+
*/
|
|
60
|
+
const emptyHint = computed(() =>
|
|
61
|
+
props.noneLabel !== undefined && (hoverId.value ?? props.modelValue) === ''
|
|
62
|
+
? t('pipeline.picker.noneHint')
|
|
63
|
+
: t('pipeline.picker.hoverHint'),
|
|
64
|
+
)
|
|
65
|
+
|
|
42
66
|
function choose(id: string) {
|
|
43
67
|
emit('update:modelValue', id)
|
|
44
68
|
open.value = false
|
|
@@ -63,13 +87,13 @@ function choose(id: string) {
|
|
|
63
87
|
|
|
64
88
|
<template #content>
|
|
65
89
|
<div
|
|
66
|
-
class="flex max-h-[
|
|
90
|
+
class="flex max-h-[28rem] w-[min(44rem,94vw)]"
|
|
67
91
|
data-testid="pipeline-picker-panel"
|
|
68
92
|
@mouseleave="hoverId = undefined"
|
|
69
93
|
>
|
|
70
94
|
<!-- left: selectable options -->
|
|
71
95
|
<ul class="w-1/2 shrink-0 overflow-y-auto border-e border-slate-800 p-1">
|
|
72
|
-
<li>
|
|
96
|
+
<li v-if="noneLabel !== undefined">
|
|
73
97
|
<button
|
|
74
98
|
type="button"
|
|
75
99
|
class="flex w-full items-center gap-2 rounded px-2 py-1.5 text-start text-sm hover:bg-slate-800/60"
|
|
@@ -110,9 +134,7 @@ function choose(id: string) {
|
|
|
110
134
|
<!-- right: preview of the hovered (or selected) pipeline -->
|
|
111
135
|
<div class="w-1/2 overflow-y-auto p-3">
|
|
112
136
|
<PipelinePreview v-if="previewPipeline" :pipeline="previewPipeline" />
|
|
113
|
-
<div v-else class="text-[12px] leading-snug text-slate-500">
|
|
114
|
-
{{ t('pipeline.picker.noneHint') }}
|
|
115
|
-
</div>
|
|
137
|
+
<div v-else class="text-[12px] leading-snug text-slate-500">{{ emptyHint }}</div>
|
|
116
138
|
</div>
|
|
117
139
|
</div>
|
|
118
140
|
</template>
|
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// A compact, read-only summary of a pipeline: its name, prose description (when authored), and the
|
|
3
|
-
// ordered list of
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// ordered list of steps it will actually run — each a numbered row carrying the agent's icon, label
|
|
4
|
+
// and catalog description, with the human approval gates flagged. The FULL list, not a step count:
|
|
5
|
+
// "what does this pipeline actually do" is the only question a picker's preview pane exists to
|
|
6
|
+
// answer, and a row of interchangeable chips answers just "how many". Shared by the pipeline
|
|
7
|
+
// pickers everywhere a pipeline is chosen, so the explanation is identical on every surface.
|
|
8
|
+
// Resolves each step's display metadata through the single `agentKindMeta` path (via
|
|
9
|
+
// <AgentKindIcon> and {@link stepDescription}), so a system/custom kind can never blow up the
|
|
10
|
+
// renderer.
|
|
7
11
|
import { computed } from 'vue'
|
|
8
12
|
import type { Pipeline } from '~/types/domain'
|
|
9
|
-
import {
|
|
13
|
+
import { agentKindMeta } from '~/utils/catalog'
|
|
14
|
+
import { pipelineDisplaySteps, pipelineGateCount } from '~/utils/pipeline'
|
|
10
15
|
|
|
11
16
|
const props = defineProps<{ pipeline: Pipeline }>()
|
|
12
17
|
const { t } = useI18n()
|
|
13
18
|
|
|
14
19
|
const steps = computed(() => pipelineDisplaySteps(props.pipeline))
|
|
20
|
+
const gateCount = computed(() => pipelineGateCount(props.pipeline))
|
|
21
|
+
|
|
22
|
+
/** What the agent at this step does — the same catalog prose the palette and step tooltips use. */
|
|
23
|
+
function stepDescription(kind: string): string {
|
|
24
|
+
return agentKindMeta(kind).description
|
|
25
|
+
}
|
|
15
26
|
</script>
|
|
16
27
|
|
|
17
28
|
<template>
|
|
@@ -24,26 +35,57 @@ const steps = computed(() => pipelineDisplaySteps(props.pipeline))
|
|
|
24
35
|
>
|
|
25
36
|
{{ pipeline.description }}
|
|
26
37
|
</p>
|
|
27
|
-
|
|
28
|
-
|
|
38
|
+
|
|
39
|
+
<div
|
|
40
|
+
class="flex flex-wrap items-center gap-x-3 gap-y-1 text-[10px] uppercase tracking-wide text-slate-500"
|
|
41
|
+
>
|
|
42
|
+
<span class="inline-flex items-center gap-1">
|
|
29
43
|
<UIcon name="i-lucide-workflow" class="h-3 w-3" />
|
|
30
44
|
{{ t('pipeline.preview.stepCount', { count: steps.length }, steps.length) }}
|
|
31
|
-
</
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<AgentKindIcon :kind="s.kind" show-label icon-class="h-3.5 w-3.5" />
|
|
39
|
-
<UIcon
|
|
40
|
-
v-if="s.gated"
|
|
41
|
-
name="i-lucide-shield-check"
|
|
42
|
-
class="h-3 w-3 text-amber-400"
|
|
43
|
-
:title="t('pipeline.preview.gated')"
|
|
44
|
-
/>
|
|
45
|
-
</li>
|
|
46
|
-
</ol>
|
|
45
|
+
</span>
|
|
46
|
+
<!-- Gates are the reason a run stops for a human, so they earn a headline of their own
|
|
47
|
+
rather than only the per-step marker below. -->
|
|
48
|
+
<span v-if="gateCount" class="inline-flex items-center gap-1 text-amber-500">
|
|
49
|
+
<UIcon name="i-lucide-shield-check" class="h-3 w-3" />
|
|
50
|
+
{{ t('pipeline.preview.gateCount', { count: gateCount }, gateCount) }}
|
|
51
|
+
</span>
|
|
47
52
|
</div>
|
|
53
|
+
|
|
54
|
+
<!-- The ordered steps. The number column doubles as the flow connector (a rule drawn between
|
|
55
|
+
consecutive numbers), so the list reads as a sequence rather than an unordered set. -->
|
|
56
|
+
<ol>
|
|
57
|
+
<li
|
|
58
|
+
v-for="(s, i) in steps"
|
|
59
|
+
:key="i"
|
|
60
|
+
class="flex gap-2"
|
|
61
|
+
data-testid="pipeline-preview-step"
|
|
62
|
+
:data-step-kind="s.kind"
|
|
63
|
+
>
|
|
64
|
+
<div class="flex flex-col items-center">
|
|
65
|
+
<span
|
|
66
|
+
class="flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-slate-800 font-mono text-[9px] tabular-nums text-slate-400"
|
|
67
|
+
>
|
|
68
|
+
{{ i + 1 }}
|
|
69
|
+
</span>
|
|
70
|
+
<span v-if="i < steps.length - 1" class="w-px flex-1 bg-slate-800" />
|
|
71
|
+
</div>
|
|
72
|
+
<div class="min-w-0 flex-1 pb-2">
|
|
73
|
+
<div class="flex items-center gap-1">
|
|
74
|
+
<AgentKindIcon :kind="s.kind" show-label icon-class="h-3.5 w-3.5" />
|
|
75
|
+
<UIcon
|
|
76
|
+
v-if="s.gated"
|
|
77
|
+
name="i-lucide-shield-check"
|
|
78
|
+
class="h-3 w-3 shrink-0 text-amber-400"
|
|
79
|
+
:title="t('pipeline.preview.gated')"
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
<!-- Clamped: the catalog prose runs long for some kinds, and <AgentKindIcon> already
|
|
83
|
+
carries the full text in its hover tooltip. -->
|
|
84
|
+
<p class="line-clamp-2 text-[11px] leading-snug text-slate-500">
|
|
85
|
+
{{ stepDescription(s.kind) }}
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
</li>
|
|
89
|
+
</ol>
|
|
48
90
|
</div>
|
|
49
91
|
</template>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { buildSourceChoices, reconcileSource } from './ContextIssuePicker.logic'
|
|
3
|
+
import type { TaskSourceState } from '~/types/domain'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The pure source-selection behind `<ContextIssuePicker>`. Pins what the always-visible
|
|
7
|
+
* selector promises: the tracker in use is named even when it is the only one, a tracker
|
|
8
|
+
* the workspace hasn't got yet is offered as something to ADD (worded for its actual
|
|
9
|
+
* state), and the selection stays valid as the offered set changes underneath it.
|
|
10
|
+
*/
|
|
11
|
+
const state = (source: string, { available = true, enabled = true } = {}): TaskSourceState =>
|
|
12
|
+
({
|
|
13
|
+
source,
|
|
14
|
+
label: source.toUpperCase(),
|
|
15
|
+
icon: `i-lucide-${source}`,
|
|
16
|
+
available,
|
|
17
|
+
enabled,
|
|
18
|
+
credentialFields: [],
|
|
19
|
+
refLabel: '',
|
|
20
|
+
refPlaceholder: '',
|
|
21
|
+
}) as unknown as TaskSourceState
|
|
22
|
+
|
|
23
|
+
describe('buildSourceChoices', () => {
|
|
24
|
+
it('lists a single offered tracker, marked active (the selector is never hidden)', () => {
|
|
25
|
+
const groups = buildSourceChoices([state('github')], 'github')
|
|
26
|
+
expect(groups).toEqual([
|
|
27
|
+
[
|
|
28
|
+
{
|
|
29
|
+
action: 'select',
|
|
30
|
+
source: 'github',
|
|
31
|
+
label: 'GITHUB',
|
|
32
|
+
icon: 'i-lucide-github',
|
|
33
|
+
active: true,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('marks only the selected tracker active', () => {
|
|
40
|
+
const [offered] = buildSourceChoices([state('github'), state('jira')], 'jira')
|
|
41
|
+
expect(offered!.map((c) => [c.source, 'active' in c && c.active])).toEqual([
|
|
42
|
+
['github', false],
|
|
43
|
+
['jira', true],
|
|
44
|
+
])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('offers an unavailable tracker as `connect` and an available-but-off one as `enable`', () => {
|
|
48
|
+
const [, addable] = buildSourceChoices(
|
|
49
|
+
[state('github'), state('jira', { available: false }), state('linear', { enabled: false })],
|
|
50
|
+
'github',
|
|
51
|
+
)
|
|
52
|
+
expect(addable).toEqual([
|
|
53
|
+
{ action: 'connect', source: 'jira', label: 'JIRA', icon: 'i-lucide-jira' },
|
|
54
|
+
{ action: 'enable', source: 'linear', label: 'LINEAR', icon: 'i-lucide-linear' },
|
|
55
|
+
])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('drops empty groups so the menu renders no stray separator', () => {
|
|
59
|
+
expect(buildSourceChoices([state('github')], 'github')).toHaveLength(1)
|
|
60
|
+
expect(buildSourceChoices([state('jira', { available: false })], undefined)).toHaveLength(1)
|
|
61
|
+
expect(buildSourceChoices([], undefined)).toEqual([])
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('reconcileSource', () => {
|
|
66
|
+
it('selects the tracker the user just went off to connect, once it is offered', () => {
|
|
67
|
+
expect(reconcileSource(['github', 'jira'], 'github', 'jira')).toBe('jira')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('keeps the current selection while the connect is still pending', () => {
|
|
71
|
+
expect(reconcileSource(['github'], 'github', 'jira')).toBe('github')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('keeps a still-offered selection', () => {
|
|
75
|
+
expect(reconcileSource(['github', 'jira'], 'jira', null)).toBe('jira')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('falls back to the first offered tracker when the selection stopped being offered', () => {
|
|
79
|
+
expect(reconcileSource(['github'], 'jira', null)).toBe('github')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('selects the first offered tracker when nothing is selected yet', () => {
|
|
83
|
+
expect(reconcileSource(['github'], undefined, null)).toBe('github')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('resolves to nothing when the workspace offers no tracker at all', () => {
|
|
87
|
+
expect(reconcileSource([], 'jira', 'jira')).toBeUndefined()
|
|
88
|
+
})
|
|
89
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { TaskSourceKind, TaskSourceState } from '~/types/domain'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure source-selection logic for `<ContextIssuePicker>`: which tracker the picker is
|
|
5
|
+
* searching, and what the source menu offers. Kept out of the component so both the
|
|
6
|
+
* template's `computed`s and the unit spec call the same code.
|
|
7
|
+
*
|
|
8
|
+
* The menu is deliberately two-tier — pick an already-offered tracker, or go and add
|
|
9
|
+
* one — because "attach a context issue" is where a user first discovers a tracker is
|
|
10
|
+
* missing, and sending them to the Integrations hub loses their in-progress task.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** One row of the picker's source menu. */
|
|
14
|
+
export type SourceChoice =
|
|
15
|
+
/** An offered tracker the picker can search right now. */
|
|
16
|
+
| { action: 'select'; source: TaskSourceKind; label: string; icon: string; active: boolean }
|
|
17
|
+
/**
|
|
18
|
+
* A configured tracker that is not offered yet, so it can be added from here. `connect`
|
|
19
|
+
* has no credential/App behind it; `enable` is connected but toggled off for the
|
|
20
|
+
* workspace. Both open the same connect modal, which serves either case — only the
|
|
21
|
+
* wording differs, so the user isn't told to "connect" something already connected.
|
|
22
|
+
*/
|
|
23
|
+
| { action: 'connect' | 'enable'; source: TaskSourceKind; label: string; icon: string }
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The picker's source menu, as non-empty groups (the offered trackers, then the ones the
|
|
27
|
+
* user could add). Empty groups are dropped so the menu never renders a stray separator.
|
|
28
|
+
*/
|
|
29
|
+
export function buildSourceChoices(
|
|
30
|
+
sources: TaskSourceState[],
|
|
31
|
+
selected: TaskSourceKind | undefined,
|
|
32
|
+
): SourceChoice[][] {
|
|
33
|
+
const offered: SourceChoice[] = []
|
|
34
|
+
const addable: SourceChoice[] = []
|
|
35
|
+
for (const s of sources) {
|
|
36
|
+
if (s.available && s.enabled) {
|
|
37
|
+
offered.push({
|
|
38
|
+
action: 'select',
|
|
39
|
+
source: s.source,
|
|
40
|
+
label: s.label,
|
|
41
|
+
icon: s.icon,
|
|
42
|
+
active: s.source === selected,
|
|
43
|
+
})
|
|
44
|
+
} else {
|
|
45
|
+
addable.push({
|
|
46
|
+
action: s.available ? 'enable' : 'connect',
|
|
47
|
+
source: s.source,
|
|
48
|
+
label: s.label,
|
|
49
|
+
icon: s.icon,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return [offered, addable].filter((group) => group.length > 0)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The source the picker should hold once the offered set changes — after a connect, a
|
|
58
|
+
* disconnect, or the per-workspace toggle flipping elsewhere.
|
|
59
|
+
*
|
|
60
|
+
* `awaiting` is the tracker the user just left to connect: the moment it becomes offered
|
|
61
|
+
* it wins, so they land back on the source they went to add rather than on whatever was
|
|
62
|
+
* selected before. Otherwise a still-offered selection is kept, and a selection that
|
|
63
|
+
* stopped being offered falls back to the first one (searching a tracker the workspace no
|
|
64
|
+
* longer offers only yields errors).
|
|
65
|
+
*/
|
|
66
|
+
export function reconcileSource(
|
|
67
|
+
offered: TaskSourceKind[],
|
|
68
|
+
selected: TaskSourceKind | undefined,
|
|
69
|
+
awaiting: TaskSourceKind | null,
|
|
70
|
+
): TaskSourceKind | undefined {
|
|
71
|
+
if (awaiting && offered.includes(awaiting)) return awaiting
|
|
72
|
+
if (selected && offered.includes(selected)) return selected
|
|
73
|
+
return offered[0]
|
|
74
|
+
}
|
|
@@ -6,8 +6,15 @@
|
|
|
6
6
|
// It only *stages* a choice: the caller collects PendingContext items and links
|
|
7
7
|
// them once the block exists (see useContextLinking). A search hit / pasted ref
|
|
8
8
|
// carries `needsImport: true` so it's fetched + persisted before linking.
|
|
9
|
+
//
|
|
10
|
+
// The tracker being searched is ALWAYS on screen (even when the workspace offers
|
|
11
|
+
// exactly one), and its menu doubles as the "add a tracker" affordance: attaching a
|
|
12
|
+
// context issue is where a missing integration is discovered, and the connect modal
|
|
13
|
+
// opens over the caller's form rather than navigating away from it.
|
|
14
|
+
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
9
15
|
import type { SourceTask, TaskSearchResult, TaskSourceKind } from '~/types/domain'
|
|
10
16
|
import EmptyState from '~/components/common/EmptyState.vue'
|
|
17
|
+
import { buildSourceChoices, reconcileSource } from '~/components/tasks/ContextIssuePicker.logic'
|
|
11
18
|
|
|
12
19
|
const props = defineProps<{
|
|
13
20
|
/** contextKeys already staged by the caller, so they're filtered out / not re-offered. */
|
|
@@ -23,13 +30,6 @@ const props = defineProps<{
|
|
|
23
30
|
* `v-model:source`); omitted, the picker manages it internally (the add-task case).
|
|
24
31
|
*/
|
|
25
32
|
source?: TaskSourceKind
|
|
26
|
-
/**
|
|
27
|
-
* Always render the source selector, even with a single offered tracker — so the
|
|
28
|
-
* user can see *which* tracker is being searched (the "create task from issue"
|
|
29
|
-
* surface, where the source is otherwise invisible). Off by default: the inline
|
|
30
|
-
* add-task picker stays compact and only shows a selector when there's a choice.
|
|
31
|
-
*/
|
|
32
|
-
alwaysShowSource?: boolean
|
|
33
33
|
}>()
|
|
34
34
|
const emit = defineEmits<{
|
|
35
35
|
pick: [item: PendingContext]
|
|
@@ -38,12 +38,14 @@ const emit = defineEmits<{
|
|
|
38
38
|
|
|
39
39
|
const { t } = useI18n()
|
|
40
40
|
const tasks = useTasksStore()
|
|
41
|
+
const ui = useUiStore()
|
|
41
42
|
|
|
42
43
|
const chosen = computed(() => new Set(props.chosenKeys ?? []))
|
|
43
44
|
|
|
44
45
|
// Source: default to the first offered tracker. Controlled when the parent passes
|
|
45
|
-
// `source` (write-through to `update:source`), else internal.
|
|
46
|
-
//
|
|
46
|
+
// `source` (write-through to `update:source`), else internal. The selector is always
|
|
47
|
+
// rendered, single tracker or not — which tracker is being searched decides what a
|
|
48
|
+
// pasted key resolves to, so it must never be invisible.
|
|
47
49
|
const internalSource = ref<TaskSourceKind | undefined>(tasks.offeredSources[0]?.source)
|
|
48
50
|
const source = computed<TaskSourceKind | undefined>({
|
|
49
51
|
get: () => props.source ?? internalSource.value,
|
|
@@ -52,13 +54,49 @@ const source = computed<TaskSourceKind | undefined>({
|
|
|
52
54
|
if (v) emit('update:source', v)
|
|
53
55
|
},
|
|
54
56
|
})
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
+
const descriptor = computed(() => (source.value ? tasks.descriptorFor(source.value) : undefined))
|
|
58
|
+
|
|
59
|
+
// The tracker the user left to connect, so it becomes the selection the moment it turns
|
|
60
|
+
// up offered (the connect modal re-probes on success). Also the reconcile trigger for a
|
|
61
|
+
// source that stops being offered — disconnected, or toggled off in settings.
|
|
62
|
+
const awaitingConnect = ref<TaskSourceKind | null>(null)
|
|
63
|
+
function addSource(s: TaskSourceKind) {
|
|
64
|
+
awaitingConnect.value = s
|
|
65
|
+
ui.openTaskConnect(s)
|
|
66
|
+
}
|
|
67
|
+
watch(
|
|
68
|
+
() => tasks.offeredSources.map((s) => s.source),
|
|
69
|
+
(offered) => {
|
|
70
|
+
const next = reconcileSource(offered, source.value, awaitingConnect.value)
|
|
71
|
+
if (next && next === awaitingConnect.value) awaitingConnect.value = null
|
|
72
|
+
if (next !== source.value && next) source.value = next
|
|
73
|
+
},
|
|
57
74
|
)
|
|
58
|
-
|
|
59
|
-
|
|
75
|
+
|
|
76
|
+
// Two-tier menu: pick an offered tracker, or add one that isn't offered yet.
|
|
77
|
+
const sourceMenu = computed<DropdownMenuItem[][]>(() =>
|
|
78
|
+
buildSourceChoices(tasks.sources, source.value).map((group) =>
|
|
79
|
+
group.map((choice) =>
|
|
80
|
+
choice.action === 'select'
|
|
81
|
+
? {
|
|
82
|
+
label: choice.label,
|
|
83
|
+
icon: choice.icon,
|
|
84
|
+
trailingIcon: choice.active ? 'i-lucide-check' : undefined,
|
|
85
|
+
onSelect: () => {
|
|
86
|
+
source.value = choice.source
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
: {
|
|
90
|
+
label:
|
|
91
|
+
choice.action === 'enable'
|
|
92
|
+
? t('tasks.picker.enableSource', { label: choice.label })
|
|
93
|
+
: t('tasks.picker.connectSource', { label: choice.label }),
|
|
94
|
+
icon: 'i-lucide-plug',
|
|
95
|
+
onSelect: () => addSource(choice.source),
|
|
96
|
+
},
|
|
97
|
+
),
|
|
98
|
+
),
|
|
60
99
|
)
|
|
61
|
-
const descriptor = computed(() => (source.value ? tasks.descriptorFor(source.value) : undefined))
|
|
62
100
|
const searchable = computed(() => descriptor.value?.searchable ?? false)
|
|
63
101
|
|
|
64
102
|
const query = ref('')
|
|
@@ -222,13 +260,25 @@ onMounted(() => {
|
|
|
222
260
|
|
|
223
261
|
<template>
|
|
224
262
|
<div class="space-y-2 rounded-lg border border-slate-800 bg-slate-900/40 p-2">
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
263
|
+
<!-- Which tracker is being searched, always visible, plus the trackers the user
|
|
264
|
+
could add from here (each opens the connect modal over the caller's form). -->
|
|
265
|
+
<div class="flex items-center gap-1.5">
|
|
266
|
+
<span class="shrink-0 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
267
|
+
{{ t('tasks.picker.sourceLabel') }}
|
|
268
|
+
</span>
|
|
269
|
+
<UDropdownMenu :items="sourceMenu" :content="{ side: 'bottom', align: 'start' }">
|
|
270
|
+
<UButton
|
|
271
|
+
color="neutral"
|
|
272
|
+
variant="soft"
|
|
273
|
+
size="xs"
|
|
274
|
+
:icon="icon"
|
|
275
|
+
trailing-icon="i-lucide-chevron-down"
|
|
276
|
+
class="max-w-full"
|
|
277
|
+
>
|
|
278
|
+
<span class="truncate">{{ descriptor?.label ?? t('tasks.picker.noSource') }}</span>
|
|
279
|
+
</UButton>
|
|
280
|
+
</UDropdownMenu>
|
|
281
|
+
</div>
|
|
232
282
|
|
|
233
283
|
<UInput
|
|
234
284
|
v-model="query"
|
|
@@ -27,7 +27,7 @@ const back = useIntegrationBack(open)
|
|
|
27
27
|
|
|
28
28
|
// The tracker being browsed. Owned here (not the picker) so the epic action and the
|
|
29
29
|
// ref-input placeholder share the same selected source; passed to the picker via
|
|
30
|
-
// `v-model:source
|
|
30
|
+
// `v-model:source`, whose selector always shows (and can add) the tracker in use.
|
|
31
31
|
const source = ref<TaskSourceKind | undefined>(undefined)
|
|
32
32
|
const ref_ = ref('')
|
|
33
33
|
const importing = ref(false)
|
|
@@ -162,13 +162,11 @@ async function doSpawnEpic() {
|
|
|
162
162
|
uses for context issues: search by title, pick an already-imported one,
|
|
163
163
|
or paste a URL/key — choosing one opens the prefilled add-task form. The
|
|
164
164
|
search is scoped to the chosen container's repo (so a GitHub search stays
|
|
165
|
-
in that service's repo and a pasted URL / bare number resolves there)
|
|
166
|
-
the source selector is always shown so it's clear which tracker is in use. -->
|
|
165
|
+
in that service's repo and a pasted URL / bare number resolves there). -->
|
|
167
166
|
<UFormField :label="t('tasks.import.searchIssues')">
|
|
168
167
|
<ContextIssuePicker
|
|
169
168
|
v-model:source="source"
|
|
170
169
|
:scope-block-id="containerId"
|
|
171
|
-
always-show-source
|
|
172
170
|
@pick="createFromPick"
|
|
173
171
|
/>
|
|
174
172
|
</UFormField>
|
|
@@ -1,13 +1,53 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { pipelineAllowedForTaskType, purposeAllowsAgentCategory } from '@cat-factory/contracts'
|
|
3
3
|
import type { Block, Pipeline } from '~/types/domain'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
pipelineAllowedForManualStart,
|
|
6
|
+
pipelineDisplaySteps,
|
|
7
|
+
pipelineGateCount,
|
|
8
|
+
} from '~/utils/pipeline'
|
|
5
9
|
|
|
6
10
|
// A minimal pipeline: only the fields the launch/task-type filters read matter here.
|
|
7
11
|
function pipeline(over: Partial<Pipeline> = {}): Pipeline {
|
|
8
12
|
return { id: 'pl_x', name: 'X', agentKinds: ['coder'], ...over } as Pipeline
|
|
9
13
|
}
|
|
10
14
|
|
|
15
|
+
// The data behind every picker's preview pane: the ordered steps a run will actually execute.
|
|
16
|
+
describe('pipelineDisplaySteps', () => {
|
|
17
|
+
it('keeps the authored order and flags the gated steps', () => {
|
|
18
|
+
const p = pipeline({
|
|
19
|
+
agentKinds: ['task-estimator', 'coder', 'reviewer'],
|
|
20
|
+
gates: [false, true, false],
|
|
21
|
+
})
|
|
22
|
+
expect(pipelineDisplaySteps(p)).toEqual([
|
|
23
|
+
{ kind: 'task-estimator', gated: false },
|
|
24
|
+
{ kind: 'coder', gated: true },
|
|
25
|
+
{ kind: 'reviewer', gated: false },
|
|
26
|
+
])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('drops steps disabled by default — they never run, so listing them would misdescribe it', () => {
|
|
30
|
+
// The short `enabled` array also pins "no entry ⇒ enabled": `tester` has none and stays.
|
|
31
|
+
const p = pipeline({ agentKinds: ['architect', 'coder', 'tester'], enabled: [false, true] })
|
|
32
|
+
expect(pipelineDisplaySteps(p).map((s) => s.kind)).toEqual(['coder', 'tester'])
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('pipelineGateCount', () => {
|
|
37
|
+
it('counts only the gates that a run can actually stop at', () => {
|
|
38
|
+
expect(pipelineGateCount(pipeline({ agentKinds: ['coder'] }))).toBe(0)
|
|
39
|
+
expect(
|
|
40
|
+
pipelineGateCount(pipeline({ agentKinds: ['coder', 'merger'], gates: [true, true] })),
|
|
41
|
+
).toBe(2)
|
|
42
|
+
// A gate declared on a disabled step gates nothing: the step is skipped at run.
|
|
43
|
+
expect(
|
|
44
|
+
pipelineGateCount(
|
|
45
|
+
pipeline({ agentKinds: ['coder', 'merger'], gates: [true, true], enabled: [true, false] }),
|
|
46
|
+
),
|
|
47
|
+
).toBe(1)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
11
51
|
describe('pipelineAllowedForTaskType', () => {
|
|
12
52
|
it('a document task offers ONLY document-purpose pipelines', () => {
|
|
13
53
|
expect(pipelineAllowedForTaskType(pipeline({ purpose: 'document' }), 'document')).toBe(true)
|
package/app/utils/pipeline.ts
CHANGED
|
@@ -29,6 +29,15 @@ export function pipelineDisplaySteps(pipeline: Pipeline): PipelineDisplayStep[]
|
|
|
29
29
|
.map(({ kind, gated }) => ({ kind, gated }))
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* How many times a run of `pipeline` will stop for a human. Counted off the DISPLAYED steps, not
|
|
34
|
+
* `pipeline.gates`, because a gate declared on a step that is disabled by default gates nothing —
|
|
35
|
+
* that step never runs, so promising a stop there would misstate what the pipeline does.
|
|
36
|
+
*/
|
|
37
|
+
export function pipelineGateCount(pipeline: Pipeline): number {
|
|
38
|
+
return pipelineDisplaySteps(pipeline).filter((s) => s.gated).length
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
// Re-exported so a picker can import the task-type gate from the same module as the
|
|
33
42
|
// launch/frame gates it composes with (the classifier itself lives in `@cat-factory/contracts`).
|
|
34
43
|
export { pipelineAllowedForTaskType }
|
package/i18n/locales/de.json
CHANGED
|
@@ -3245,7 +3245,11 @@
|
|
|
3245
3245
|
"attachByReference": "{ref} per Referenz anhängen",
|
|
3246
3246
|
"noMatches": "Keine passenden Issues.",
|
|
3247
3247
|
"emptySearchable": "Nach Titel suchen oder ein importiertes Issue auswählen.",
|
|
3248
|
-
"emptyRefOnly": "Fügen Sie eine Issue-URL oder einen -Schlüssel ein, um es anzuhängen."
|
|
3248
|
+
"emptyRefOnly": "Fügen Sie eine Issue-URL oder einen -Schlüssel ein, um es anzuhängen.",
|
|
3249
|
+
"sourceLabel": "Quelle",
|
|
3250
|
+
"noSource": "Quelle wählen",
|
|
3251
|
+
"connectSource": "{label} verbinden",
|
|
3252
|
+
"enableSource": "{label} aktivieren"
|
|
3249
3253
|
},
|
|
3250
3254
|
"contextIssues": {
|
|
3251
3255
|
"title": "Kontext-Issues",
|
|
@@ -3358,10 +3362,12 @@
|
|
|
3358
3362
|
},
|
|
3359
3363
|
"preview": {
|
|
3360
3364
|
"stepCount": "{count} Schritt | {count} Schritte",
|
|
3365
|
+
"gateCount": "{count} Freigabe | {count} Freigaben",
|
|
3361
3366
|
"gated": "Manuelle Freigabe nach diesem Schritt"
|
|
3362
3367
|
},
|
|
3363
3368
|
"picker": {
|
|
3364
|
-
"noneHint": "Keine Standard-Pipeline. Du wählst beim Ausführen der Aufgabe eine aus."
|
|
3369
|
+
"noneHint": "Keine Standard-Pipeline. Du wählst beim Ausführen der Aufgabe eine aus.",
|
|
3370
|
+
"hoverHint": "Fahre über eine Pipeline, um die Schritte zu sehen, die sie ausführt."
|
|
3365
3371
|
},
|
|
3366
3372
|
"builder": {
|
|
3367
3373
|
"title": "Pipeline-Builder",
|
package/i18n/locales/en.json
CHANGED
|
@@ -3644,7 +3644,11 @@
|
|
|
3644
3644
|
"attachByReference": "Attach {ref} by reference",
|
|
3645
3645
|
"noMatches": "No matching issues.",
|
|
3646
3646
|
"emptySearchable": "Search by title, or pick an imported issue.",
|
|
3647
|
-
"emptyRefOnly": "Paste an issue URL or key to attach it."
|
|
3647
|
+
"emptyRefOnly": "Paste an issue URL or key to attach it.",
|
|
3648
|
+
"sourceLabel": "Source",
|
|
3649
|
+
"noSource": "Choose a source",
|
|
3650
|
+
"connectSource": "Connect {label}",
|
|
3651
|
+
"enableSource": "Enable {label}"
|
|
3648
3652
|
},
|
|
3649
3653
|
"contextIssues": {
|
|
3650
3654
|
"title": "Context issues",
|
|
@@ -3763,10 +3767,12 @@
|
|
|
3763
3767
|
},
|
|
3764
3768
|
"preview": {
|
|
3765
3769
|
"stepCount": "{count} step | {count} steps",
|
|
3770
|
+
"gateCount": "{count} approval gate | {count} approval gates",
|
|
3766
3771
|
"gated": "Human approval gate after this step"
|
|
3767
3772
|
},
|
|
3768
3773
|
"picker": {
|
|
3769
|
-
"noneHint": "No default pipeline. You choose one when you run the task."
|
|
3774
|
+
"noneHint": "No default pipeline. You choose one when you run the task.",
|
|
3775
|
+
"hoverHint": "Hover a pipeline to see the steps it runs."
|
|
3770
3776
|
},
|
|
3771
3777
|
"builder": {
|
|
3772
3778
|
"title": "Pipeline builder",
|
package/i18n/locales/es.json
CHANGED
|
@@ -3539,7 +3539,11 @@
|
|
|
3539
3539
|
"attachByReference": "Adjuntar {ref} por referencia",
|
|
3540
3540
|
"noMatches": "No hay incidencias coincidentes.",
|
|
3541
3541
|
"emptySearchable": "Busca por título o elige una incidencia importada.",
|
|
3542
|
-
"emptyRefOnly": "Pega la URL o clave de una incidencia para adjuntarla."
|
|
3542
|
+
"emptyRefOnly": "Pega la URL o clave de una incidencia para adjuntarla.",
|
|
3543
|
+
"sourceLabel": "Fuente",
|
|
3544
|
+
"noSource": "Elige una fuente",
|
|
3545
|
+
"connectSource": "Conectar {label}",
|
|
3546
|
+
"enableSource": "Habilitar {label}"
|
|
3543
3547
|
},
|
|
3544
3548
|
"contextIssues": {
|
|
3545
3549
|
"title": "Incidencias de contexto",
|
|
@@ -3652,10 +3656,12 @@
|
|
|
3652
3656
|
},
|
|
3653
3657
|
"preview": {
|
|
3654
3658
|
"stepCount": "{count} paso | {count} pasos",
|
|
3659
|
+
"gateCount": "{count} aprobación | {count} aprobaciones",
|
|
3655
3660
|
"gated": "Aprobación humana después de este paso"
|
|
3656
3661
|
},
|
|
3657
3662
|
"picker": {
|
|
3658
|
-
"noneHint": "Sin pipeline predeterminado. Eliges uno al ejecutar la tarea."
|
|
3663
|
+
"noneHint": "Sin pipeline predeterminado. Eliges uno al ejecutar la tarea.",
|
|
3664
|
+
"hoverHint": "Pasa el cursor por una pipeline para ver los pasos que ejecuta."
|
|
3659
3665
|
},
|
|
3660
3666
|
"builder": {
|
|
3661
3667
|
"title": "Constructor de pipelines",
|
package/i18n/locales/fr.json
CHANGED
|
@@ -3539,7 +3539,11 @@
|
|
|
3539
3539
|
"attachByReference": "Joindre {ref} par référence",
|
|
3540
3540
|
"noMatches": "Aucun ticket correspondant.",
|
|
3541
3541
|
"emptySearchable": "Recherchez par titre ou choisissez un ticket importé.",
|
|
3542
|
-
"emptyRefOnly": "Collez l'URL ou la clé d'un ticket pour le joindre."
|
|
3542
|
+
"emptyRefOnly": "Collez l'URL ou la clé d'un ticket pour le joindre.",
|
|
3543
|
+
"sourceLabel": "Source",
|
|
3544
|
+
"noSource": "Choisir une source",
|
|
3545
|
+
"connectSource": "Connecter {label}",
|
|
3546
|
+
"enableSource": "Activer {label}"
|
|
3543
3547
|
},
|
|
3544
3548
|
"contextIssues": {
|
|
3545
3549
|
"title": "Tickets de contexte",
|
|
@@ -3652,10 +3656,12 @@
|
|
|
3652
3656
|
},
|
|
3653
3657
|
"preview": {
|
|
3654
3658
|
"stepCount": "{count} étape | {count} étapes",
|
|
3659
|
+
"gateCount": "{count} validation | {count} validations",
|
|
3655
3660
|
"gated": "Validation humaine après cette étape"
|
|
3656
3661
|
},
|
|
3657
3662
|
"picker": {
|
|
3658
|
-
"noneHint": "Aucun pipeline par défaut. Vous en choisissez un au lancement de la tâche."
|
|
3663
|
+
"noneHint": "Aucun pipeline par défaut. Vous en choisissez un au lancement de la tâche.",
|
|
3664
|
+
"hoverHint": "Survolez un pipeline pour voir les étapes qu'il exécute."
|
|
3659
3665
|
},
|
|
3660
3666
|
"builder": {
|
|
3661
3667
|
"title": "Constructeur de pipelines",
|
package/i18n/locales/he.json
CHANGED
|
@@ -3550,7 +3550,11 @@
|
|
|
3550
3550
|
"attachByReference": "צרף {ref} לפי הפניה",
|
|
3551
3551
|
"noMatches": "אין ניושנים תואמים.",
|
|
3552
3552
|
"emptySearchable": "חפש לפי כותרת, או בחר ניושן מיובא.",
|
|
3553
|
-
"emptyRefOnly": "הדבק URL או מפתח של ניושן כדי לצרף אותו."
|
|
3553
|
+
"emptyRefOnly": "הדבק URL או מפתח של ניושן כדי לצרף אותו.",
|
|
3554
|
+
"sourceLabel": "מקור",
|
|
3555
|
+
"noSource": "בחר מקור",
|
|
3556
|
+
"connectSource": "חבר {label}",
|
|
3557
|
+
"enableSource": "הפעל {label}"
|
|
3554
3558
|
},
|
|
3555
3559
|
"contextIssues": {
|
|
3556
3560
|
"title": "ניושני הקשר",
|
|
@@ -3663,10 +3667,12 @@
|
|
|
3663
3667
|
},
|
|
3664
3668
|
"preview": {
|
|
3665
3669
|
"stepCount": "שלב {count} | {count} שלבים",
|
|
3670
|
+
"gateCount": "אישור {count} | {count} אישורים",
|
|
3666
3671
|
"gated": "אישור אנושי אחרי שלב זה"
|
|
3667
3672
|
},
|
|
3668
3673
|
"picker": {
|
|
3669
|
-
"noneHint": "אין צינור ברירת מחדל. בוחרים אחד כשמריצים את המשימה."
|
|
3674
|
+
"noneHint": "אין צינור ברירת מחדל. בוחרים אחד כשמריצים את המשימה.",
|
|
3675
|
+
"hoverHint": "העבירו את העכבר מעל צינור כדי לראות את השלבים שהוא מריץ."
|
|
3670
3676
|
},
|
|
3671
3677
|
"builder": {
|
|
3672
3678
|
"title": "בונה הצינור",
|
package/i18n/locales/it.json
CHANGED
|
@@ -3245,7 +3245,11 @@
|
|
|
3245
3245
|
"attachByReference": "Allega {ref} per riferimento",
|
|
3246
3246
|
"noMatches": "Nessuna issue corrispondente.",
|
|
3247
3247
|
"emptySearchable": "Cerca per titolo, oppure scegli una issue importata.",
|
|
3248
|
-
"emptyRefOnly": "Incolla l'URL o la chiave di una issue per allegarla."
|
|
3248
|
+
"emptyRefOnly": "Incolla l'URL o la chiave di una issue per allegarla.",
|
|
3249
|
+
"sourceLabel": "Fonte",
|
|
3250
|
+
"noSource": "Scegli una fonte",
|
|
3251
|
+
"connectSource": "Collega {label}",
|
|
3252
|
+
"enableSource": "Abilita {label}"
|
|
3249
3253
|
},
|
|
3250
3254
|
"contextIssues": {
|
|
3251
3255
|
"title": "Issue di contesto",
|
|
@@ -3358,10 +3362,12 @@
|
|
|
3358
3362
|
},
|
|
3359
3363
|
"preview": {
|
|
3360
3364
|
"stepCount": "{count} step | {count} step",
|
|
3365
|
+
"gateCount": "{count} approvazione | {count} approvazioni",
|
|
3361
3366
|
"gated": "Approvazione umana dopo questo step"
|
|
3362
3367
|
},
|
|
3363
3368
|
"picker": {
|
|
3364
|
-
"noneHint": "Nessuna pipeline predefinita. Ne scegli una quando esegui l'attivita."
|
|
3369
|
+
"noneHint": "Nessuna pipeline predefinita. Ne scegli una quando esegui l'attivita.",
|
|
3370
|
+
"hoverHint": "Passa il mouse su una pipeline per vedere gli step che esegue."
|
|
3365
3371
|
},
|
|
3366
3372
|
"builder": {
|
|
3367
3373
|
"title": "Costruttore di pipeline",
|
package/i18n/locales/ja.json
CHANGED
|
@@ -3551,7 +3551,11 @@
|
|
|
3551
3551
|
"attachByReference": "{ref} を参照で添付",
|
|
3552
3552
|
"noMatches": "一致する課題がありません。",
|
|
3553
3553
|
"emptySearchable": "タイトルで検索するか、インポート済みの課題を選択してください。",
|
|
3554
|
-
"emptyRefOnly": "課題の URL またはキーを貼り付けて添付します。"
|
|
3554
|
+
"emptyRefOnly": "課題の URL またはキーを貼り付けて添付します。",
|
|
3555
|
+
"sourceLabel": "ソース",
|
|
3556
|
+
"noSource": "ソースを選択",
|
|
3557
|
+
"connectSource": "{label} を接続",
|
|
3558
|
+
"enableSource": "{label} を有効化"
|
|
3555
3559
|
},
|
|
3556
3560
|
"contextIssues": {
|
|
3557
3561
|
"title": "コンテキスト課題",
|
|
@@ -3664,10 +3668,12 @@
|
|
|
3664
3668
|
},
|
|
3665
3669
|
"preview": {
|
|
3666
3670
|
"stepCount": "{count}ステップ | {count}ステップ",
|
|
3671
|
+
"gateCount": "{count}件の承認 | {count}件の承認",
|
|
3667
3672
|
"gated": "このステップの後に人による承認"
|
|
3668
3673
|
},
|
|
3669
3674
|
"picker": {
|
|
3670
|
-
"noneHint": "デフォルトのパイプラインはありません。タスクの実行時に選択します。"
|
|
3675
|
+
"noneHint": "デフォルトのパイプラインはありません。タスクの実行時に選択します。",
|
|
3676
|
+
"hoverHint": "パイプラインにカーソルを合わせると、実行されるステップが表示されます。"
|
|
3671
3677
|
},
|
|
3672
3678
|
"builder": {
|
|
3673
3679
|
"title": "パイプラインビルダー",
|
package/i18n/locales/pl.json
CHANGED
|
@@ -3539,7 +3539,11 @@
|
|
|
3539
3539
|
"attachByReference": "Dołącz {ref} przez odniesienie",
|
|
3540
3540
|
"noMatches": "Brak pasujących zgłoszeń.",
|
|
3541
3541
|
"emptySearchable": "Szukaj według tytułu lub wybierz zaimportowane zgłoszenie.",
|
|
3542
|
-
"emptyRefOnly": "Wklej adres URL lub klucz zgłoszenia, aby je dołączyć."
|
|
3542
|
+
"emptyRefOnly": "Wklej adres URL lub klucz zgłoszenia, aby je dołączyć.",
|
|
3543
|
+
"sourceLabel": "Źródło",
|
|
3544
|
+
"noSource": "Wybierz źródło",
|
|
3545
|
+
"connectSource": "Połącz {label}",
|
|
3546
|
+
"enableSource": "Włącz {label}"
|
|
3543
3547
|
},
|
|
3544
3548
|
"contextIssues": {
|
|
3545
3549
|
"title": "Zgłoszenia kontekstowe",
|
|
@@ -3652,10 +3656,12 @@
|
|
|
3652
3656
|
},
|
|
3653
3657
|
"preview": {
|
|
3654
3658
|
"stepCount": "{count} krok | {count} kroki | {count} kroków",
|
|
3659
|
+
"gateCount": "{count} zatwierdzenie | {count} zatwierdzenia | {count} zatwierdzeń",
|
|
3655
3660
|
"gated": "Ręczne zatwierdzenie po tym kroku"
|
|
3656
3661
|
},
|
|
3657
3662
|
"picker": {
|
|
3658
|
-
"noneHint": "Brak domyślnego pipeline'u. Wybierzesz go podczas uruchamiania zadania."
|
|
3663
|
+
"noneHint": "Brak domyślnego pipeline'u. Wybierzesz go podczas uruchamiania zadania.",
|
|
3664
|
+
"hoverHint": "Najedź na pipeline, aby zobaczyć kroki, które wykonuje."
|
|
3659
3665
|
},
|
|
3660
3666
|
"builder": {
|
|
3661
3667
|
"title": "Kreator pipeline'ów",
|
package/i18n/locales/tr.json
CHANGED
|
@@ -3551,7 +3551,11 @@
|
|
|
3551
3551
|
"attachByReference": "{ref} öğesini referansla ekle",
|
|
3552
3552
|
"noMatches": "Eşleşen sorun yok.",
|
|
3553
3553
|
"emptySearchable": "Başlığa göre ara veya içe aktarılmış bir sorun seç.",
|
|
3554
|
-
"emptyRefOnly": "Eklemek için bir sorun URL'si veya anahtarı yapıştır."
|
|
3554
|
+
"emptyRefOnly": "Eklemek için bir sorun URL'si veya anahtarı yapıştır.",
|
|
3555
|
+
"sourceLabel": "Kaynak",
|
|
3556
|
+
"noSource": "Bir kaynak seç",
|
|
3557
|
+
"connectSource": "{label} bağla",
|
|
3558
|
+
"enableSource": "{label} etkinleştir"
|
|
3555
3559
|
},
|
|
3556
3560
|
"contextIssues": {
|
|
3557
3561
|
"title": "Bağlam sorunları",
|
|
@@ -3664,10 +3668,12 @@
|
|
|
3664
3668
|
},
|
|
3665
3669
|
"preview": {
|
|
3666
3670
|
"stepCount": "{count} adım | {count} adım",
|
|
3671
|
+
"gateCount": "{count} onay | {count} onay",
|
|
3667
3672
|
"gated": "Bu adımdan sonra insan onayı"
|
|
3668
3673
|
},
|
|
3669
3674
|
"picker": {
|
|
3670
|
-
"noneHint": "Varsayılan pipeline yok. Görevi çalıştırırken birini seçersin."
|
|
3675
|
+
"noneHint": "Varsayılan pipeline yok. Görevi çalıştırırken birini seçersin.",
|
|
3676
|
+
"hoverHint": "Çalıştırdığı adımları görmek için bir pipeline'ın üzerine gelin."
|
|
3671
3677
|
},
|
|
3672
3678
|
"builder": {
|
|
3673
3679
|
"title": "Pipeline oluşturucu",
|
package/i18n/locales/uk.json
CHANGED
|
@@ -3539,7 +3539,11 @@
|
|
|
3539
3539
|
"attachByReference": "Прикріпити {ref} за посиланням",
|
|
3540
3540
|
"noMatches": "Немає відповідних завдань.",
|
|
3541
3541
|
"emptySearchable": "Шукайте за назвою або виберіть імпортоване завдання.",
|
|
3542
|
-
"emptyRefOnly": "Вставте URL або ключ завдання, щоб прикріпити його."
|
|
3542
|
+
"emptyRefOnly": "Вставте URL або ключ завдання, щоб прикріпити його.",
|
|
3543
|
+
"sourceLabel": "Джерело",
|
|
3544
|
+
"noSource": "Виберіть джерело",
|
|
3545
|
+
"connectSource": "Підключити {label}",
|
|
3546
|
+
"enableSource": "Увімкнути {label}"
|
|
3543
3547
|
},
|
|
3544
3548
|
"contextIssues": {
|
|
3545
3549
|
"title": "Контекстні завдання",
|
|
@@ -3652,10 +3656,12 @@
|
|
|
3652
3656
|
},
|
|
3653
3657
|
"preview": {
|
|
3654
3658
|
"stepCount": "{count} крок | {count} кроки | {count} кроків",
|
|
3659
|
+
"gateCount": "{count} підтвердження | {count} підтвердження | {count} підтверджень",
|
|
3655
3660
|
"gated": "Ручне підтвердження після цього кроку"
|
|
3656
3661
|
},
|
|
3657
3662
|
"picker": {
|
|
3658
|
-
"noneHint": "Немає типового пайплайну. Ви обираєте його під час запуску завдання."
|
|
3663
|
+
"noneHint": "Немає типового пайплайну. Ви обираєте його під час запуску завдання.",
|
|
3664
|
+
"hoverHint": "Наведіть курсор на пайплайн, щоб побачити кроки, які він виконує."
|
|
3659
3665
|
},
|
|
3660
3666
|
"builder": {
|
|
3661
3667
|
"title": "Конструктор пайплайнів",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.164.0",
|
|
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",
|