@cat-factory/app 0.139.0 → 0.141.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.
@@ -1,4 +1,5 @@
1
1
  import type { DocumentSourceKind, TaskSourceKind } from '~/types/domain'
2
+ import { apiErrorEnvelope, apiErrorStatus } from './api/errors'
2
3
 
3
4
  // Shared model + orchestration for attaching external context (imported docs and
4
5
  // tracker issues) to a board block. A "pending" item is something the user has
@@ -30,22 +31,96 @@ export interface PendingContext {
30
31
  needsImport: boolean
31
32
  }
32
33
 
34
+ /**
35
+ * A single pending attachment that failed to import or link, captured with its
36
+ * actual cause instead of swallowed. The message is the server's own explanation
37
+ * (e.g. "GitHub denied access to …" / "… was not found on the default branch"),
38
+ * the status is the HTTP code, and the code is the backend error code
39
+ * (`conflict` / `validation` / …) — enough to both display a specific reason and
40
+ * assemble a copy-pasteable diagnostic report.
41
+ */
42
+ export interface LinkFailure {
43
+ item: PendingContext
44
+ /** The server's message (or a network-fault message) explaining why it failed. */
45
+ message: string
46
+ /** HTTP status of the failed request, when the error carried one. */
47
+ status?: number
48
+ /** Backend error code (`conflict` / `validation` / …), when present. */
49
+ code?: string
50
+ /**
51
+ * The backend error envelope's `details` bag, when present — for a GitHub doc read
52
+ * this carries the repo coordinates + the UPSTREAM GitHub status (e.g. `status: 403`),
53
+ * which differs from the HTTP `status` above (the mapped response code, e.g. 409). Kept
54
+ * so the diagnostic report shows the real GitHub status, not just the mapped one.
55
+ */
56
+ details?: Record<string, unknown>
57
+ }
58
+
33
59
  /** Stable key for a pending item, used for dedupe + selection toggles. */
34
60
  export function contextKey(c: Pick<PendingContext, 'kind' | 'source' | 'externalId'>): string {
35
61
  return `${c.kind}:${c.source}:${c.externalId}`
36
62
  }
37
63
 
64
+ /**
65
+ * Render a batch of {@link LinkFailure}s into a single plain-text diagnostic block
66
+ * for the clipboard — the exact context a bug report needs (the item coordinates,
67
+ * the HTTP status + backend code, the backend `details` bag, and the server's message)
68
+ * so the user does not have to retype any of it. Deliberately English/technical (a log
69
+ * dump, not UI prose), mirroring how format/code examples stay out of the i18n catalog.
70
+ */
71
+ export function buildLinkFailureReport(
72
+ failures: LinkFailure[],
73
+ context: { workspaceId?: string | null; blockId?: string; when?: string } = {},
74
+ ): string {
75
+ const lines: string[] = []
76
+ lines.push(`Context link failures: ${failures.length}`)
77
+ if (context.when) lines.push(`when: ${context.when}`)
78
+ if (context.workspaceId) lines.push(`workspace: ${context.workspaceId}`)
79
+ if (context.blockId) lines.push(`block: ${context.blockId}`)
80
+ for (const f of failures) {
81
+ lines.push('')
82
+ lines.push(`- ${f.item.kind}/${f.item.source}: ${f.item.externalId}`)
83
+ lines.push(` title: ${f.item.title}`)
84
+ if (f.status !== undefined) lines.push(` status: ${f.status}`)
85
+ if (f.code) lines.push(` code: ${f.code}`)
86
+ // Dump the backend `details` (repo coordinates + upstream status), each key namespaced
87
+ // so its `status` reads clearly as the GitHub status, distinct from the HTTP `status`.
88
+ for (const [key, value] of Object.entries(f.details ?? {})) {
89
+ lines.push(` details.${key}: ${formatDetailValue(value)}`)
90
+ }
91
+ lines.push(` error: ${f.message}`)
92
+ }
93
+ return lines.join('\n')
94
+ }
95
+
96
+ /** One-line rendering of a `details` value for the diagnostic dump (objects → JSON). */
97
+ function formatDetailValue(value: unknown): string {
98
+ if (value === null || value === undefined) return String(value)
99
+ if (typeof value === 'object') {
100
+ try {
101
+ return JSON.stringify(value)
102
+ } catch {
103
+ return String(value)
104
+ }
105
+ }
106
+ return String(value)
107
+ }
108
+
38
109
  export function useContextLinking() {
39
110
  const documents = useDocumentsStore()
40
111
  const tasks = useTasksStore()
112
+ const workspace = useWorkspaceStore()
113
+ const toast = useToast()
114
+ const { t } = useI18n()
115
+ const { copyAction } = useCopyToClipboard()
41
116
 
42
117
  /**
43
118
  * Import (when needed) then link every pending item to `blockId`. Each failure
44
- * is counted rather than aborting the batch, so one bad attachment doesn't sink
45
- * the rest; returns how many failed.
119
+ * is captured with its actual cause rather than aborting the batch, so one bad
120
+ * attachment doesn't sink the rest; returns the failures (empty ⇒ all linked).
46
121
  */
47
- async function linkPending(blockId: string, items: PendingContext[]): Promise<number> {
48
- let failed = 0
122
+ async function linkPending(blockId: string, items: PendingContext[]): Promise<LinkFailure[]> {
123
+ const failures: LinkFailure[] = []
49
124
  for (const item of items) {
50
125
  try {
51
126
  if (item.kind === 'document') {
@@ -61,12 +136,49 @@ export function useContextLinking() {
61
136
  : item.externalId
62
137
  await tasks.linkToBlock(blockId, source, externalId)
63
138
  }
64
- } catch {
65
- failed++
139
+ } catch (e) {
140
+ // Never swallow the cause: capture the server's own message + status/code/details
141
+ // so the toast can name the specific reason and the copy affordance can carry the
142
+ // full context (incl. the upstream GitHub status the backend puts on `details`).
143
+ const envelope = apiErrorEnvelope(e)
144
+ failures.push({
145
+ item,
146
+ message: e instanceof Error ? e.message : String(e),
147
+ status: apiErrorStatus(e),
148
+ code: envelope?.code,
149
+ details:
150
+ envelope?.details && typeof envelope.details === 'object'
151
+ ? (envelope.details as Record<string, unknown>)
152
+ : undefined,
153
+ })
66
154
  }
67
155
  }
68
- return failed
156
+ return failures
157
+ }
158
+
159
+ /**
160
+ * Surface link failures as a single actionable toast: the specific per-item
161
+ * reasons as the body, and a "Copy details" action that puts the full diagnostic
162
+ * report ({@link buildLinkFailureReport}) on the clipboard. Sticky (`duration: 0`)
163
+ * so the cause stays readable long enough to act on. No-op when nothing failed.
164
+ */
165
+ function presentLinkFailures(failures: LinkFailure[], blockId?: string): void {
166
+ if (failures.length === 0) return
167
+ const description = failures.map((f) => `${f.item.title}: ${f.message}`).join('\n')
168
+ const report = buildLinkFailureReport(failures, {
169
+ workspaceId: workspace.workspaceId,
170
+ blockId,
171
+ when: new Date().toISOString(),
172
+ })
173
+ toast.add({
174
+ title: t('board.addTask.linkFailed', { count: failures.length }, failures.length),
175
+ description,
176
+ icon: 'i-lucide-triangle-alert',
177
+ color: 'warning',
178
+ duration: 0,
179
+ actions: [copyAction(report)],
180
+ })
69
181
  }
70
182
 
71
- return { linkPending }
183
+ return { linkPending, presentLinkFailures }
72
184
  }
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ // `useCopyToClipboard` wraps VueUse's clipboard; mock it so `copyAction` can be exercised
4
+ // without a real clipboard. The global `useToast`/`useI18n` stubs come from test/setup.ts
5
+ // (`t` echoes the key), so the default label resolves to its i18n key.
6
+ const { writeClipboard } = vi.hoisted(() => ({ writeClipboard: vi.fn(async () => {}) }))
7
+ vi.mock('@vueuse/core', () => ({
8
+ useClipboard: () => ({ copy: writeClipboard, isSupported: { value: true } }),
9
+ }))
10
+
11
+ import { useCopyToClipboard } from '~/composables/useCopyToClipboard'
12
+
13
+ describe('copyAction', () => {
14
+ it('builds a Copy-details toast action that copies the given text', async () => {
15
+ const action = useCopyToClipboard().copyAction('diagnostic report')
16
+ expect(action.label).toBe('common.copyDetails')
17
+ expect(action.icon).toBe('i-lucide-clipboard')
18
+
19
+ action.onClick()
20
+ await Promise.resolve()
21
+ expect(writeClipboard).toHaveBeenCalledWith('diagnostic report')
22
+ })
23
+
24
+ it('honours a custom label', () => {
25
+ expect(useCopyToClipboard().copyAction('x', 'Custom label').label).toBe('Custom label')
26
+ })
27
+ })
@@ -25,5 +25,21 @@ export function useCopyToClipboard() {
25
25
  }
26
26
  }
27
27
 
28
- return { copy, isSupported }
28
+ /**
29
+ * A ready-made toast action that copies `text` (through {@link copy}, so it shows the
30
+ * same "Copied" / "Copy failed" feedback). Drop it into a toast's `actions` so any
31
+ * error/warning toast can offer a one-click "Copy details" — the message + context the
32
+ * user would otherwise have to retype into a bug report.
33
+ */
34
+ function copyAction(text: string, label?: string) {
35
+ return {
36
+ label: label ?? t('common.copyDetails'),
37
+ icon: 'i-lucide-clipboard',
38
+ onClick: () => {
39
+ void copy(text)
40
+ },
41
+ }
42
+ }
43
+
44
+ return { copy, copyAction, isSupported }
29
45
  }
@@ -11,6 +11,7 @@ import type {
11
11
  GitHubRepo,
12
12
  MergePullRequestInput,
13
13
  OpenPullRequestInput,
14
+ RepoTreeEntry,
14
15
  ResyncRequest,
15
16
  } from '~/types/domain'
16
17
  import { useSingleFlightProbe } from '~/composables/useSingleFlightProbe'
@@ -215,6 +216,26 @@ export const useGitHubStore = defineStore('github', () => {
215
216
  return api.listGitHubRepoTree(workspace.requireId(), repoGithubId, path)
216
217
  }
217
218
 
219
+ /** Full file listing per repo (recursive tree), cached by GitHub numeric id. */
220
+ const repoFiles = ref<Record<number, RepoTreeEntry[]>>({})
221
+
222
+ /**
223
+ * Load (and cache) EVERY file in a repo — the whole tree in one recursive read — so a
224
+ * picker can search files by path entirely client-side (no per-keystroke server call).
225
+ * Cached by repo id so re-opening the picker for the same repo is instant; force a
226
+ * refetch with `{ reload: true }`. Mirrors the branches cache.
227
+ */
228
+ async function loadRepoFiles(
229
+ repoGithubId: number,
230
+ opts: { reload?: boolean } = {},
231
+ ): Promise<RepoTreeEntry[]> {
232
+ const cached = repoFiles.value[repoGithubId]
233
+ if (cached && !opts.reload) return cached
234
+ const files = await api.listGitHubRepoFiles(workspace.requireId(), repoGithubId)
235
+ repoFiles.value = { ...repoFiles.value, [repoGithubId]: files }
236
+ return files
237
+ }
238
+
218
239
  /** The URL a workspace owner visits to install the App against this workspace. */
219
240
  function getInstallUrl(): Promise<string> {
220
241
  return api.getGitHubInstallUrl(workspace.requireId()).then((r) => r.url)
@@ -313,6 +334,7 @@ export const useGitHubStore = defineStore('github', () => {
313
334
  pulls.value = []
314
335
  issues.value = []
315
336
  branches.value = {}
337
+ repoFiles.value = {}
316
338
  }
317
339
 
318
340
  return {
@@ -347,6 +369,8 @@ export const useGitHubStore = defineStore('github', () => {
347
369
  searchAvailableRepos,
348
370
  setLinkedRepos,
349
371
  loadRepoTree,
372
+ repoFiles,
373
+ loadRepoFiles,
350
374
  loadBranches,
351
375
  getInstallUrl,
352
376
  loadInstallations,
@@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
2
2
  import { computed, ref } from 'vue'
3
3
  import type { AgentKind, Pipeline } from '~/types/domain'
4
4
  import type { ConsensusStepConfig, StepGating } from '~/types/consensus'
5
- import type { StepOptions, TesterQualityConfig } from '@cat-factory/contracts'
5
+ import type { PipelinePurpose, StepOptions, TesterQualityConfig } from '@cat-factory/contracts'
6
6
  import { companionForProducer, uid } from '~/utils/catalog'
7
7
  import { useUpsertList } from '~/composables/useUpsertList'
8
8
  import { useWorkspaceStore } from '~/stores/workspace'
@@ -81,7 +81,16 @@ export const usePipelinesStore = defineStore('pipelines', () => {
81
81
  const draftStepOptions = ref<(StepOptions | null)[]>([])
82
82
  /** Organizational labels for the pipeline being assembled/edited. */
83
83
  const draftLabels = ref<string[]>([])
84
+ /**
85
+ * The use-case classifier of the pipeline being assembled/edited (`build` / `document` /
86
+ * `review` / `research` / `planning`), or null when unclassified. Drives which task pickers
87
+ * offer the saved pipeline and which agent kinds the builder palette shows (a non-`build`
88
+ * purpose hides the Implementation/Testing kinds).
89
+ */
90
+ const draftPurpose = ref<PipelinePurpose | null>(null)
84
91
  const draftName = ref('New pipeline')
92
+ /** Prose description for the pipeline being assembled/edited (shown in the pickers). */
93
+ const draftDescription = ref('')
85
94
  /** The id of the pipeline being edited, or null when assembling a brand-new one. */
86
95
  const editingId = ref<string | null>(null)
87
96
 
@@ -321,7 +330,9 @@ export const usePipelinesStore = defineStore('pipelines', () => {
321
330
  draftTesterQuality.value = []
322
331
  draftStepOptions.value = []
323
332
  draftLabels.value = []
333
+ draftPurpose.value = null
324
334
  draftName.value = 'New pipeline'
335
+ draftDescription.value = ''
325
336
  editingId.value = null
326
337
  }
327
338
 
@@ -339,7 +350,9 @@ export const usePipelinesStore = defineStore('pipelines', () => {
339
350
  )
340
351
  draftStepOptions.value = pipeline.agentKinds.map((_, i) => pipeline.stepOptions?.[i] ?? null)
341
352
  draftLabels.value = [...(pipeline.labels ?? [])]
353
+ draftPurpose.value = pipeline.purpose ?? null
342
354
  draftName.value = pipeline.name
355
+ draftDescription.value = pipeline.description ?? ''
343
356
  editingId.value = pipeline.id
344
357
  }
345
358
 
@@ -347,6 +360,10 @@ export const usePipelinesStore = defineStore('pipelines', () => {
347
360
  function draftPayload() {
348
361
  return {
349
362
  name: draftName.value.trim() || 'Untitled pipeline',
363
+ // ALWAYS send description (like stepOptions) so an update can CLEAR it — an omitted field
364
+ // reads as "keep existing", so toggling the last of the text away must send the empty string.
365
+ // The backend trims + drops a blank one, so this is a no-op on create / an empty description.
366
+ description: draftDescription.value.trim(),
350
367
  agentKinds: [...draft.value],
351
368
  // Only send gates when at least one step is gated.
352
369
  ...(draftGates.value.some(Boolean) ? { gates: [...draftGates.value] } : {}),
@@ -381,6 +398,10 @@ export const usePipelinesStore = defineStore('pipelines', () => {
381
398
  stepOptions: draftStepOptions.value.map((o) => o ?? null),
382
399
  // Only send labels when there are any.
383
400
  ...(draftLabels.value.length ? { labels: [...draftLabels.value] } : {}),
401
+ // Only send purpose when the pipeline is classified (null ⇒ leave unclassified). Like the
402
+ // legacy per-step arrays, an omitted `purpose` on update reads as "keep existing"; clearing
403
+ // a classification back to unclassified is not a supported edit (every built-in ships one).
404
+ ...(draftPurpose.value ? { purpose: draftPurpose.value } : {}),
384
405
  }
385
406
  }
386
407
 
@@ -451,7 +472,9 @@ export const usePipelinesStore = defineStore('pipelines', () => {
451
472
  draftTesterQuality,
452
473
  draftStepOptions,
453
474
  draftLabels,
475
+ draftPurpose,
454
476
  draftName,
477
+ draftDescription,
455
478
  editingId,
456
479
  units,
457
480
  hydrate,
@@ -60,6 +60,7 @@ export type {
60
60
  AgentCategory,
61
61
  CustomAgentKind,
62
62
  Pipeline,
63
+ PipelinePurpose,
63
64
  SpendStatus,
64
65
  BudgetCaps,
65
66
  Workspace,
@@ -0,0 +1,66 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { pipelineAllowedForTaskType, purposeAllowsAgentCategory } from '@cat-factory/contracts'
3
+ import type { Block, Pipeline } from '~/types/domain'
4
+ import { pipelineAllowedForManualStart } from '~/utils/pipeline'
5
+
6
+ // A minimal pipeline: only the fields the launch/task-type filters read matter here.
7
+ function pipeline(over: Partial<Pipeline> = {}): Pipeline {
8
+ return { id: 'pl_x', name: 'X', agentKinds: ['coder'], ...over } as Pipeline
9
+ }
10
+
11
+ describe('pipelineAllowedForTaskType', () => {
12
+ it('a document task offers ONLY document-purpose pipelines', () => {
13
+ expect(pipelineAllowedForTaskType(pipeline({ purpose: 'document' }), 'document')).toBe(true)
14
+ expect(pipelineAllowedForTaskType(pipeline({ purpose: 'build' }), 'document')).toBe(false)
15
+ expect(pipelineAllowedForTaskType(pipeline({ purpose: 'research' }), 'document')).toBe(false)
16
+ // An unclassified pipeline is hidden from a document task (it requires the explicit classifier).
17
+ expect(pipelineAllowedForTaskType(pipeline({ purpose: undefined }), 'document')).toBe(false)
18
+ })
19
+
20
+ it('every non-document task type is unrestricted (any purpose, and undefined type)', () => {
21
+ for (const type of ['feature', 'bug', 'spike', 'review', 'ralph', undefined] as const) {
22
+ expect(pipelineAllowedForTaskType(pipeline({ purpose: 'build' }), type)).toBe(true)
23
+ expect(pipelineAllowedForTaskType(pipeline({ purpose: 'document' }), type)).toBe(true)
24
+ expect(pipelineAllowedForTaskType(pipeline({ purpose: undefined }), type)).toBe(true)
25
+ }
26
+ })
27
+ })
28
+
29
+ describe('purposeAllowsAgentCategory (builder palette gate)', () => {
30
+ it('a build (or unclassified) pipeline may use every category', () => {
31
+ for (const purpose of ['build', null, undefined] as const) {
32
+ for (const cat of ['review', 'design', 'build', 'test', 'docs', 'gates'] as const) {
33
+ expect(purposeAllowsAgentCategory(purpose, cat)).toBe(true)
34
+ }
35
+ }
36
+ })
37
+
38
+ it('a non-build pipeline hides the Implementation (build) and Testing (test) categories', () => {
39
+ for (const purpose of ['document', 'review', 'research', 'planning'] as const) {
40
+ expect(purposeAllowsAgentCategory(purpose, 'build')).toBe(false)
41
+ expect(purposeAllowsAgentCategory(purpose, 'test')).toBe(false)
42
+ // Non-code categories stay visible.
43
+ expect(purposeAllowsAgentCategory(purpose, 'docs')).toBe(true)
44
+ expect(purposeAllowsAgentCategory(purpose, 'review')).toBe(true)
45
+ expect(purposeAllowsAgentCategory(purpose, 'gates')).toBe(true)
46
+ }
47
+ })
48
+ })
49
+
50
+ describe('pipelineAllowedForManualStart composes the task-type gate', () => {
51
+ const noFrame = undefined
52
+ const blocks: Block[] = []
53
+
54
+ it('drops a non-document pipeline for a document task, keeps it for others', () => {
55
+ const build = pipeline({ purpose: 'build' })
56
+ expect(pipelineAllowedForManualStart(build, noFrame, blocks, 'document')).toBe(false)
57
+ expect(pipelineAllowedForManualStart(build, noFrame, blocks, 'feature')).toBe(true)
58
+ // No task type supplied ⇒ no task-type restriction.
59
+ expect(pipelineAllowedForManualStart(build, noFrame, blocks)).toBe(true)
60
+ })
61
+
62
+ it('still excludes recurring-only pipelines regardless of task type', () => {
63
+ const recurring = pipeline({ purpose: 'document', availability: 'recurring' })
64
+ expect(pipelineAllowedForManualStart(recurring, noFrame, blocks, 'document')).toBe(false)
65
+ })
66
+ })
@@ -1,5 +1,37 @@
1
- import { frameAllowsVisualPipeline, pipelineHasVisualStep } from '@cat-factory/contracts'
2
- import type { Block, Pipeline } from '~/types/domain'
1
+ import {
2
+ frameAllowsVisualPipeline,
3
+ pipelineAllowedForTaskType,
4
+ pipelineHasVisualStep,
5
+ } from '@cat-factory/contracts'
6
+ import type { AgentKind, Block, Pipeline } from '~/types/domain'
7
+
8
+ /** One agent step of a pipeline as shown in a preview: its kind + whether it's a human-gated step. */
9
+ export interface PipelineDisplayStep {
10
+ kind: AgentKind
11
+ /** A human approval gate pauses the run after this step (`gates[i]`). */
12
+ gated: boolean
13
+ }
14
+
15
+ /**
16
+ * The steps a pipeline preview should render: the ENABLED steps in order (a step disabled by
17
+ * default — `enabled[i] === false` — is skipped at run, so it would misrepresent the pipeline to
18
+ * list it), each flagged when it carries a human approval gate. Companions are included as their
19
+ * own chips, mirroring how the run timeline lists every step.
20
+ */
21
+ export function pipelineDisplaySteps(pipeline: Pipeline): PipelineDisplayStep[] {
22
+ return pipeline.agentKinds
23
+ .map((kind, i) => ({
24
+ kind,
25
+ enabled: pipeline.enabled?.[i] !== false,
26
+ gated: pipeline.gates?.[i] === true,
27
+ }))
28
+ .filter((s) => s.enabled)
29
+ .map(({ kind, gated }) => ({ kind, gated }))
30
+ }
31
+
32
+ // Re-exported so a picker can import the task-type gate from the same module as the
33
+ // launch/frame gates it composes with (the classifier itself lives in `@cat-factory/contracts`).
34
+ export { pipelineAllowedForTaskType }
3
35
 
4
36
  // Surface counterpart to the backend's slice-4c run-start gate: a pipeline with a visual step
5
37
  // (`tester-ui` / `visual-confirmation`) may run only on a frame with a UI to exercise — a
@@ -29,14 +61,21 @@ export function pipelineAllowedForFrame(
29
61
  /**
30
62
  * Whether `pipeline` may be started as a MANUAL one-off task run (the board/inspector Run menus,
31
63
  * the add-task modal, the task run-settings default). Excludes `'recurring'`-only pipelines the
32
- * backend would refuse.
64
+ * backend would refuse, visual pipelines on a frame with no UI, and — when a `taskType` is given —
65
+ * pipelines whose `purpose` doesn't fit that task type (a `document` task offers only document
66
+ * pipelines). `taskType` omitted ⇒ no task-type restriction (an un-typed context shows all).
33
67
  */
34
68
  export function pipelineAllowedForManualStart(
35
69
  pipeline: Pipeline,
36
70
  frame: Block | undefined,
37
71
  blocks: readonly Block[],
72
+ taskType?: Block['taskType'],
38
73
  ): boolean {
39
- return pipeline.availability !== 'recurring' && pipelineAllowedForFrame(pipeline, frame, blocks)
74
+ return (
75
+ pipeline.availability !== 'recurring' &&
76
+ pipelineAllowedForFrame(pipeline, frame, blocks) &&
77
+ pipelineAllowedForTaskType(pipeline, taskType)
78
+ )
40
79
  }
41
80
 
42
81
  /**
@@ -2936,6 +2936,17 @@
2936
2936
  "emptySearchable": "Nach Titel suchen oder ein importiertes Dokument auswählen.",
2937
2937
  "emptyRefOnly": "Fügen Sie eine Seiten-URL oder -ID ein, um sie anzuhängen."
2938
2938
  },
2939
+ "repoPicker": {
2940
+ "searchRepoPlaceholder": "Repositorys suchen…",
2941
+ "clearRepo": "Repository entfernen",
2942
+ "searchTab": "Dateien suchen",
2943
+ "browseTab": "Durchsuchen",
2944
+ "searchFilesPlaceholder": "Dateien nach Pfad suchen…",
2945
+ "searchFilesHint": "Tippen, um Dateien in diesem Repository zu suchen.",
2946
+ "noFileMatches": "Keine passenden Dateien.",
2947
+ "filesFailed": "Dateien konnten nicht geladen werden: {error}",
2948
+ "moreFiles": "Es werden die ersten {count} Treffer angezeigt – grenzen Sie Ihre Suche ein."
2949
+ },
2939
2950
  "import": {
2940
2951
  "title": "Aus einer Dokumentquelle importieren",
2941
2952
  "connectFirst": "Verbinden Sie zuerst eine Dokumentquelle.",
@@ -3084,6 +3095,13 @@
3084
3095
  "proceed": "Trotzdem fortfahren",
3085
3096
  "stopReset": "Aufgabe stoppen & zurücksetzen"
3086
3097
  },
3098
+ "preview": {
3099
+ "stepCount": "{count} Schritt | {count} Schritte",
3100
+ "gated": "Manuelle Freigabe nach diesem Schritt"
3101
+ },
3102
+ "picker": {
3103
+ "noneHint": "Keine Standard-Pipeline. Du wählst beim Ausführen der Aufgabe eine aus."
3104
+ },
3087
3105
  "builder": {
3088
3106
  "title": "Pipeline-Builder",
3089
3107
  "agentPalette": "Agenten-Palette",
@@ -3092,6 +3110,7 @@
3092
3110
  "configureModels": "Modelle konfigurieren",
3093
3111
  "configureModelsTooltip": "Modell-Presets verwalten (auf welchem Modell jeder Agent läuft)",
3094
3112
  "namePlaceholder": "Pipeline-Name",
3113
+ "descriptionPlaceholder": "Beschreibung (wird bei der Pipeline-Auswahl angezeigt)",
3095
3114
  "labelPlaceholder": "+ Label",
3096
3115
  "gatingNeedsEstimator": "Ein Gated-Schritt benötigt davor einen Task Estimator. Fügen Sie einen hinzu, sonst wird die Pipeline nicht gespeichert.",
3097
3116
  "emptyDraft": "Klicken Sie links auf Agenten, um eine lineare Pipeline zusammenzustellen.",
@@ -3123,6 +3142,15 @@
3123
3142
  "impactThreshold": "Auswirkung ≥",
3124
3143
  "strategy": "Strategie",
3125
3144
  "rounds": "Runden",
3145
+ "purposeLabel": "Zweck",
3146
+ "purposePlaceholder": "Zweck auswählen",
3147
+ "purposeOption": {
3148
+ "build": "Entwicklung",
3149
+ "document": "Dokumentation",
3150
+ "review": "Review",
3151
+ "research": "Recherche",
3152
+ "planning": "Planung"
3153
+ },
3126
3154
  "strategyOption": {
3127
3155
  "specialist-panel": "Spezialisten-Panel",
3128
3156
  "debate": "Debatte",
@@ -3176,7 +3204,8 @@
3176
3204
  "skillPlaceholder": "Skill auswählen",
3177
3205
  "skillNoneAvailable": "Keine Skills verfügbar. Verknüpfe eine Skill-Quelle in den Kontoeinstellungen.",
3178
3206
  "skillMissing": "Dieser Skill ist nicht mehr im Katalog; wähle einen anderen.",
3179
- "skillNeedsPick": "Ein Skill-Schritt braucht einen ausgewählten Skill, bevor du speichern kannst."
3207
+ "skillNeedsPick": "Ein Skill-Schritt braucht einen ausgewählten Skill, bevor du speichern kannst.",
3208
+ "purposeStepsConflict": "Dieser Zweck schreibt keinen Code und führt keine Tests aus, doch die Pipeline enthält noch Implementierungs- oder Testschritte. Entfernen Sie diese oder setzen Sie den Zweck auf Entwicklung."
3180
3209
  },
3181
3210
  "progress": {
3182
3211
  "status": {
@@ -3892,6 +3921,7 @@
3892
3921
  "retry": "Erneut versuchen",
3893
3922
  "copy": "Kopieren",
3894
3923
  "copied": "In die Zwischenablage kopiert",
3924
+ "copyDetails": "Details kopieren",
3895
3925
  "reveal": "Anzeigen",
3896
3926
  "hide": "Verbergen",
3897
3927
  "delete": "Löschen",
@@ -30,6 +30,7 @@
30
30
  "retry": "Retry",
31
31
  "copy": "Copy",
32
32
  "copied": "Copied to clipboard",
33
+ "copyDetails": "Copy details",
33
34
  "reveal": "Reveal",
34
35
  "hide": "Hide",
35
36
  "delete": "Delete",
@@ -3298,6 +3299,17 @@
3298
3299
  "emptySearchable": "Search by title, or pick an imported document.",
3299
3300
  "emptyRefOnly": "Paste a page URL or ID to attach it."
3300
3301
  },
3302
+ "repoPicker": {
3303
+ "searchRepoPlaceholder": "Search repositories…",
3304
+ "clearRepo": "Clear repository",
3305
+ "searchTab": "Search files",
3306
+ "browseTab": "Browse",
3307
+ "searchFilesPlaceholder": "Search files by path…",
3308
+ "searchFilesHint": "Type to search files in this repository.",
3309
+ "noFileMatches": "No matching files.",
3310
+ "filesFailed": "Couldn't load files: {error}",
3311
+ "moreFiles": "Showing the first {count} matches — refine your search to narrow it down."
3312
+ },
3301
3313
  "import": {
3302
3314
  "title": "Import from a document source",
3303
3315
  "connectFirst": "Connect a document source first.",
@@ -3449,6 +3461,13 @@
3449
3461
  "proceed": "Proceed anyway",
3450
3462
  "stopReset": "Stop & reset task"
3451
3463
  },
3464
+ "preview": {
3465
+ "stepCount": "{count} step | {count} steps",
3466
+ "gated": "Human approval gate after this step"
3467
+ },
3468
+ "picker": {
3469
+ "noneHint": "No default pipeline. You choose one when you run the task."
3470
+ },
3452
3471
  "builder": {
3453
3472
  "title": "Pipeline builder",
3454
3473
  "agentPalette": "Agent palette",
@@ -3457,6 +3476,7 @@
3457
3476
  "configureModels": "Configure models",
3458
3477
  "configureModelsTooltip": "Manage model presets (which model each agent runs on)",
3459
3478
  "namePlaceholder": "Pipeline name",
3479
+ "descriptionPlaceholder": "Description (shown when picking this pipeline)",
3460
3480
  "labelPlaceholder": "+ label",
3461
3481
  "gatingNeedsEstimator": "A gated step needs a Task Estimator before it. Add one or the pipeline won't save.",
3462
3482
  "emptyDraft": "Click agents on the left to assemble a linear pipeline.",
@@ -3488,6 +3508,15 @@
3488
3508
  "impactThreshold": "impact ≥",
3489
3509
  "strategy": "Strategy",
3490
3510
  "rounds": "Rounds",
3511
+ "purposeLabel": "Purpose",
3512
+ "purposePlaceholder": "Select a purpose",
3513
+ "purposeOption": {
3514
+ "build": "Build",
3515
+ "document": "Documentation",
3516
+ "review": "Review",
3517
+ "research": "Research",
3518
+ "planning": "Planning"
3519
+ },
3491
3520
  "strategyOption": {
3492
3521
  "specialist-panel": "Specialist panel",
3493
3522
  "debate": "Debate",
@@ -3541,7 +3570,8 @@
3541
3570
  "skillPlaceholder": "Select a skill",
3542
3571
  "skillNoneAvailable": "No skills available. Link a skill source in account settings.",
3543
3572
  "skillMissing": "This skill is no longer in the catalog; pick another.",
3544
- "skillNeedsPick": "A Skill step needs a skill selected before you can save."
3573
+ "skillNeedsPick": "A Skill step needs a skill selected before you can save.",
3574
+ "purposeStepsConflict": "This purpose writes no code and runs no tests, but the pipeline still has implementation or testing steps. Remove them or switch the purpose to Build."
3545
3575
  },
3546
3576
  "progress": {
3547
3577
  "status": {