@cat-factory/app 0.296.2 → 0.296.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +4 -3
  2. package/app/components/board/AddTaskModal.vue +2 -2
  3. package/app/components/board/BoardCanvas.vue +4 -92
  4. package/app/components/board/CreateInitiativeModal.vue +2 -2
  5. package/app/components/foundational/FoundationalServiceCatalogList.vue +3 -12
  6. package/app/components/foundational/FoundationalServiceSources.vue +2 -11
  7. package/app/components/fragments/FragmentLibraryManager.vue +3 -12
  8. package/app/components/layout/AccountAuditLog.logic.ts +2 -2
  9. package/app/components/panels/ResultWindowShell.logic.spec.ts +7 -22
  10. package/app/components/panels/StepToolServers.logic.ts +2 -2
  11. package/app/components/settings/ConsensusGroupsSection.vue +1 -4
  12. package/app/components/settings/MergeRolePolicyEditor.logic.spec.ts +0 -1
  13. package/app/components/skills/SkillLibraryManager.vue +3 -12
  14. package/app/components/tutorial/TutorialOverlay.logic.ts +2 -2
  15. package/app/composables/api/client.spec.ts +0 -5
  16. package/app/composables/useBusyRows.ts +22 -0
  17. package/app/composables/useFrameResize.ts +0 -5
  18. package/app/composables/useSemanticZoom.ts +1 -1
  19. package/app/modular/panels/inspector.ts +1 -1
  20. package/app/modular/result-views.ts +4 -3
  21. package/app/stores/board/mutations.ts +1 -14
  22. package/app/stores/observability/agentContext.ts +1 -8
  23. package/app/stores/observability/toolCalls.ts +1 -8
  24. package/app/stores/observability/withFlag.ts +9 -0
  25. package/app/utils/blockRects.ts +1 -1
  26. package/app/utils/boardWakeGate.ts +1 -1
  27. package/app/utils/catalog.ts +1 -14
  28. package/app/utils/descriptorFields.spec.ts +0 -45
  29. package/app/utils/descriptorFields.ts +1 -13
  30. package/app/utils/dnd.ts +1 -25
  31. package/app/utils/initiative.ts +1 -1
  32. package/app/utils/settlingLoop.ts +1 -1
  33. package/app/utils/taskExpansionRanking.spec.ts +8 -17
  34. package/app/utils/tutorial.ts +1 -1
  35. package/app/utils/uiMode.spec.ts +0 -1
  36. package/app/utils/uiRole.ts +1 -1
  37. package/i18n/locales/de.json +1 -7
  38. package/i18n/locales/en.json +1 -7
  39. package/i18n/locales/es.json +1 -7
  40. package/i18n/locales/fr.json +1 -7
  41. package/i18n/locales/he.json +1 -7
  42. package/i18n/locales/it.json +1 -7
  43. package/i18n/locales/ja.json +1 -7
  44. package/i18n/locales/pl.json +1 -7
  45. package/i18n/locales/tr.json +1 -7
  46. package/i18n/locales/uk.json +1 -7
  47. package/package.json +2 -2
package/README.md CHANGED
@@ -203,9 +203,10 @@ reaches the wire. **Every string a descriptor carries is deployment-authored Eng
203
203
  verbatim**, labels, help, option captions and the `section` grouping captions alike: only the
204
204
  platform's own chrome around them (the path-invalid message) is i18n, so no descriptor string enters
205
205
  a locale catalog. And **the value-bag rules live in `utils/descriptorFields.ts`, not in the SFC**
206
- (`defaultDescriptorValues` for the initial values, `setDescriptorValue` / `setDescriptorCheckbox` /
207
- `toggleDescriptorGroupValue` for one edit): what an edit freezes on an entity is what a unit test
208
- must be able to reach, and a rule inside a component is only reachable by mounting one.
206
+ (`setDescriptorValue` / `setDescriptorCheckbox` / `toggleDescriptorGroupValue` for one edit; the
207
+ initial values come straight from contracts' `descriptorFieldDefaults`, the same helper the server
208
+ folds in at the creation door): what an edit freezes on an entity is what a unit test must be able
209
+ to reach, and a rule inside a component is only reachable by mounting one.
209
210
 
210
211
  Mirroring the server's check leaves one refusal still reachable, deliberately: the deployment can
211
212
  re-register the descriptor while the dialog sits open, so a create can come back `422` with
@@ -39,7 +39,7 @@ import {
39
39
  sanitizeDescriptorFields,
40
40
  validateDescriptorFields,
41
41
  } from '@cat-factory/contracts'
42
- import { defaultDescriptorValues } from '~/utils/descriptorFields'
42
+ import { descriptorFieldDefaults } from '@cat-factory/contracts'
43
43
  import { pipelineAllowedForManualStart } from '~/utils/pipeline'
44
44
  import { buildTaskTypePickerRows } from '~/utils/taskTypePicker'
45
45
 
@@ -493,7 +493,7 @@ watch(taskType, (next) => {
493
493
  const custom = customTaskTypes.value.find((tt) => tt.taskType === next)
494
494
  // A custom type owns a fresh field bag on every switch (its descriptors differ per type), seeded
495
495
  // to whatever defaults the new type declares.
496
- customFieldValues.value = defaultDescriptorValues(custom?.fields ?? [])
496
+ customFieldValues.value = descriptorFieldDefaults(custom?.fields ?? [])
497
497
  // An unresolvable preset leaves the current selection alone rather than blanking it: a type
498
498
  // switch is an edit to a form the user is already filling in, not a reset.
499
499
  const preset = defaultPipelineIdFor(next)
@@ -5,7 +5,7 @@ import BlockNode from './nodes/BlockNode.vue'
5
5
  import EpicNode from './nodes/EpicNode.vue'
6
6
  import TaskDependencyEdges from './TaskDependencyEdges.vue'
7
7
  import DependencyConnectOverlay from './DependencyConnectOverlay.vue'
8
- import { readDndPayload, blockIdFromEvent } from '~/utils/dnd'
8
+ import { blockIdFromEvent } from '~/utils/dnd'
9
9
  import { BOARD_FLOW_ID, BOARD_MIN_ZOOM, BOARD_MAX_ZOOM } from '~/composables/useBoardFlow'
10
10
  import { provideBoardActivity } from '~/composables/useBoardActivity'
11
11
  import { useTaskExpansion } from '~/composables/useTaskExpansion'
@@ -18,21 +18,17 @@ import { boardPanMode } from '~/utils/boardPanMode'
18
18
  import { createBoardNodeProjection } from './BoardCanvas.logic'
19
19
 
20
20
  const board = useBoardStore()
21
- const pipelines = usePipelinesStore()
22
- const execution = useExecutionStore()
23
21
  const ui = useUiStore()
24
22
  const github = useGitHubStore()
25
- const toast = useToast()
26
23
  // The drop resolves its target task at the moment of the drop, so the sandbox reading is a
27
24
  // function of that id rather than a computed over a bound block.
28
- const { forcedFor } = useDryRunPolicy()
29
25
  const access = useWorkspaceAccess()
30
26
  const { t } = useI18n()
31
27
 
32
- const { onNodeDragStop, onViewportChange, screenToFlowCoordinate } = useVueFlow(BOARD_FLOW_ID)
28
+ const { onNodeDragStop, onViewportChange } = useVueFlow(BOARD_FLOW_ID)
33
29
  const { draggingId } = useBlockDrag()
34
30
  const { hoveredFrameId } = useFrameStacking()
35
- const { freeFramePosition, focusFrame } = useFramePlacement()
31
+ const { focusFrame } = useFramePlacement()
36
32
  // The board's "no two top-level nodes overlap" invariant: whenever a frame comes to overlap a
37
33
  // neighbour (dragged onto it, grown into it by a border drag, or grown by its first task), the two
38
34
  // are bounced apart. Every client draws that correction; only the one whose own gesture caused the
@@ -127,94 +123,10 @@ function onNodeDoubleClick({ event, node }: NodeMouseEvent) {
127
123
  function onPaneClick() {
128
124
  ui.select(null)
129
125
  }
130
-
131
- // ---- palette drag & drop onto the canvas ----------------------------------
132
- function onDragOver(event: DragEvent) {
133
- event.preventDefault()
134
- if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy'
135
- }
136
-
137
- async function onDrop(event: DragEvent) {
138
- event.preventDefault()
139
- const payload = readDndPayload(event)
140
- if (!payload) return
141
-
142
- if (payload.kind === 'block') {
143
- // Drop where the cursor is, but nudge off any existing frame it lands on so a new
144
- // frame never overlaps a neighbour; then centre the camera on it.
145
- const dropped = screenToFlowCoordinate({ x: event.clientX, y: event.clientY })
146
- const position = freeFramePosition({ near: dropped })
147
- try {
148
- const block = await board.addBlock(payload.blockType, position)
149
- ui.select(block.id)
150
- await focusFrame(block.id)
151
- } catch {
152
- toast.add({
153
- title: t('board.canvas.addBlockFailedTitle'),
154
- description: t('board.canvas.addBlockFailedBody'),
155
- color: 'error',
156
- })
157
- }
158
- return
159
- }
160
-
161
- if (payload.kind === 'pipeline') {
162
- // Pipelines run against tasks, not frames. The nearest [data-block-id] under
163
- // the cursor is the task card when dropped inside an expanded frame.
164
- const blockId = blockIdFromEvent(event)
165
- const target = blockId ? board.getBlock(blockId) : undefined
166
- const pipeline = pipelines.getPipeline(payload.pipelineId)
167
- // Unknown pipeline id is an internal glitch (nothing the user can act on); a drop
168
- // onto blank canvas / a non-block, though, needs the same "aim at a task" nudge the
169
- // wrong-level path gives — otherwise the drop just vanishes (UX-07).
170
- if (!pipeline) return
171
- if (!target) {
172
- toast.add({
173
- title: t('board.canvas.dropOntoTaskTitle'),
174
- description: t('board.canvas.dropOntoTaskBody'),
175
- })
176
- return
177
- }
178
- if (target.level !== 'task') {
179
- toast.add({
180
- title: t('board.canvas.dropOntoTaskTitle'),
181
- description: t('board.canvas.dropOntoTaskBody'),
182
- })
183
- return
184
- }
185
- if (!board.isRunnable(target.id)) {
186
- toast.add({
187
- title: t('board.canvas.taskBlockedTitle'),
188
- description: t('board.canvas.taskBlockedBody'),
189
- })
190
- return
191
- }
192
- // A dropped pipeline starts live and has nothing to ask for, but a preset that sandboxes the
193
- // dropper's role means this run merges nothing. Said HERE, because the drop is the only
194
- // moment this surface has: the ordinary start stays silent (it is already visible on the
195
- // card it landed on), and only the sandbox, which is not, earns a toast.
196
- if (forcedFor(target.id)) {
197
- toast.add({
198
- title: t('board.dryRunToast.title'),
199
- description: t('board.dryRunToast.body', { name: pipeline.name }),
200
- color: 'warning',
201
- icon: 'i-lucide-shield',
202
- })
203
- }
204
- execution.start(target.id, pipeline)
205
- ui.select(target.id)
206
- }
207
- }
208
126
  </script>
209
127
 
210
128
  <template>
211
- <div
212
- ref="boardEl"
213
- data-testid="board-canvas"
214
- class="relative h-full w-full"
215
- @drop="onDrop"
216
- @dragover="onDragOver"
217
- >
129
+ <div ref="boardEl" data-testid="board-canvas" class="relative h-full w-full">
218
130
  <VueFlow
219
131
  :id="BOARD_FLOW_ID"
220
132
  :nodes="nodes"
@@ -21,7 +21,7 @@ import {
21
21
  validateInitiativePresetInputs,
22
22
  } from '@cat-factory/contracts'
23
23
  import type { InitiativePresetInputs, InitiativePresetInputValue } from '~/types/domain'
24
- import { defaultDescriptorValues } from '~/utils/descriptorFields'
24
+ import { descriptorFieldDefaults } from '@cat-factory/contracts'
25
25
  import { GENERIC_PRESET_ID } from '~/stores/initiative'
26
26
  import DescriptorFields from '~/components/common/DescriptorFields.vue'
27
27
  import ContextAttachmentFields from '~/components/context/ContextAttachmentFields.vue'
@@ -71,7 +71,7 @@ let probeSeq = 0
71
71
  /** Seed the form to the selected preset's descriptor defaults, then fire its detection probe. */
72
72
  function applyPreset(): void {
73
73
  const descriptor = selectedPreset.value
74
- inputs.value = descriptor ? defaultDescriptorValues(descriptor.fields) : {}
74
+ inputs.value = descriptor ? descriptorFieldDefaults(descriptor.fields) : {}
75
75
  void runProbe()
76
76
  }
77
77
 
@@ -14,7 +14,8 @@
14
14
  // A contract body is fetched only when a human expands one, through the SAME lazy read a consumer
15
15
  // dispatch makes — so what is inspected here is what an agent would be given, and merely opening
16
16
  // the catalog transfers no documents.
17
- import { computed, reactive, ref } from 'vue'
17
+ import { computed, ref } from 'vue'
18
+ import { useBusyRows } from '~/composables/useBusyRows'
18
19
  import type { ApiContractFormat, FoundationalServiceTier } from '~/types/domain'
19
20
  import { useFoundationalServicesStore } from '~/stores/foundationalServices'
20
21
  import FoundationalContractSummary from '~/components/foundational/FoundationalContractSummary.vue'
@@ -45,17 +46,7 @@ const tierColor = {
45
46
  workspace: 'primary',
46
47
  } as const satisfies Record<FoundationalServiceTier, string>
47
48
 
48
- const busyRows = reactive(new Set<string>())
49
- const rowBusy = (key: string) => busyRows.has(key)
50
- async function withRow(key: string, fn: () => Promise<void>) {
51
- if (busyRows.has(key)) return
52
- busyRows.add(key)
53
- try {
54
- await fn()
55
- } finally {
56
- busyRows.delete(key)
57
- }
58
- }
49
+ const { rowBusy, withRow } = useBusyRows()
59
50
 
60
51
  /** Service ids whose contract documents the user has expanded. */
61
52
  const expanded = ref<string[]>([])
@@ -14,6 +14,7 @@
14
14
  // Mirrors the skill library's sources UI: with the GitHub App connected the user searches a repo
15
15
  // and browses to a path; otherwise the manual owner/name fields are the fallback.
16
16
  import { computed, reactive, ref, watch } from 'vue'
17
+ import { useBusyRows } from '~/composables/useBusyRows'
17
18
  import type {
18
19
  FolderScanCoverage,
19
20
  FoundationalServiceOwnerKind,
@@ -44,17 +45,7 @@ const { confirm } = useConfirm()
44
45
  const githubReady = computed(() => github.available === true && github.connected)
45
46
 
46
47
  // Per-row in-flight tracking so only the control that triggered an action spins.
47
- const busyRows = reactive(new Set<string>())
48
- const rowBusy = (key: string) => busyRows.has(key)
49
- async function withRow(key: string, fn: () => Promise<void>) {
50
- if (busyRows.has(key)) return
51
- busyRows.add(key)
52
- try {
53
- await fn()
54
- } finally {
55
- busyRows.delete(key)
56
- }
57
- }
48
+ const { rowBusy, withRow } = useBusyRows()
58
49
 
59
50
  // ---- link a repo source ----------------------------------------------------
60
51
  const mode = ref<FoundationalServiceSourceMode>('directory')
@@ -6,7 +6,8 @@
6
6
  // the merged catalog (built-in ∪ account ∪ workspace) an agent is selected from per
7
7
  // run. The account scope has no resolved/merged catalog and fetches document
8
8
  // fragments through `viaWorkspaceId` (document-source credentials are per-workspace).
9
- import { computed, nextTick, reactive, ref, watch } from 'vue'
9
+ import { computed, nextTick, ref, watch } from 'vue'
10
+ import { useBusyRows } from '~/composables/useBusyRows'
10
11
  import type {
11
12
  DocumentSourceKind,
12
13
  FragmentOwnerKind,
@@ -108,17 +109,7 @@ function tabLabel(which: Tab): string {
108
109
  // Per-row / per-form in-flight tracking. The store's single `library.loading` flag
109
110
  // drove every row's button at once (UX-29) and cross-spun the add/link forms; key
110
111
  // each async action so only the control that triggered it shows a spinner.
111
- const busyRows = reactive(new Set<string>())
112
- const rowBusy = (key: string) => busyRows.has(key)
113
- async function withRow(key: string, fn: () => Promise<void>) {
114
- if (busyRows.has(key)) return
115
- busyRows.add(key)
116
- try {
117
- await fn()
118
- } finally {
119
- busyRows.delete(key)
120
- }
121
- }
112
+ const { rowBusy, withRow } = useBusyRows()
122
113
  const creating = ref(false)
123
114
  const linkingDoc = ref(false)
124
115
  const linkingSource = ref(false)
@@ -47,7 +47,7 @@ export type Translate = (key: string, params?: Record<string, string>) => string
47
47
  * Derived from the contract rather than re-listed, so an action whose fields change cannot leave
48
48
  * this behind: the `Record` it reads is exhaustive over the same picklist `ACTION_KEYS` is.
49
49
  */
50
- export function detailParams(
50
+ function detailParams(
51
51
  action: AuditAction,
52
52
  details: AuditEventWire['details'],
53
53
  none: string,
@@ -61,7 +61,7 @@ export function detailParams(
61
61
  }
62
62
 
63
63
  /** Who the action was performed ON: the resolved name, else the raw id. */
64
- export function targetLabel(event: AuditEventWire): string {
64
+ function targetLabel(event: AuditEventWire): string {
65
65
  return event.targetName ?? event.targetId
66
66
  }
67
67
 
@@ -1,11 +1,7 @@
1
1
  import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
2
  import { resolve } from 'node:path'
3
3
  import { describe, expect, it } from 'vitest'
4
- import {
5
- PROSE_MEASURE_CLASS,
6
- RESULT_WINDOW_WIDTH_CLASS,
7
- type ResultWindowWidth,
8
- } from './ResultWindowShell.logic'
4
+ import { PROSE_MEASURE_CLASS, type ResultWindowWidth } from './ResultWindowShell.logic'
9
5
 
10
6
  // The width decision is a per-window LAYOUT judgement that lives on each window's
11
7
  // `<ResultWindowShell width="…">`, which means nothing in the type system can ask a window author
@@ -165,22 +161,11 @@ describe('result-window width buckets', () => {
165
161
  })
166
162
  })
167
163
 
168
- describe('RESULT_WINDOW_WIDTH_CLASS', () => {
169
- it('caps every bucket at its own name and leaves `full` uncapped', () => {
170
- expect(RESULT_WINDOW_WIDTH_CLASS).toEqual({
171
- '3xl': 'max-w-3xl',
172
- '4xl': 'max-w-4xl',
173
- '5xl': 'max-w-5xl',
174
- // Not a bigger number: the panel's `w-full` spans the backdrop and the variant's gutter
175
- // (`m-4` / `p-4`) is the only inset. A cap here would reintroduce the indefensible number
176
- // `full` exists to avoid.
177
- full: 'max-w-none',
178
- })
179
- })
180
-
181
- it('shares one measure with the step reader', () => {
182
- // `AgentStepDetail` reads prose at `mx-auto max-w-3xl`; the windows must not hold a second
183
- // opinion about how wide prose should be.
184
- expect(PROSE_MEASURE_CLASS).toBe('max-w-3xl')
164
+ describe('PROSE_MEASURE_CLASS', () => {
165
+ it('is the measure the step reader already uses', () => {
166
+ // `AgentStepDetail` reads prose at its own measure; the windows must not hold a second
167
+ // opinion about how wide prose should be, so the constant is checked against that source.
168
+ const stepReader = readFileSync(resolve(componentsDir, 'panels/AgentStepDetail.vue'), 'utf8')
169
+ expect(stepReader).toContain(PROSE_MEASURE_CLASS)
185
170
  })
186
171
  })
@@ -81,7 +81,7 @@ export const KNOWN_REASONS = toolServerUnavailableReasonSchema.options
81
81
  * it were a translation key, taking the retired-member path away from exactly the case it exists
82
82
  * for. Narrowing at the boundary keeps the exhaustive `Record` compile-time guard intact.
83
83
  */
84
- export function isKnownReason(reason: string): reason is ToolServerUnavailableReason {
84
+ function isKnownReason(reason: string): reason is ToolServerUnavailableReason {
85
85
  return (KNOWN_REASONS as readonly string[]).includes(reason)
86
86
  }
87
87
 
@@ -205,7 +205,7 @@ export const KNOWN_OBSERVED_STATUSES = toolServerObservedStatusSchema.options
205
205
  * A predicate rather than a truthiness check on the lookup, for the reason {@link isKnownReason}
206
206
  * gives: an `Object.prototype` member name reads back as a truthy non-key.
207
207
  */
208
- export function isKnownObservedStatus(status: string): status is ToolServerObservedStatus {
208
+ function isKnownObservedStatus(status: string): status is ToolServerObservedStatus {
209
209
  return (KNOWN_OBSERVED_STATUSES as readonly string[]).includes(status)
210
210
  }
211
211
 
@@ -12,6 +12,7 @@
12
12
  import { computed, ref } from 'vue'
13
13
  import type { ConsensusGroup, ConsensusStrategy } from '~/types/consensus'
14
14
  import { isSelectable } from '~/stores/models'
15
+ import { uid } from '~/utils/catalog'
15
16
 
16
17
  const { t } = useI18n()
17
18
  const groups = useConsensusGroupsStore()
@@ -52,10 +53,6 @@ interface EditorState {
52
53
  const editor = ref<EditorState | null>(null)
53
54
  const busy = ref(false)
54
55
 
55
- function uid(prefix: string) {
56
- return `${prefix}_${Math.random().toString(36).slice(2, 9)}`
57
- }
58
-
59
56
  /** A fresh group starts as a gated two-model panel — the shape the feature is for. */
60
57
  function startCreate() {
61
58
  editor.value = {
@@ -34,7 +34,6 @@ describe('narrowingOptionsFor', () => {
34
34
  describe('INHERIT_RULE', () => {
35
35
  it('is a non-empty value the select can carry as an item', () => {
36
36
  expect(INHERIT_RULE).not.toBe('')
37
- expect(INHERIT_RULE.length).toBeGreaterThan(0)
38
37
  })
39
38
 
40
39
  // It is also not one of the rules, or clearing a row would be indistinguishable from setting it.
@@ -5,7 +5,8 @@
5
5
  // synced skill catalog a pipeline `skill` step picks from. Mirrors the fragment library's
6
6
  // repo-sources UI; when the GitHub App is connected the user searches a repo + browses to the
7
7
  // skills directory, otherwise the manual owner/name/dir fields are the fallback.
8
- import { computed, reactive, ref, watch } from 'vue'
8
+ import { computed, ref, watch } from 'vue'
9
+ import { useBusyRows } from '~/composables/useBusyRows'
9
10
  import type { GitHubAvailableRepo } from '~/types/domain'
10
11
  import { useSkillLibrary } from '~/stores/skillLibrary'
11
12
  import { SKILL_GROUP_LABEL_KEYS } from '~/utils/skills'
@@ -36,17 +37,7 @@ watch(
36
37
  const githubReady = computed(() => github.available === true && github.connected)
37
38
 
38
39
  // Per-row in-flight tracking so only the control that triggered an action spins.
39
- const busyRows = reactive(new Set<string>())
40
- const rowBusy = (key: string) => busyRows.has(key)
41
- async function withRow(key: string, fn: () => Promise<void>) {
42
- if (busyRows.has(key)) return
43
- busyRows.add(key)
44
- try {
45
- await fn()
46
- } finally {
47
- busyRows.delete(key)
48
- }
49
- }
40
+ const { rowBusy, withRow } = useBusyRows()
50
41
 
51
42
  // ---- link a repo source ----------------------------------------------------
52
43
  const sourceRepoId = ref<number | undefined>(undefined)
@@ -82,7 +82,7 @@ export function focusLeftCard(card: Node | null, relatedTarget: EventTarget | nu
82
82
  * no selector is ever built from a string that could break one, and an id that fails simply
83
83
  * finds no anchor, which the runtime already handles as a skipped step.
84
84
  */
85
- export const TARGET_ID_PATTERN = /^[a-z0-9-]+$/
85
+ const TARGET_ID_PATTERN = /^[a-z0-9-]+$/
86
86
 
87
87
  /** Is this a well-formed anchor id, i.e. safe to put in a selector? */
88
88
  export function isSafeTargetId(id: string): boolean {
@@ -129,7 +129,7 @@ export function resolveSkip(
129
129
  * step's target id, since the same id (`task-card`) is a canvas node on the board and a plain
130
130
  * list row in a panel.
131
131
  */
132
- export const BOARD_NODE_SELECTOR = '.vue-flow__node'
132
+ const BOARD_NODE_SELECTOR = '.vue-flow__node'
133
133
 
134
134
  /** The part of an element the reveal path needs: ancestry, and that ancestor's node id. */
135
135
  interface RevealNode {
@@ -27,11 +27,6 @@ describe('withoutUndefinedQueryParams', () => {
27
27
  expect(out.queryParams).toEqual({ blockId: 'blk_1', page: 0, all: false, q: '' })
28
28
  })
29
29
 
30
- it('drops only the undefined keys from a mixed set', () => {
31
- const out = strip({ queryParams: { window: '7d', workspaceId: undefined } })
32
- expect(out.queryParams).toEqual({ window: '7d' })
33
- })
34
-
35
30
  it('leaves other request params untouched', () => {
36
31
  const params = {
37
32
  pathPrefix: '/workspaces/ws_1',
@@ -0,0 +1,22 @@
1
+ import { reactive } from 'vue'
2
+
3
+ /**
4
+ * A per-row in-flight guard for lists whose rows each carry their own action buttons: a row's
5
+ * action runs at most once at a time, and the button it belongs to reads `rowBusy(key)` for its
6
+ * spinner. Keys are caller-chosen (`sync:<id>`, `unlink:<id>`), so one row can hold several
7
+ * independent actions.
8
+ */
9
+ export function useBusyRows() {
10
+ const busyRows = reactive(new Set<string>())
11
+ const rowBusy = (key: string) => busyRows.has(key)
12
+ async function withRow(key: string, fn: () => Promise<void>) {
13
+ if (busyRows.has(key)) return
14
+ busyRows.add(key)
15
+ try {
16
+ await fn()
17
+ } finally {
18
+ busyRows.delete(key)
19
+ }
20
+ }
21
+ return { rowBusy, withRow }
22
+ }
@@ -35,11 +35,6 @@ const resizingId = ref<string | null>(null)
35
35
  /** The grips in render order, so a component can `v-for` them instead of listing eight blocks. */
36
36
  export const RESIZE_EDGES = Object.keys(HANDLES) as ResizeEdge[]
37
37
 
38
- /** The `cursor` a given grip shows, and holds on `<body>` while its drag runs. */
39
- export function resizeCursor(edge: ResizeEdge): string {
40
- return HANDLES[edge].cursor
41
- }
42
-
43
38
  /**
44
39
  * Pointer-driven resizing for containers (service frames and modules) by dragging any border or
45
40
  * corner, Miro-style. The drag delta is divided by the board zoom so the border tracks the
@@ -1,7 +1,7 @@
1
1
  import type { LodLevel } from '~/types/domain'
2
2
 
3
3
  /** The LOD scale, shallow → deep. Index order lets callers ask "is at least". */
4
- export const LOD_ORDER: LodLevel[] = ['far', 'mid', 'close', 'steps', 'subtasks']
4
+ const LOD_ORDER: LodLevel[] = ['far', 'mid', 'close', 'steps', 'subtasks']
5
5
 
6
6
  /** Map a raw zoom factor to a level-of-detail bucket. Shared by the main board
7
7
  * and the drill-down focus view so both honour the same thresholds.
@@ -91,7 +91,7 @@ const COMPONENTS: Record<InspectorPanelId, Component> = {
91
91
  }
92
92
 
93
93
  /** The built-in `PanelEntry`s: each spec's gating/order + its wrapped component. */
94
- export const INSPECTOR_PANEL_ENTRIES: PanelEntry<Block>[] = INSPECTOR_PANEL_SPECS.map((spec) => ({
94
+ const INSPECTOR_PANEL_ENTRIES: PanelEntry<Block>[] = INSPECTOR_PANEL_SPECS.map((spec) => ({
95
95
  id: spec.id,
96
96
  order: spec.order,
97
97
  when: spec.when,
@@ -144,9 +144,10 @@ const BUILT_IN_RESULT_VIEWS: Record<ResultViewId, Component> = {
144
144
  * The built-in windows as slot entries, derived from `RESULT_VIEW_IDS` so the slot order matches
145
145
  * the canonical id order. Exhaustiveness is guaranteed by {@link BUILT_IN_RESULT_VIEWS}'s type.
146
146
  */
147
- export const RESULT_VIEW_CONTRIBUTIONS: readonly ResultViewContribution[] = RESULT_VIEW_IDS.map(
148
- (id) => ({ id, component: BUILT_IN_RESULT_VIEWS[id] }),
149
- )
147
+ const RESULT_VIEW_CONTRIBUTIONS: readonly ResultViewContribution[] = RESULT_VIEW_IDS.map((id) => ({
148
+ id,
149
+ component: BUILT_IN_RESULT_VIEWS[id],
150
+ }))
150
151
 
151
152
  /**
152
153
  * The first-party result-views module: contributes every built-in window to the
@@ -1,10 +1,4 @@
1
- import type {
2
- BlockType,
3
- CreateTaskType,
4
- FrameRepoType,
5
- TaskTypeFields,
6
- Block,
7
- } from '~/types/domain'
1
+ import type { CreateTaskType, FrameRepoType, TaskTypeFields, Block } from '~/types/domain'
8
2
  import { useWorkspaceStore } from '~/stores/workspace'
9
3
  import type { BoardWriteContext } from './context'
10
4
 
@@ -19,12 +13,6 @@ import type { BoardWriteContext } from './context'
19
13
  export function createBoardMutations(ctx: BoardWriteContext) {
20
14
  const { getBlock, upsert, api, present } = ctx
21
15
 
22
- async function addBlock(type: BlockType, position: { x: number; y: number }): Promise<Block> {
23
- const block = await api.addFrame(useWorkspaceStore().requireId(), { type, position })
24
- upsert(block)
25
- return block
26
- }
27
-
28
16
  /**
29
17
  * Import an existing GitHub repo (the App is installed + it's projected) as a
30
18
  * service frame, with no bootstrap run. The backend links the repo to the new
@@ -164,7 +152,6 @@ export function createBoardMutations(ctx: BoardWriteContext) {
164
152
  }
165
153
 
166
154
  return {
167
- addBlock,
168
155
  addServiceFromRepo,
169
156
  addTask,
170
157
  addModule,
@@ -1,6 +1,7 @@
1
1
  import { ref } from 'vue'
2
2
  import type { AgentContextSnapshot, AgentSearchQuery } from '~/types/execution'
3
3
  import { useSingleFlight } from '~/composables/useSingleFlight'
4
+ import { withFlag } from './withFlag'
4
5
 
5
6
  /** What the two reads need from the store: the workspace binding, nothing else. */
6
7
  export interface AgentContextSinkDeps {
@@ -10,14 +11,6 @@ export interface AgentContextSinkDeps {
10
11
  fetchSearchQueries: (executionId: string) => Promise<{ searchQueries: AgentSearchQuery[] }>
11
12
  }
12
13
 
13
- /** Add or remove a key from a reactive `Set` ref, replacing it so the reactivity fires. */
14
- function withFlag(set: ReturnType<typeof ref<Set<string>>>, key: string, on: boolean) {
15
- const next = new Set(set.value)
16
- if (on) next.add(key)
17
- else next.delete(key)
18
- set.value = next
19
- }
20
-
21
14
  /**
22
15
  * The observability store's AGENT-CONTEXT and SEARCH-QUERY sinks, extracted as one cohesive pair:
23
16
  * both are per-dispatch records the drill-down panel loads on open, neither is pushed live, and
@@ -1,6 +1,7 @@
1
1
  import { ref } from 'vue'
2
2
  import type { RunToolCallFailures, RunToolCallTrajectory } from '~/types/execution'
3
3
  import { useSingleFlight } from '~/composables/useSingleFlight'
4
+ import { withFlag } from './withFlag'
4
5
 
5
6
  // The observability store's TOOL-CALL sink, extracted whole because it is one concern with two
6
7
  // reads and its own coherence rule between them.
@@ -40,14 +41,6 @@ export const EMPTY_TRAJECTORY: RunToolCallTrajectory = Object.freeze({
40
41
  truncated: false,
41
42
  })
42
43
 
43
- /** Add or remove a key from a reactive `Set` ref, replacing it so the reactivity fires. */
44
- function withFlag(set: ReturnType<typeof ref<Set<string>>>, key: string, on: boolean) {
45
- const next = new Set(set.value)
46
- if (on) next.add(key)
47
- else next.delete(key)
48
- set.value = next
49
- }
50
-
51
44
  export function createToolCallSinkState(deps: ToolCallSinkDeps) {
52
45
  /**
53
46
  * One in-flight read per (sink, run). Both loads below fire on the panel OPENING, and the panel
@@ -0,0 +1,9 @@
1
+ import type { Ref } from 'vue'
2
+
3
+ /** Add or remove a key from a reactive `Set` ref, replacing it so the reactivity fires. */
4
+ export function withFlag(set: Ref<Set<string>>, key: string, on: boolean) {
5
+ const next = new Set(set.value)
6
+ if (on) next.add(key)
7
+ else next.delete(key)
8
+ set.value = next
9
+ }
@@ -30,7 +30,7 @@ export type BlockMeasurements = {
30
30
  rectFor: (el: Element) => DOMRect
31
31
  }
32
32
 
33
- export const BLOCK_ID_ATTRIBUTE = 'data-block-id'
33
+ const BLOCK_ID_ATTRIBUTE = 'data-block-id'
34
34
 
35
35
  export function measureBlocks(root: ParentNode = document): BlockMeasurements {
36
36
  let elements: Map<string, HTMLElement> | null = null
@@ -34,7 +34,7 @@ export type WakeGate = {
34
34
  * instead of every frame, while a change that really did move a card is on screen well inside
35
35
  * the window a reader would notice.
36
36
  */
37
- export const RENDER_WAKE_INTERVAL_MS = 250
37
+ const RENDER_WAKE_INTERVAL_MS = 250
38
38
 
39
39
  export function createWakeGate(options: {
40
40
  /** Raise the pulse. */
@@ -15,7 +15,7 @@ import {
15
15
  } from '@cat-factory/contracts'
16
16
 
17
17
  /** Simple unique id helper (fine for a client-only prototype). */
18
- export function uid(prefix = 'id'): string {
18
+ export function uid(prefix: string): string {
19
19
  return `${prefix}_${Math.random().toString(36).slice(2, 9)}`
20
20
  }
21
21
 
@@ -1203,16 +1203,3 @@ export const FORK_DECISION_META = {
1203
1203
  icon: 'i-lucide-git-fork',
1204
1204
  color: '#a78bfa',
1205
1205
  }
1206
-
1207
- /**
1208
- * Whether a Coder step has the Follow-up companion enabled, given the pipeline's per-step
1209
- * `followUps` toggle at index `i`. Enabled by default on a `coder` step (only `false`
1210
- * disables it); ignored on other kinds.
1211
- */
1212
- export function followUpCompanionEnabled(
1213
- kind: string,
1214
- followUps: (boolean | null)[] | undefined,
1215
- i: number,
1216
- ): boolean {
1217
- return kind === 'coder' && followUps?.[i] !== false
1218
- }
@@ -1,7 +1,6 @@
1
1
  import { describe, it, expect } from 'vitest'
2
2
  import type { DescriptorField } from '~/types/domain'
3
3
  import {
4
- defaultDescriptorValues,
5
4
  descriptorFormRows,
6
5
  descriptorGroupValue,
7
6
  setDescriptorCheckbox,
@@ -14,50 +13,6 @@ const field = (over: Partial<DescriptorField> & Pick<DescriptorField, 'key'>): D
14
13
  ...over,
15
14
  })
16
15
 
17
- describe('defaultDescriptorValues', () => {
18
- it('seeds each declared default in its own contract shape', () => {
19
- // The form model is the wire shape, so a default has to arrive typed: the shared validator
20
- // refuses a `'3'` where a `number` field is declared, and would refuse it at create time too.
21
- expect(
22
- defaultDescriptorValues([
23
- field({ key: 'style', type: 'select', default: 'collection' }),
24
- field({ key: 'depth', type: 'number', default: '3' }),
25
- field({ key: 'gate', type: 'checkbox', default: 'true' }),
26
- field({ key: 'ops', type: 'checkbox-group', defaultValues: ['create', 'list'] }),
27
- field({ key: 'dir', type: 'path', default: 'docs' }),
28
- ]),
29
- ).toEqual({
30
- style: 'collection',
31
- depth: 3,
32
- gate: true,
33
- ops: ['create', 'list'],
34
- dir: 'docs',
35
- })
36
- })
37
-
38
- it('leaves a field with no meaningful default ABSENT rather than blank', () => {
39
- // Absent is what validation reads as unset, so seeding `''`/`[]`/`false` would both freeze an
40
- // empty answer and (for a required field) look filled to nothing that checks it.
41
- expect(
42
- defaultDescriptorValues([
43
- field({ key: 'entity', type: 'text' }),
44
- field({ key: 'notes', type: 'textarea', default: '' }),
45
- field({ key: 'gate', type: 'checkbox' }),
46
- field({ key: 'gateOff', type: 'checkbox', default: 'false' }),
47
- field({ key: 'ops', type: 'checkbox-group', defaultValues: [] }),
48
- field({ key: 'depth', type: 'number', default: 'not-a-number' }),
49
- ]),
50
- ).toEqual({})
51
- })
52
-
53
- it('copies a multi-select default, so editing the form cannot mutate the descriptor', () => {
54
- const ops = field({ key: 'ops', type: 'checkbox-group', defaultValues: ['create'] })
55
- const seeded = defaultDescriptorValues([ops])
56
- ;(seeded.ops as string[]).push('delete')
57
- expect(ops.defaultValues).toEqual(['create'])
58
- })
59
- })
60
-
61
16
  describe('setDescriptorValue', () => {
62
17
  it('drops a value the shared rules read as unset rather than freezing it', () => {
63
18
  // Absent is what `validateDescriptorFields` treats as unfilled and what
@@ -1,4 +1,4 @@
1
- import { descriptorFieldDefaults, descriptorFieldSections } from '@cat-factory/contracts'
1
+ import { descriptorFieldSections } from '@cat-factory/contracts'
2
2
  import type { DescriptorField, DescriptorFieldValue, DescriptorFieldValues } from '~/types/domain'
3
3
 
4
4
  // Form-side helpers over the shared descriptor-field vocabulary (`contracts/src/form-fields.ts`),
@@ -11,18 +11,6 @@ import type { DescriptorField, DescriptorFieldValue, DescriptorFieldValues } fro
11
11
  // inside the SFC, so the mutation rules a wrong answer would freeze on an entity are unit-testable
12
12
  // without mounting a component.
13
13
 
14
- /**
15
- * The initial values a field list implies, for seeding a freshly opened form. A repo-detection
16
- * probe's prefill and the user's own edits layer on top.
17
- *
18
- * The SHARED helper, not a form-side copy: the server folds the same defaults in at the creation
19
- * door (`withDescriptorFieldDefaults`), so a duplicate here would be the drift that made a headless
20
- * caller and this form disagree about what a descriptor's default means.
21
- */
22
- export function defaultDescriptorValues(fields: readonly DescriptorField[]): DescriptorFieldValues {
23
- return descriptorFieldDefaults(fields)
24
- }
25
-
26
14
  /**
27
15
  * A value that must stay ABSENT from the bag rather than freeze on the entity: an unchecked
28
16
  * (`false`) checkbox, a blank string, an empty multi-select, or a number that is not one (a
package/app/utils/dnd.ts CHANGED
@@ -1,29 +1,5 @@
1
- import type { BlockType } from '~/types/domain'
2
-
3
- /** MIME-ish key used to carry palette payloads across the HTML5 DnD boundary. */
4
- export const DND_MIME = 'application/agent-board'
5
-
6
- export type DndPayload =
7
- | { kind: 'block'; blockType: BlockType }
8
- | { kind: 'pipeline'; pipelineId: string }
9
-
10
- export function setDndPayload(event: DragEvent, payload: DndPayload) {
11
- event.dataTransfer?.setData(DND_MIME, JSON.stringify(payload))
12
- if (event.dataTransfer) event.dataTransfer.effectAllowed = 'copy'
13
- }
14
-
15
- export function readDndPayload(event: DragEvent): DndPayload | null {
16
- const raw = event.dataTransfer?.getData(DND_MIME)
17
- if (!raw) return null
18
- try {
19
- return JSON.parse(raw) as DndPayload
20
- } catch {
21
- return null
22
- }
23
- }
24
-
25
1
  /** Walk up from an event's target to find the block it landed on, if any. Works for any
26
- * DOM event (drop, double-click, …) only `event.target` is read. */
2
+ * DOM event (double-click, context menu, …): only `event.target` is read. */
27
3
  export function blockIdFromEvent(event: Event): string | null {
28
4
  const el = (event.target as HTMLElement | null)?.closest('[data-block-id]')
29
5
  return el?.getAttribute('data-block-id') ?? null
@@ -79,7 +79,7 @@ export const INITIATIVE_ATTENTION_ICONS: Record<InitiativeAttentionKind, string>
79
79
  * interview park is already owned by the planning window, behind the differently-worded "Answer
80
80
  * planning questions".
81
81
  */
82
- export const INTERVIEW_GATE_RESULT_VIEW = 'initiative-planning'
82
+ const INTERVIEW_GATE_RESULT_VIEW = 'initiative-planning'
83
83
 
84
84
  /**
85
85
  * The block's parked approval that is a PLAN REVIEW — the planner's human gate (`pl_initiative`
@@ -41,7 +41,7 @@ export type SettlingLoop = {
41
41
  * unchanged frame would miss every animation. Four frames (~66ms at 60Hz) clears that gap
42
42
  * while keeping a false wake-up cheap.
43
43
  */
44
- export const DEFAULT_SETTLE_FRAMES = 4
44
+ const DEFAULT_SETTLE_FRAMES = 4
45
45
 
46
46
  export function createSettlingLoop(options: {
47
47
  /** Runs one frame; returns whether it changed anything the user can see. */
@@ -1,14 +1,6 @@
1
1
  import { describe, it, expect } from 'vitest'
2
2
  import { headerDistanceSq, type Rect } from './taskExpansionRanking'
3
3
 
4
- /** Rank a set of cards against a screen centre, best (would-expand) first. */
5
- function rank(cards: Record<string, Rect>, cx: number, cy: number): string[] {
6
- return Object.entries(cards)
7
- .map(([id, rect]) => ({ id, dist: headerDistanceSq(rect, cx, cy) }))
8
- .sort((a, b) => a.dist - b.dist)
9
- .map((c) => c.id)
10
- }
11
-
12
4
  describe('headerDistanceSq', () => {
13
5
  it('measures from the centre of the card top edge', () => {
14
6
  const r: Rect = { left: 0, right: 100, top: 200, bottom: 600 }
@@ -25,27 +17,26 @@ describe('headerDistanceSq', () => {
25
17
  })
26
18
  })
27
19
 
28
- describe('ranking', () => {
29
- // The regression from the screenshot: a tall card parked at the top of the screen
30
- // expands its pipeline down past the centre, so its body covers the centre. A compact
31
- // card whose header sits right at the centre must still win the one you're looking at.
20
+ describe('ordering by header distance', () => {
21
+ // `useTaskExpansion` sorts candidates by this measure, so the card with the smaller value is
22
+ // the one that expands. The regression from the screenshot: a tall card parked at the top of
23
+ // the screen expands its pipeline down past the centre, so its body covers the centre. A
24
+ // compact card whose header sits right at the centre must still win.
32
25
  it('prefers the card whose header is at the centre over a tall card bleeding down from the top', () => {
33
26
  const top: Rect = { left: 0, right: 200, top: 30, bottom: 700 } // header far up, body covers centre
34
27
  const here: Rect = { left: 0, right: 200, top: 320, bottom: 520 } // header at the centre
35
- expect(rank({ top, here }, 100, 340)).toEqual(['here', 'top'])
36
- // document order can't flip the winner
37
- expect(rank({ here, top }, 100, 340)).toEqual(['here', 'top'])
28
+ expect(headerDistanceSq(here, 100, 340)).toBeLessThan(headerDistanceSq(top, 100, 340))
38
29
  })
39
30
 
40
31
  it('ranks by the header nearest the centre regardless of expansion state', () => {
41
32
  const above: Rect = { left: 0, right: 200, top: 100, bottom: 800 }
42
33
  const below: Rect = { left: 0, right: 200, top: 360, bottom: 420 }
43
- expect(rank({ above, below }, 100, 320)).toEqual(['below', 'above'])
34
+ expect(headerDistanceSq(below, 100, 320)).toBeLessThan(headerDistanceSq(above, 100, 320))
44
35
  })
45
36
 
46
37
  it('uses horizontal offset to break a vertical tie', () => {
47
38
  const near: Rect = { left: 0, right: 100, top: 100, bottom: 200 }
48
39
  const far: Rect = { left: 400, right: 500, top: 100, bottom: 200 }
49
- expect(rank({ far, near }, 80, 100)).toEqual(['near', 'far'])
40
+ expect(headerDistanceSq(near, 80, 100)).toBeLessThan(headerDistanceSq(far, 80, 100))
50
41
  })
51
42
  })
@@ -188,7 +188,7 @@ export const TARGET_IDLE_INTERVAL_MS = 400
188
188
  * two means "mostly visible" for a small control and "filling a good part of the screen" for a
189
189
  * large one, which is the same judgement in both cases.
190
190
  */
191
- export const MIN_VISIBLE_RATIO = 0.5
191
+ const MIN_VISIBLE_RATIO = 0.5
192
192
 
193
193
  /** Deterministic tour-list order: `order`, then `id`. */
194
194
  export function sortTours(tours: readonly TutorialTour[]): TutorialTour[] {
@@ -27,7 +27,6 @@ describe('resolveUiMode', () => {
27
27
  it('falls back to the stored choice, then to the default', () => {
28
28
  expect(resolveUiMode(null, 'advanced')).toBe('advanced')
29
29
  expect(resolveUiMode(null, null)).toBe(DEFAULT_UI_MODE)
30
- expect(DEFAULT_UI_MODE).toBe('basic')
31
30
  })
32
31
 
33
32
  it('caps an intake role at basic, above BOTH the env pin and the stored choice', () => {
@@ -28,7 +28,7 @@ export type UiRole = (typeof UI_ROLES)[number]
28
28
  * The first-run prompt is offered once per session until it is answered (see `stores/uiRole.ts`),
29
29
  * and closing it leaves the whole product in place rather than guessing a narrower persona.
30
30
  */
31
- export const DEFAULT_UI_ROLE: UiRole = 'engineer'
31
+ const DEFAULT_UI_ROLE: UiRole = 'engineer'
32
32
 
33
33
  /**
34
34
  * How much of the SPA a role is offered.
@@ -2984,13 +2984,7 @@
2984
2984
  },
2985
2985
  "canvas": {
2986
2986
  "emptyTitle": "Ihr Board ist leer",
2987
- "emptyBody": "Fügen Sie einen Service hinzu, um zu beginnen: bootstrappen Sie ein frisches Repo oder binden Sie ein bereits vorhandenes ein.",
2988
- "addBlockFailedTitle": "Block konnte nicht hinzugefügt werden",
2989
- "addBlockFailedBody": "Das Backend hat die Anfrage abgelehnt.",
2990
- "dropOntoTaskTitle": "Auf eine Aufgabe ziehen",
2991
- "dropOntoTaskBody": "Pipelines laufen gegen Aufgaben, nicht gegen Services.",
2992
- "taskBlockedTitle": "Aufgabe ist blockiert",
2993
- "taskBlockedBody": "Ihre Abhängigkeiten wurden noch nicht gemergt."
2987
+ "emptyBody": "Fügen Sie einen Service hinzu, um zu beginnen: bootstrappen Sie ein frisches Repo oder binden Sie ein bereits vorhandenes ein."
2994
2988
  },
2995
2989
  "addTask": {
2996
2990
  "title": "Eine Aufgabe hinzufügen",
@@ -235,13 +235,7 @@
235
235
  },
236
236
  "canvas": {
237
237
  "emptyTitle": "Your board is empty",
238
- "emptyBody": "Add a service to get started: bootstrap a fresh repo or pull in one you already have.",
239
- "addBlockFailedTitle": "Could not add block",
240
- "addBlockFailedBody": "The backend rejected the request.",
241
- "dropOntoTaskTitle": "Drop onto a task",
242
- "dropOntoTaskBody": "Pipelines run against tasks, not services.",
243
- "taskBlockedTitle": "Task is blocked",
244
- "taskBlockedBody": "Its dependencies haven't merged yet."
238
+ "emptyBody": "Add a service to get started: bootstrap a fresh repo or pull in one you already have."
245
239
  },
246
240
  "addTask": {
247
241
  "title": "Add a task",
@@ -202,13 +202,7 @@
202
202
  },
203
203
  "canvas": {
204
204
  "emptyTitle": "Tu tablero está vacío",
205
- "emptyBody": "Añade un servicio para empezar: arranca un repo nuevo o trae uno que ya tengas.",
206
- "addBlockFailedTitle": "No se pudo añadir el bloque",
207
- "addBlockFailedBody": "El backend rechazó la solicitud.",
208
- "dropOntoTaskTitle": "Suéltalo sobre una tarea",
209
- "dropOntoTaskBody": "Las pipelines se ejecutan sobre tareas, no sobre servicios.",
210
- "taskBlockedTitle": "La tarea está bloqueada",
211
- "taskBlockedBody": "Sus dependencias aún no se han fusionado."
205
+ "emptyBody": "Añade un servicio para empezar: arranca un repo nuevo o trae uno que ya tengas."
212
206
  },
213
207
  "addTask": {
214
208
  "title": "Añadir una tarea",
@@ -202,13 +202,7 @@
202
202
  },
203
203
  "canvas": {
204
204
  "emptyTitle": "Votre tableau est vide",
205
- "emptyBody": "Ajoutez un service pour commencer : initialisez un nouveau dépôt ou importez-en un existant.",
206
- "addBlockFailedTitle": "Impossible d’ajouter le bloc",
207
- "addBlockFailedBody": "Le backend a rejeté la requête.",
208
- "dropOntoTaskTitle": "Déposez sur une tâche",
209
- "dropOntoTaskBody": "Les pipelines s’exécutent sur des tâches, pas sur des services.",
210
- "taskBlockedTitle": "La tâche est bloquée",
211
- "taskBlockedBody": "Ses dépendances n’ont pas encore été fusionnées."
205
+ "emptyBody": "Ajoutez un service pour commencer : initialisez un nouveau dépôt ou importez-en un existant."
212
206
  },
213
207
  "addTask": {
214
208
  "title": "Ajouter une tâche",
@@ -202,13 +202,7 @@
202
202
  },
203
203
  "canvas": {
204
204
  "emptyTitle": "הלוח שלך ריק",
205
- "emptyBody": "הוסף שירות כדי להתחיל: אתחל מאגר חדש או משוך מאגר קיים שכבר יש לך.",
206
- "addBlockFailedTitle": "לא ניתן היה להוסיף בלוק",
207
- "addBlockFailedBody": "השרת דחה את הבקשה.",
208
- "dropOntoTaskTitle": "גרור על משימה",
209
- "dropOntoTaskBody": "צינורות פועלים על משימות, לא על שירותים.",
210
- "taskBlockedTitle": "המשימה חסומה",
211
- "taskBlockedBody": "התלויות שלה עדיין לא מוזגו."
205
+ "emptyBody": "הוסף שירות כדי להתחיל: אתחל מאגר חדש או משוך מאגר קיים שכבר יש לך."
212
206
  },
213
207
  "addTask": {
214
208
  "title": "הוסף משימה",
@@ -2984,13 +2984,7 @@
2984
2984
  },
2985
2985
  "canvas": {
2986
2986
  "emptyTitle": "La tua board è vuota",
2987
- "emptyBody": "Aggiungi un servizio per iniziare: esegui il bootstrap di un repo nuovo o importane uno che hai già.",
2988
- "addBlockFailedTitle": "Impossibile aggiungere il blocco",
2989
- "addBlockFailedBody": "Il backend ha rifiutato la richiesta.",
2990
- "dropOntoTaskTitle": "Rilascia su un'attività",
2991
- "dropOntoTaskBody": "Le pipeline vengono eseguite sulle attività, non sui servizi.",
2992
- "taskBlockedTitle": "L'attività è bloccata",
2993
- "taskBlockedBody": "Le sue dipendenze non sono ancora state unite."
2987
+ "emptyBody": "Aggiungi un servizio per iniziare: esegui il bootstrap di un repo nuovo o importane uno che hai già."
2994
2988
  },
2995
2989
  "addTask": {
2996
2990
  "title": "Aggiungi un'attività",
@@ -202,13 +202,7 @@
202
202
  },
203
203
  "canvas": {
204
204
  "emptyTitle": "ボードが空です",
205
- "emptyBody": "サービスを追加して始めましょう。新しいリポジトリをブートストラップするか、既存のリポジトリを取り込みます。",
206
- "addBlockFailedTitle": "ブロックを追加できませんでした",
207
- "addBlockFailedBody": "バックエンドがリクエストを拒否しました。",
208
- "dropOntoTaskTitle": "タスクにドロップ",
209
- "dropOntoTaskBody": "パイプラインはサービスではなくタスクに対して実行されます。",
210
- "taskBlockedTitle": "タスクがブロックされています",
211
- "taskBlockedBody": "依存関係がまだマージされていません。"
205
+ "emptyBody": "サービスを追加して始めましょう。新しいリポジトリをブートストラップするか、既存のリポジトリを取り込みます。"
212
206
  },
213
207
  "addTask": {
214
208
  "title": "タスクを追加",
@@ -202,13 +202,7 @@
202
202
  },
203
203
  "canvas": {
204
204
  "emptyTitle": "Twoja tablica jest pusta",
205
- "emptyBody": "Dodaj usługę, aby zacząć: zainicjuj nowe repozytorium albo dołącz istniejące.",
206
- "addBlockFailedTitle": "Nie udało się dodać bloku",
207
- "addBlockFailedBody": "Backend odrzucił żądanie.",
208
- "dropOntoTaskTitle": "Upuść na zadanie",
209
- "dropOntoTaskBody": "Pipeline’y działają na zadaniach, a nie na usługach.",
210
- "taskBlockedTitle": "Zadanie jest zablokowane",
211
- "taskBlockedBody": "Jego zależności nie zostały jeszcze scalone."
205
+ "emptyBody": "Dodaj usługę, aby zacząć: zainicjuj nowe repozytorium albo dołącz istniejące."
212
206
  },
213
207
  "addTask": {
214
208
  "title": "Dodaj zadanie",
@@ -202,13 +202,7 @@
202
202
  },
203
203
  "canvas": {
204
204
  "emptyTitle": "Panonuz boş",
205
- "emptyBody": "Başlamak için bir servis ekleyin: yeni bir depo başlatın veya zaten sahip olduğunuz bir depoyu ekleyin.",
206
- "addBlockFailedTitle": "Blok eklenemedi",
207
- "addBlockFailedBody": "Arka uç isteği reddetti.",
208
- "dropOntoTaskTitle": "Bir göreve bırakın",
209
- "dropOntoTaskBody": "İşlem hatları servislere değil, görevlere karşı çalışır.",
210
- "taskBlockedTitle": "Görev engellendi",
211
- "taskBlockedBody": "Bağımlılıkları henüz birleştirilmedi."
205
+ "emptyBody": "Başlamak için bir servis ekleyin: yeni bir depo başlatın veya zaten sahip olduğunuz bir depoyu ekleyin."
212
206
  },
213
207
  "addTask": {
214
208
  "title": "Görev ekle",
@@ -202,13 +202,7 @@
202
202
  },
203
203
  "canvas": {
204
204
  "emptyTitle": "Ваша дошка порожня",
205
- "emptyBody": "Додайте сервіс, щоб почати: ініціалізуйте нове репозиторій або підключіть наявне.",
206
- "addBlockFailedTitle": "Не вдалося додати блок",
207
- "addBlockFailedBody": "Бекенд відхилив запит.",
208
- "dropOntoTaskTitle": "Перетягніть на завдання",
209
- "dropOntoTaskBody": "Конвеєри виконуються над завданнями, а не над сервісами.",
210
- "taskBlockedTitle": "Завдання заблоковано",
211
- "taskBlockedBody": "Його залежності ще не злито."
205
+ "emptyBody": "Додайте сервіс, щоб почати: ініціалізуйте нове репозиторій або підключіть наявне."
212
206
  },
213
207
  "addTask": {
214
208
  "title": "Додати завдання",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.296.2",
3
+ "version": "0.296.3",
4
4
  "description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,7 +18,7 @@
18
18
  "access": "public"
19
19
  },
20
20
  "dependencies": {
21
- "@cat-factory/contracts": "0.346.1",
21
+ "@cat-factory/contracts": "0.346.2",
22
22
  "@modular-frontend/core": "0.6.0",
23
23
  "@modular-vue/core": "^1.5.0",
24
24
  "@modular-vue/journeys": "^1.4.0",