@cat-factory/app 0.179.0 → 0.180.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.
@@ -10,16 +10,25 @@
10
10
  // (`ui.openInitiativePlanning`) or as the interviewer step's result view. Live `initiative`
11
11
  // stream events patch the store, so an open window follows the interview as it progresses.
12
12
  //
13
+ // An interview runs over MULTIPLE ROUNDS and the entity keeps the settled ones, so the list is a
14
+ // mix of what the human still owes an answer and what they already dealt with. It renders pending
15
+ // first (`orderInterviewQuestions`) — see the `order` snapshot below for why that is recomputed per
16
+ // round rather than live.
17
+ //
13
18
  // CONTINUE/PROCEED ARE ASYNC. They only record the intent on the parked step and wake the durable
14
19
  // driver; the interviewer LLM then runs for as long as it takes, and the response carries the
15
20
  // PRE-resume entity. So the window must not key its body on the entity alone — that renders
16
21
  // identically before and after the click, which reads as the button having done nothing. The
17
22
  // phase below folds the planning RUN's status in, so the wait is visible and a failed pass says
18
23
  // so instead of leaving the human staring at questions they already submitted.
19
- import { computed, reactive, watch } from 'vue'
24
+ import { computed, reactive, ref, watch } from 'vue'
20
25
  import ClarificationItem from '~/components/common/ClarificationItem.vue'
21
26
  import InterviewGateNotice from '~/components/common/InterviewGateNotice.vue'
22
- import { INITIATIVE_STATUS_LABEL_KEYS } from '~/utils/initiative'
27
+ import {
28
+ INITIATIVE_STATUS_LABEL_KEYS,
29
+ isPendingQuestion,
30
+ orderInterviewQuestions,
31
+ } from '~/utils/initiative'
23
32
  import { interviewGatePhase } from '~/utils/interviewGate'
24
33
  import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
25
34
 
@@ -41,9 +50,7 @@ const questions = computed(() =>
41
50
  (initiative.value?.qa ?? []).map((q, i) => ({ ...q, key: q.id ?? `q-${i}` })),
42
51
  )
43
52
  /** Questions still needing an answer: not dismissed, and not yet answered (mirrors backend). */
44
- const pending = computed(() =>
45
- questions.value.filter((q) => q.status !== 'dismissed' && !(q.answer ?? '').trim()),
46
- )
53
+ const pending = computed(() => questions.value.filter(isPendingQuestion))
47
54
 
48
55
  // Per-question answer drafts, seeded from the entity and refreshed as new rounds arrive
49
56
  // without clobbering an answer the human is mid-edit on.
@@ -58,6 +65,28 @@ watch(
58
65
  { immediate: true },
59
66
  )
60
67
 
68
+ /**
69
+ * Render order (pending first — see `orderInterviewQuestions`), re-snapshotted ONLY when the
70
+ * question SET changes, i.e. when a round lands. Deriving it live from the answers instead would
71
+ * yank a question out from under the human the moment they blurred its textarea and shuffle
72
+ * everything below it up, while they are reading down the list. Re-snapshotting per ROUND rather
73
+ * than per question is what keeps a window left open across rounds correct: a question answered in
74
+ * round one has to sink below round two's new ones, which a rank frozen at first sight never would.
75
+ */
76
+ const order = ref<string[]>([])
77
+ watch(
78
+ () => questions.value.map((q) => q.key).join('|'),
79
+ () => {
80
+ order.value = orderInterviewQuestions(questions.value).map((q) => q.key)
81
+ },
82
+ { immediate: true },
83
+ )
84
+ const orderedQuestions = computed(() => {
85
+ const rank = new Map(order.value.map((key, i) => [key, i]))
86
+ // A question the snapshot has not seen is by definition new, so it is pending and sorts first.
87
+ return [...questions.value].sort((a, b) => (rank.get(a.key) ?? -1) - (rank.get(b.key) ?? -1))
88
+ })
89
+
61
90
  const resuming = computed(() => initiatives.resuming)
62
91
 
63
92
  /**
@@ -221,7 +250,7 @@ async function onDiscard() {
221
250
  <!-- Interview questions — the shared clarification surface (answer / not-relevant /
222
251
  recommend), reused with the requirements-review window. -->
223
252
  <ul v-else class="space-y-4">
224
- <li v-for="q in questions" :key="q.key" data-testid="initiative-planning-question">
253
+ <li v-for="q in orderedQuestions" :key="q.key" data-testid="initiative-planning-question">
225
254
  <ClarificationItem
226
255
  v-model:answer="drafts[q.key]"
227
256
  :prompt="q.question"
@@ -10,6 +10,13 @@
10
10
  // unavailable or failed still shows its candidates (flagged as unassessed, since the scan is
11
11
  // useful on its own), and a scan that hit its cap says so — a silently shortened list reads
12
12
  // exactly like an exhaustive one.
13
+ //
14
+ // The tracker selector doubles as the "add a tracker" affordance (the same two-tier menu
15
+ // `<ContextIssuePicker>` renders, off the shared `buildSourceChoices`): a hunt is a common
16
+ // place to find out the tracker holding the bugs isn't connected here yet, and the answer
17
+ // has to be a route to that tracker's own connect screen rather than "go find the
18
+ // Integrations hub". The connect modal opens OVER the hunt, so nothing typed here is lost.
19
+ import type { DropdownMenuItem } from '@nuxt/ui'
13
20
  import type { TaskSourceReadReason } from '@cat-factory/contracts'
14
21
  import type {
15
22
  BugHuntAnalysisStatus,
@@ -17,6 +24,7 @@ import type {
17
24
  BugHuntConfidence,
18
25
  TaskSourceKind,
19
26
  } from '~/types/domain'
27
+ import { type SourceChoice, buildSourceChoices, reconcileSource } from '~/utils/taskSources'
20
28
  import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
21
29
 
22
30
  const { t, d, n } = useI18n()
@@ -54,8 +62,64 @@ const containerItems = computed(() =>
54
62
  })),
55
63
  )
56
64
 
57
- const sourceItems = computed(() =>
58
- tasks.offeredSources.map((s) => ({ label: s.label, value: s.source })),
65
+ const descriptor = computed(() => (source.value ? tasks.descriptorFor(source.value) : undefined))
66
+
67
+ // Two-tier tracker menu: pick one the workspace already offers, or add one it doesn't.
68
+ const sourceChoices = computed(() => buildSourceChoices(tasks.sources, source.value))
69
+ const sourceMenu = computed<DropdownMenuItem[][]>(() =>
70
+ sourceChoices.value.map((group) =>
71
+ group.map((choice) =>
72
+ choice.action === 'select'
73
+ ? {
74
+ label: choice.label,
75
+ icon: choice.icon,
76
+ trailingIcon: choice.active ? 'i-lucide-check' : undefined,
77
+ onSelect: () => {
78
+ source.value = choice.source
79
+ },
80
+ }
81
+ : {
82
+ label: addLabel(choice),
83
+ icon: 'i-lucide-plug',
84
+ onSelect: () => addSource(choice.source),
85
+ },
86
+ ),
87
+ ),
88
+ )
89
+
90
+ /** The trackers that can be added — the empty state's buttons, where none is offered yet. */
91
+ const addableSources = computed(() =>
92
+ sourceChoices.value.flat().filter((c) => c.action !== 'select'),
93
+ )
94
+
95
+ /**
96
+ * Wording for an addable tracker: `enable` is connected but toggled off for this workspace,
97
+ * so the user is never told to "connect" something they already connected.
98
+ */
99
+ function addLabel(choice: SourceChoice): string {
100
+ return choice.action === 'enable'
101
+ ? t('bugHunt.enableSource', { label: choice.label })
102
+ : t('bugHunt.connectSource', { label: choice.label })
103
+ }
104
+
105
+ /**
106
+ * The tracker the user left to add, so it becomes the selection the moment it turns up
107
+ * offered (the connect modal re-probes on success and this hunt stays open underneath it).
108
+ * Also the reconcile trigger for a source that STOPS being offered — disconnected, or
109
+ * toggled off in settings while the hunt sat open.
110
+ */
111
+ const awaitingConnect = ref<TaskSourceKind | null>(null)
112
+ function addSource(s: TaskSourceKind) {
113
+ awaitingConnect.value = s
114
+ ui.openTaskConnect(s)
115
+ }
116
+ watch(
117
+ () => tasks.offeredSources.map((s) => s.source),
118
+ (offered) => {
119
+ const next = reconcileSource(offered, source.value, awaitingConnect.value)
120
+ if (next && next === awaitingConnect.value) awaitingConnect.value = null
121
+ if (next !== source.value) source.value = next
122
+ },
59
123
  )
60
124
 
61
125
  const boardItems = computed(() =>
@@ -100,6 +164,7 @@ watch(open, (isOpen) => {
100
164
  boardId.value = ''
101
165
  issueType.value = ''
102
166
  labels.value = ''
167
+ awaitingConnect.value = null
103
168
  source.value = ui.bugHunt?.source ?? tasks.offeredSources[0]?.source ?? undefined
104
169
  containerId.value = ui.bugHunt?.containerId ?? containerItems.value[0]?.value
105
170
  if (source.value) hunt.loadBoards(source.value)
@@ -193,16 +258,16 @@ const STATUS_KEYS: Record<BugHuntAnalysisStatus, string> = {
193
258
  <div v-if="!tasks.anyOffered" class="space-y-3 text-center">
194
259
  <UIcon name="i-lucide-plug" class="mx-auto h-8 w-8 text-slate-500" />
195
260
  <p class="text-sm text-slate-400">{{ t('bugHunt.connectFirst') }}</p>
196
- <div class="flex justify-center gap-2">
261
+ <div class="flex flex-wrap justify-center gap-2">
197
262
  <UButton
198
- v-for="s in tasks.sources"
199
- :key="s.source"
263
+ v-for="choice in addableSources"
264
+ :key="choice.source"
200
265
  color="primary"
201
266
  variant="soft"
202
- :icon="s.icon"
203
- @click="ui.openTaskConnect(s.source)"
267
+ :icon="choice.icon"
268
+ @click="addSource(choice.source)"
204
269
  >
205
- {{ t('bugHunt.connectSource', { label: s.label }) }}
270
+ {{ addLabel(choice) }}
206
271
  </UButton>
207
272
  </div>
208
273
  </div>
@@ -217,7 +282,24 @@ const STATUS_KEYS: Record<BugHuntAnalysisStatus, string> = {
217
282
 
218
283
  <div class="grid gap-3 sm:grid-cols-2">
219
284
  <UFormField :label="t('bugHunt.tracker')">
220
- <USelect v-model="source" :items="sourceItems" class="w-full" />
285
+ <!-- The selector is also the way to ADD a tracker: each entry in the second group
286
+ opens that tracker's own connect screen over this modal, so the hunt (and
287
+ anything typed into it) is still here when the user comes back. -->
288
+ <UDropdownMenu
289
+ :items="sourceMenu"
290
+ :content="{ side: 'bottom', align: 'start' }"
291
+ class="w-full"
292
+ >
293
+ <UButton
294
+ color="neutral"
295
+ variant="soft"
296
+ :icon="descriptor?.icon"
297
+ trailing-icon="i-lucide-chevron-down"
298
+ class="w-full justify-between"
299
+ >
300
+ <span class="truncate">{{ descriptor?.label ?? t('bugHunt.pickTracker') }}</span>
301
+ </UButton>
302
+ </UDropdownMenu>
221
303
  </UFormField>
222
304
 
223
305
  <UFormField :label="t('bugHunt.board')">
@@ -21,7 +21,7 @@ import type { TaskSourceReadReason } from '@cat-factory/contracts'
21
21
  import type { SourceTask, TaskSearchResult, TaskSourceKind } from '~/types/domain'
22
22
  import { apiErrorReason } from '~/composables/api/errors'
23
23
  import EmptyState from '~/components/common/EmptyState.vue'
24
- import { buildSourceChoices, reconcileSource } from '~/components/tasks/ContextIssuePicker.logic'
24
+ import { buildSourceChoices, reconcileSource } from '~/utils/taskSources'
25
25
 
26
26
  const props = defineProps<{
27
27
  /** contextKeys already staged by the caller, so they're filtered out / not re-offered. */
@@ -1,7 +1,7 @@
1
1
  import { INITIATIVE_ITEM_TERMINAL_STATUSES } from '@cat-factory/contracts'
2
2
  import { describe, it, expect } from 'vitest'
3
- import type { InitiativeItem, InitiativePhase } from '~/types/domain'
4
- import { pendingCheckpointPhase } from './initiative'
3
+ import type { InitiativeItem, InitiativePhase, InitiativeQa } from '~/types/domain'
4
+ import { isPendingQuestion, orderInterviewQuestions, pendingCheckpointPhase } from './initiative'
5
5
 
6
6
  // `pendingCheckpointPhase` mirrors the backend `pendingCheckpoint` (orchestration
7
7
  // `initiative.logic.ts`); these pin the same ordering/edge cases the loop pauses on, so the
@@ -71,3 +71,82 @@ describe('pendingCheckpointPhase', () => {
71
71
  expect(pendingCheckpointPhase(phases, [item('a', 'p1', status)])?.id).toBe('p1')
72
72
  })
73
73
  })
74
+
75
+ // `isPendingQuestion` mirrors the backend rule of the same name (orchestration
76
+ // `initiative.logic.ts`); `orderInterviewQuestions` is what the planning window renders by, so a
77
+ // multi-round interview puts what the human still owes an answer above what they already settled.
78
+
79
+ const qa = (over: Partial<InitiativeQa> & { id: string }): InitiativeQa => ({
80
+ question: over.id,
81
+ answer: '',
82
+ status: 'open',
83
+ ...over,
84
+ })
85
+
86
+ describe('isPendingQuestion', () => {
87
+ it('is pending while unanswered and not dismissed', () => {
88
+ expect(isPendingQuestion(qa({ id: 'a' }))).toBe(true)
89
+ })
90
+
91
+ it('is settled once answered', () => {
92
+ expect(isPendingQuestion(qa({ id: 'a', answer: 'yes' }))).toBe(false)
93
+ })
94
+
95
+ it('treats a whitespace-only answer as unanswered', () => {
96
+ expect(isPendingQuestion(qa({ id: 'a', answer: ' \n' }))).toBe(true)
97
+ })
98
+
99
+ it('is settled once dismissed, answered or not', () => {
100
+ expect(isPendingQuestion(qa({ id: 'a', status: 'dismissed' }))).toBe(false)
101
+ })
102
+
103
+ it('treats an absent answer/status (a hand-authored exchange) as pending', () => {
104
+ expect(isPendingQuestion({})).toBe(true)
105
+ })
106
+ })
107
+
108
+ describe('orderInterviewQuestions', () => {
109
+ const ids = (list: InitiativeQa[]) => orderInterviewQuestions(list).map((q) => q.id)
110
+
111
+ it('floats a later round of unanswered questions above the settled digest', () => {
112
+ // The shape the backend's `[...retainedQa, ...pending]` append produces on round two.
113
+ const list = [
114
+ qa({ id: 'r1-answered', answer: 'yes' }),
115
+ qa({ id: 'r1-dismissed', status: 'dismissed' }),
116
+ qa({ id: 'r2-a' }),
117
+ qa({ id: 'r2-b' }),
118
+ ]
119
+ expect(ids(list)).toEqual(['r2-a', 'r2-b', 'r1-answered', 'r1-dismissed'])
120
+ })
121
+
122
+ it('keeps chronological order within each group', () => {
123
+ const list = [
124
+ qa({ id: 'p1' }),
125
+ qa({ id: 's1', answer: 'yes' }),
126
+ qa({ id: 'p2' }),
127
+ qa({ id: 's2', status: 'dismissed' }),
128
+ qa({ id: 'p3' }),
129
+ ]
130
+ expect(ids(list)).toEqual(['p1', 'p2', 'p3', 's1', 's2'])
131
+ })
132
+
133
+ it('leaves a first round (all pending) exactly as the interviewer asked it', () => {
134
+ const list = [qa({ id: 'a' }), qa({ id: 'b' }), qa({ id: 'c' })]
135
+ expect(ids(list)).toEqual(['a', 'b', 'c'])
136
+ })
137
+
138
+ it('leaves a fully settled interview in its digest order', () => {
139
+ const list = [qa({ id: 'a', answer: 'x' }), qa({ id: 'b', status: 'dismissed' })]
140
+ expect(ids(list)).toEqual(['a', 'b'])
141
+ })
142
+
143
+ it('does not mutate the stored order (the interviewer prompt + tracker digest read it)', () => {
144
+ const list = [qa({ id: 'answered', answer: 'x' }), qa({ id: 'pending' })]
145
+ orderInterviewQuestions(list)
146
+ expect(list.map((q) => q.id)).toEqual(['answered', 'pending'])
147
+ })
148
+
149
+ it('handles an empty interview', () => {
150
+ expect(orderInterviewQuestions([])).toEqual([])
151
+ })
152
+ })
@@ -6,6 +6,7 @@ import type {
6
6
  InitiativePhase,
7
7
  InitiativePresetDescriptor,
8
8
  InitiativePresetInputs,
9
+ InitiativeQa,
9
10
  InitiativeStatus,
10
11
  } from '~/types/domain'
11
12
 
@@ -111,6 +112,37 @@ export function initiativeProgress(
111
112
  }
112
113
  }
113
114
 
115
+ /**
116
+ * Whether a planning-interview question still needs a human answer: not dismissed, and no answer
117
+ * yet. Mirrors the backend `isPendingQuestion` (orchestration `initiative.logic.ts`) — the rule the
118
+ * interviewer, the retained-across-rounds digest and the continue gate all key off — so the window's
119
+ * pending list, its unanswered counter and its render order can never disagree with the engine
120
+ * about what is still open.
121
+ */
122
+ export function isPendingQuestion(q: Partial<Pick<InitiativeQa, 'answer' | 'status'>>): boolean {
123
+ return q.status !== 'dismissed' && (q.answer ?? '').trim().length === 0
124
+ }
125
+
126
+ /**
127
+ * Interview questions in the order the planning window renders them: everything still pending
128
+ * first, everything already settled (answered, or dismissed as not relevant) after, each group
129
+ * keeping the interviewer's own chronological order.
130
+ *
131
+ * Each round APPENDS its new questions after the digest retained from the previous ones (backend
132
+ * `applyInterviewQuestions`: `[...retainedQa, ...pending]`), so from round two onwards the only
133
+ * questions the human still has to act on sit below a growing wall of ones they already settled —
134
+ * on a long interview, below the fold entirely. This reorders the RENDER only; the stored `qa`
135
+ * order, which the interviewer prompt and the in-repo tracker digest read, is untouched.
136
+ */
137
+ export function orderInterviewQuestions<T extends Partial<Pick<InitiativeQa, 'answer' | 'status'>>>(
138
+ qa: readonly T[],
139
+ ): T[] {
140
+ const pending: T[] = []
141
+ const settled: T[] = []
142
+ for (const q of qa) (isPendingQuestion(q) ? pending : settled).push(q)
143
+ return [...pending, ...settled]
144
+ }
145
+
114
146
  /**
115
147
  * The phase whose completed checkpoint (D2) is awaiting a human, or null. Mirrors the backend
116
148
  * `pendingCheckpoint` (orchestration `initiative.logic.ts`) so the SPA recomputes the pending
@@ -1,12 +1,12 @@
1
1
  import { describe, it, expect } from 'vitest'
2
- import { buildSourceChoices, reconcileSource } from './ContextIssuePicker.logic'
2
+ import { buildSourceChoices, reconcileSource } from './taskSources'
3
3
  import type { TaskSourceState } from '~/types/domain'
4
4
 
5
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.
6
+ * The pure tracker-selection behind `<ContextIssuePicker>` and `<BugHuntModal>`. Pins what
7
+ * the always-visible selector promises: the tracker in use is named even when it is the only
8
+ * one, a tracker the workspace hasn't got yet is offered as something to ADD (worded for its
9
+ * actual state), and the selection stays valid as the offered set changes underneath it.
10
10
  */
11
11
  const state = (source: string, { available = true, enabled = true } = {}): TaskSourceState =>
12
12
  ({
@@ -55,6 +55,17 @@ describe('buildSourceChoices', () => {
55
55
  ])
56
56
  })
57
57
 
58
+ // What a surface's "nothing connected yet" state renders its add buttons from: it flattens
59
+ // the groups, so every choice there has to be addable. A `select` leaking through would
60
+ // offer to connect a tracker the workspace already has.
61
+ it('yields only addable choices when the workspace offers no tracker', () => {
62
+ const choices = buildSourceChoices(
63
+ [state('jira', { available: false }), state('linear', { enabled: false })],
64
+ undefined,
65
+ ).flat()
66
+ expect(choices.map((c) => c.action)).toEqual(['connect', 'enable'])
67
+ })
68
+
58
69
  it('drops empty groups so the menu renders no stray separator', () => {
59
70
  expect(buildSourceChoices([state('github')], 'github')).toHaveLength(1)
60
71
  expect(buildSourceChoices([state('jira', { available: false })], undefined)).toHaveLength(1)
@@ -1,18 +1,20 @@
1
1
  import type { TaskSourceKind, TaskSourceState } from '~/types/domain'
2
2
 
3
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.
4
+ * Pure tracker-selection logic shared by every surface that picks a task source: which
5
+ * tracker is selected, and what its menu offers. Kept out of the components so their
6
+ * `computed`s and the unit spec call the same code.
7
7
  *
8
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.
9
+ * one — because a tracker-picking surface is exactly where a user discovers the tracker
10
+ * they want is missing, and sending them off to the Integrations hub loses whatever they
11
+ * had in progress. Today `<ContextIssuePicker>` (attach a context issue) and
12
+ * `<BugHuntModal>` (scan a board for bugs) both render it.
11
13
  */
12
14
 
13
- /** One row of the picker's source menu. */
15
+ /** One row of a tracker menu. */
14
16
  export type SourceChoice =
15
- /** An offered tracker the picker can search right now. */
17
+ /** An offered tracker the surface can use right now. */
16
18
  | { action: 'select'; source: TaskSourceKind; label: string; icon: string; active: boolean }
17
19
  /**
18
20
  * A configured tracker that is not offered yet, so it can be added from here. `connect`
@@ -23,8 +25,8 @@ export type SourceChoice =
23
25
  | { action: 'connect' | 'enable'; source: TaskSourceKind; label: string; icon: string }
24
26
 
25
27
  /**
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
+ * A tracker menu, as non-empty groups (the offered trackers, then the ones the user could
29
+ * add). Empty groups are dropped so the menu never renders a stray separator.
28
30
  */
29
31
  export function buildSourceChoices(
30
32
  sources: TaskSourceState[],
@@ -54,13 +56,13 @@ export function buildSourceChoices(
54
56
  }
55
57
 
56
58
  /**
57
- * The source the picker should hold once the offered set changes — after a connect, a
59
+ * The source a surface should hold once the offered set changes — after a connect, a
58
60
  * disconnect, or the per-workspace toggle flipping elsewhere.
59
61
  *
60
62
  * `awaiting` is the tracker the user just left to connect: the moment it becomes offered
61
63
  * it wins, so they land back on the source they went to add rather than on whatever was
62
64
  * 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
65
+ * stopped being offered falls back to the first one (reading a tracker the workspace no
64
66
  * longer offers only yields errors).
65
67
  */
66
68
  export function reconcileSource(
@@ -3444,6 +3444,8 @@
3444
3444
  "intro": "Durchsuche ein Tracker-Board nach offenen, nicht zugewiesenen Fehlern und bewerte sie nach Auswirkung im Verhältnis zum geschätzten Aufwand. Wähle einen aus, und er wird zu einer Aufgabe, die die Fehlerbehebungs-Pipeline durchläuft.",
3445
3445
  "connectFirst": "Verbinde oder aktiviere zuerst eine Aufgabenquelle.",
3446
3446
  "connectSource": "{label} verbinden",
3447
+ "enableSource": "{label} aktivieren",
3448
+ "pickTracker": "Tracker auswählen",
3447
3449
  "needFrameFirst": "Füge zuerst einen Service-Rahmen zum Board hinzu, damit ein übernommener Fehler irgendwo landen kann.",
3448
3450
  "tracker": "Tracker",
3449
3451
  "board": "Board",
@@ -3867,6 +3867,8 @@
3867
3867
  "intro": "Scan a tracker board for open, unassigned bugs and rank them by impact against how hard each looks to fix. Pick one and it becomes a task running the bug-fix pipeline.",
3868
3868
  "connectFirst": "Connect or enable a task source first.",
3869
3869
  "connectSource": "Connect {label}",
3870
+ "enableSource": "Enable {label}",
3871
+ "pickTracker": "Pick a tracker",
3870
3872
  "needFrameFirst": "Add a service frame to the board first, so a picked-up bug has somewhere to land.",
3871
3873
  "tracker": "Tracker",
3872
3874
  "board": "Board",
@@ -3756,6 +3756,8 @@
3756
3756
  "intro": "Explora un tablero del gestor de incidencias en busca de errores abiertos y sin asignar, y clasifícalos por impacto frente a lo difícil que parece arreglar cada uno. Elige uno y se convertirá en una tarea que ejecuta la canalización de corrección de errores.",
3757
3757
  "connectFirst": "Conecta o activa primero una fuente de tareas.",
3758
3758
  "connectSource": "Conectar {label}",
3759
+ "enableSource": "Habilitar {label}",
3760
+ "pickTracker": "Elige un gestor de incidencias",
3759
3761
  "needFrameFirst": "Añade primero un marco de servicio al tablero para que el error elegido tenga dónde aterrizar.",
3760
3762
  "tracker": "Gestor de incidencias",
3761
3763
  "board": "Tablero",
@@ -3756,6 +3756,8 @@
3756
3756
  "intro": "Parcourez un tableau du gestionnaire de tickets à la recherche de bugs ouverts et non assignés, puis classez-les selon leur impact face à la difficulté apparente du correctif. Choisissez-en un et il devient une tâche qui exécute le pipeline de correction.",
3757
3757
  "connectFirst": "Connectez ou activez d'abord une source de tâches.",
3758
3758
  "connectSource": "Connecter {label}",
3759
+ "enableSource": "Activer {label}",
3760
+ "pickTracker": "Choisir un gestionnaire de tickets",
3759
3761
  "needFrameFirst": "Ajoutez d'abord un cadre de service au tableau, pour que le bug retenu ait un endroit où atterrir.",
3760
3762
  "tracker": "Gestionnaire de tickets",
3761
3763
  "board": "Tableau",
@@ -3767,6 +3767,8 @@
3767
3767
  "intro": "סרוק לוח של מערכת מעקב לאיתור באגים פתוחים שאינם משויכים לאיש, ודרג אותם לפי ההשפעה מול מידת הקושי המשוערת בתיקון. בחר אחד והוא יהפוך למשימה שמריצה את צינור תיקון הבאגים.",
3768
3768
  "connectFirst": "חבר או הפעל תחילה מקור משימות.",
3769
3769
  "connectSource": "חבר את {label}",
3770
+ "enableSource": "הפעל {label}",
3771
+ "pickTracker": "בחר מערכת מעקב",
3770
3772
  "needFrameFirst": "הוסף תחילה מסגרת שירות ללוח, כדי שלבאג הנבחר יהיה לאן להגיע.",
3771
3773
  "tracker": "מערכת מעקב",
3772
3774
  "board": "לוח",
@@ -3444,6 +3444,8 @@
3444
3444
  "intro": "Esplora una bacheca del tracker alla ricerca di bug aperti e non assegnati e classificali per impatto rispetto a quanto sembra difficile risolverli. Scegline uno e diventerà un'attività che esegue la pipeline di correzione.",
3445
3445
  "connectFirst": "Collega o abilita prima una sorgente di attività.",
3446
3446
  "connectSource": "Collega {label}",
3447
+ "enableSource": "Abilita {label}",
3448
+ "pickTracker": "Scegli un tracker",
3447
3449
  "needFrameFirst": "Aggiungi prima un frame di servizio alla bacheca, così il bug scelto ha dove atterrare.",
3448
3450
  "tracker": "Tracker",
3449
3451
  "board": "Bacheca",
@@ -3768,6 +3768,8 @@
3768
3768
  "intro": "トラッカーのボードから未割り当ての未解決バグを探し、影響度と修正の難しさの比で並べ替えます。ひとつ選ぶと、バグ修正パイプラインを実行するタスクになります。",
3769
3769
  "connectFirst": "先にタスクソースを接続または有効化してください。",
3770
3770
  "connectSource": "{label} を接続",
3771
+ "enableSource": "{label} を有効化",
3772
+ "pickTracker": "トラッカーを選択",
3771
3773
  "needFrameFirst": "選んだバグの受け入れ先が必要なので、先にサービスフレームをボードに追加してください。",
3772
3774
  "tracker": "トラッカー",
3773
3775
  "board": "ボード",
@@ -3756,6 +3756,8 @@
3756
3756
  "intro": "Przeszukaj tablicę systemu zgłoszeń w poszukiwaniu otwartych, nieprzypisanych błędów i uszereguj je według wpływu w stosunku do przewidywanej trudności naprawy. Wybierz jeden, a stanie się zadaniem uruchamiającym potok naprawy błędów.",
3757
3757
  "connectFirst": "Najpierw połącz lub włącz źródło zadań.",
3758
3758
  "connectSource": "Połącz {label}",
3759
+ "enableSource": "Włącz {label}",
3760
+ "pickTracker": "Wybierz system zgłoszeń",
3759
3761
  "needFrameFirst": "Najpierw dodaj ramkę usługi do tablicy, aby wybrany błąd miał gdzie wylądować.",
3760
3762
  "tracker": "System zgłoszeń",
3761
3763
  "board": "Tablica",
@@ -3768,6 +3768,8 @@
3768
3768
  "intro": "Bir takip panosunu açık ve kimseye atanmamış hatalar için tarayın ve bunları etkilerine karşı düzeltmenin ne kadar zor göründüğüne göre sıralayın. Birini seçin, hata düzeltme hattını çalıştıran bir göreve dönüşsün.",
3769
3769
  "connectFirst": "Önce bir görev kaynağı bağlayın veya etkinleştirin.",
3770
3770
  "connectSource": "{label} bağla",
3771
+ "enableSource": "{label} etkinleştir",
3772
+ "pickTracker": "Bir takip aracı seçin",
3771
3773
  "needFrameFirst": "Seçilen hatanın ineceği bir yer olması için önce panoya bir servis çerçevesi ekleyin.",
3772
3774
  "tracker": "Takip aracı",
3773
3775
  "board": "Pano",
@@ -3756,6 +3756,8 @@
3756
3756
  "intro": "Перегляньте дошку трекера у пошуках відкритих і не призначених нікому помилок та впорядкуйте їх за впливом щодо того, наскільки складним видається виправлення. Оберіть одну, і вона стане завданням, що виконує конвеєр виправлення помилок.",
3757
3757
  "connectFirst": "Спершу під’єднайте або увімкніть джерело завдань.",
3758
3758
  "connectSource": "Під'єднати {label}",
3759
+ "enableSource": "Увімкнути {label}",
3760
+ "pickTracker": "Оберіть трекер",
3759
3761
  "needFrameFirst": "Спершу додайте рамку сервісу на дошку, щоб обрана помилка мала куди потрапити.",
3760
3762
  "tracker": "Трекер",
3761
3763
  "board": "Дошка",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.179.0",
3
+ "version": "0.180.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",