@cat-factory/app 0.74.1 → 0.74.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.
@@ -18,20 +18,46 @@ const props = defineProps<{
18
18
  * in-repo and a pasted URL / bare issue number resolves to the exact issue.
19
19
  */
20
20
  scopeBlockId?: string
21
+ /**
22
+ * Controlled source: when provided the parent owns the selected tracker (via
23
+ * `v-model:source`); omitted, the picker manages it internally (the add-task case).
24
+ */
25
+ source?: TaskSourceKind
26
+ /**
27
+ * Always render the source selector, even with a single offered tracker — so the
28
+ * user can see *which* tracker is being searched (the "create task from issue"
29
+ * surface, where the source is otherwise invisible). Off by default: the inline
30
+ * add-task picker stays compact and only shows a selector when there's a choice.
31
+ */
32
+ alwaysShowSource?: boolean
33
+ }>()
34
+ const emit = defineEmits<{
35
+ pick: [item: PendingContext]
36
+ 'update:source': [value: TaskSourceKind]
21
37
  }>()
22
- const emit = defineEmits<{ pick: [item: PendingContext] }>()
23
38
 
24
39
  const { t } = useI18n()
25
40
  const tasks = useTasksStore()
26
41
 
27
42
  const chosen = computed(() => new Set(props.chosenKeys ?? []))
28
43
 
29
- // Source: default to the first offered tracker; a selector appears only when more
30
- // than one is offered (the common case is a single source).
31
- const source = ref<TaskSourceKind | undefined>(tasks.offeredSources[0]?.source)
44
+ // Source: default to the first offered tracker. Controlled when the parent passes
45
+ // `source` (write-through to `update:source`), else internal. A selector appears when
46
+ // more than one source is offered, or whenever the parent asks (`alwaysShowSource`).
47
+ const internalSource = ref<TaskSourceKind | undefined>(tasks.offeredSources[0]?.source)
48
+ const source = computed<TaskSourceKind | undefined>({
49
+ get: () => props.source ?? internalSource.value,
50
+ set: (v) => {
51
+ internalSource.value = v
52
+ if (v) emit('update:source', v)
53
+ },
54
+ })
32
55
  const sourceItems = computed(() =>
33
56
  tasks.offeredSources.map((s) => ({ label: s.label, value: s.source })),
34
57
  )
58
+ const showSourceSelect = computed(
59
+ () => sourceItems.value.length > 1 || (props.alwaysShowSource && sourceItems.value.length > 0),
60
+ )
35
61
  const descriptor = computed(() => (source.value ? tasks.descriptorFor(source.value) : undefined))
36
62
  const searchable = computed(() => descriptor.value?.searchable ?? false)
37
63
 
@@ -40,10 +66,32 @@ const results = ref<TaskSearchResult[]>([])
40
66
  const searching = ref(false)
41
67
  const searchError = ref<string | null>(null)
42
68
 
69
+ // Already-imported issues, scoped to the target container's repo on the backend
70
+ // (GitHub narrows to the service's linked repo, exactly as search does; repo-less
71
+ // sources are unaffected). Held locally rather than read from the shared workspace
72
+ // list, so a task created for one service never offers issues from sibling repos.
73
+ const imported = ref<SourceTask[]>([])
74
+ async function reloadImported() {
75
+ try {
76
+ imported.value = await tasks.listTasksForBlock(props.scopeBlockId)
77
+ } catch {
78
+ imported.value = []
79
+ }
80
+ }
81
+ // Re-scope when the target container changes (its repo, hence the in-repo issues, differ).
82
+ watch(
83
+ () => props.scopeBlockId,
84
+ () => {
85
+ reloadImported()
86
+ },
87
+ )
88
+
43
89
  // Debounced search: free text hits the tracker; a query that's clearly a URL/key
44
90
  // is left to the explicit "by reference" row below (search won't surface it).
91
+ // Re-scope when `scopeBlockId` changes too (a GitHub search is scoped to the block's
92
+ // repo, so switching the target container re-runs against the new repo).
45
93
  let timer: ReturnType<typeof setTimeout> | undefined
46
- watch([query, source], () => {
94
+ watch([query, source, () => props.scopeBlockId], () => {
47
95
  if (timer) clearTimeout(timer)
48
96
  results.value = []
49
97
  searchError.value = null
@@ -80,7 +128,7 @@ function keyFor(externalId: string): string {
80
128
  const importedRows = computed(() => {
81
129
  if (!source.value) return []
82
130
  const q = query.value.trim().toLowerCase()
83
- return tasks.tasks
131
+ return imported.value
84
132
  .filter((t) => t.source === source.value)
85
133
  .filter((t) => !chosen.value.has(keyFor(t.externalId)))
86
134
  .filter(
@@ -92,7 +140,7 @@ const importedRows = computed(() => {
92
140
  const searchRows = computed(() => {
93
141
  if (!source.value) return []
94
142
  const importedIds = new Set(
95
- tasks.tasks.filter((t) => t.source === source.value).map((t) => t.externalId),
143
+ imported.value.filter((t) => t.source === source.value).map((t) => t.externalId),
96
144
  )
97
145
  return results.value
98
146
  .filter((r) => !importedIds.has(r.externalId))
@@ -108,8 +156,10 @@ const refRow = computed(() => {
108
156
  searchRows.value.some((r) => r.externalId === q) ||
109
157
  chosen.value.has(keyFor(q))
110
158
  if (known) return null
111
- // Only worth offering when it looks like a reference, not a search phrase.
112
- const looksLikeRef = q.includes('#') || q.includes('/') || /^https?:\/\//i.test(q)
159
+ // Only worth offering when it looks like a reference, not a search phrase: a URL,
160
+ // an owner/repo#n or #n GitHub ref, or a Jira/Linear-style key (PROJ-123, ENG-42).
161
+ const looksLikeRef =
162
+ q.includes('#') || q.includes('/') || /^https?:\/\//i.test(q) || /^[a-z][a-z0-9]*-\d+$/i.test(q)
113
163
  return looksLikeRef ? q : null
114
164
  })
115
165
 
@@ -165,15 +215,15 @@ function pickRef(q: string) {
165
215
  }
166
216
 
167
217
  onMounted(() => {
168
- // Keep the quick-pick list current (cheap; the store dedupes).
169
- tasks.loadTasks().catch(() => {})
218
+ // Load the quick-pick list, scoped to the target container's repo.
219
+ reloadImported()
170
220
  })
171
221
  </script>
172
222
 
173
223
  <template>
174
224
  <div class="space-y-2 rounded-lg border border-slate-800 bg-slate-900/40 p-2">
175
225
  <USelect
176
- v-if="sourceItems.length > 1"
226
+ v-if="showSourceSelect"
177
227
  v-model="source"
178
228
  :items="sourceItems"
179
229
  size="xs"
@@ -1,13 +1,14 @@
1
1
  <script setup lang="ts">
2
- // Import an issue from a connected task source (by key or URL). An imported issue
3
- // can be attached to an existing task for context from the inspector (see
4
- // TaskContextIssues.vue), or turned into a new board task here: pick a container
5
- // (service frame or module), then click a search hit to open the prefilled
6
- // add-task form (title seeded, issue staged as linked context) where the user
7
- // confirms the pipeline / presets before creating it. A separate icon button on
8
- // each row opens the issue on GitHub.
9
- import type { TaskSearchResult, TaskSourceKind } from '~/types/domain'
10
- import type { AddTaskPrefill } from '~/stores/ui'
2
+ // Create a board task from a connected tracker's issue. Pick a container (service
3
+ // frame or module), then use the inline picker below to find an issue (search by
4
+ // title, pick an already-imported one, or paste a URL/key) choosing one opens the
5
+ // prefilled add-task form (title seeded, issue staged as linked context) where the
6
+ // user confirms the pipeline / presets before it's created. This is the same picker
7
+ // the add-task form uses for "context issues", so the two behave identically. A
8
+ // pasted parent/epic reference can instead be spawned as a whole linked task group.
9
+ import type { TaskSourceKind } from '~/types/domain'
10
+ import type { PendingContext } from '~/composables/useContextLinking'
11
+ import ContextIssuePicker from '~/components/tasks/ContextIssuePicker.vue'
11
12
  import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
12
13
 
13
14
  const { t } = useI18n()
@@ -24,6 +25,9 @@ const open = computed({
24
25
  })
25
26
  const back = useIntegrationBack(open)
26
27
 
28
+ // The tracker being browsed. Owned here (not the picker) so the epic action and the
29
+ // ref-input placeholder share the same selected source; passed to the picker via
30
+ // `v-model:source` with `always-show-source` so it's always visible + selectable.
27
31
  const source = ref<TaskSourceKind | undefined>(undefined)
28
32
  const ref_ = ref('')
29
33
  const importing = ref(false)
@@ -34,60 +38,11 @@ const title = computed(() =>
34
38
  ui.taskImport?.containerId ? t('tasks.import.titleCreate') : t('tasks.import.titleBrowse'),
35
39
  )
36
40
 
37
- const sourceItems = computed(() =>
38
- tasks.offeredSources.map((s) => ({ label: s.label, value: s.source })),
39
- )
40
41
  const descriptor = computed(() => (source.value ? tasks.descriptorFor(source.value) : undefined))
41
- const searchable = computed(() => descriptor.value?.searchable ?? false)
42
42
 
43
- // The container (service frame or module) a new task is created in. Also the repo
44
- // scope for the issue search — declared up here so the search watch can read it.
43
+ // The container (service frame or module) a new task is created in.
45
44
  const containerId = ref<string | undefined>(undefined)
46
45
 
47
- // Browse the tracker by free text so an issue can be turned into a task without
48
- // knowing its key. Debounced; a created/imported hit also lands in the list below.
49
- const searchQuery = ref('')
50
- const searchResults = ref<TaskSearchResult[]>([])
51
- const searching = ref(false)
52
- const searchError = ref<string | null>(null)
53
-
54
- let searchTimer: ReturnType<typeof setTimeout> | undefined
55
- // Re-run when the chosen container changes too: a GitHub search is scoped to the
56
- // selected service's repo, so switching containers re-scopes the results.
57
- watch([searchQuery, source, () => containerId.value], () => {
58
- if (searchTimer) clearTimeout(searchTimer)
59
- searchResults.value = []
60
- searchError.value = null
61
- const q = searchQuery.value.trim()
62
- if (!q || !searchable.value) return
63
- searchTimer = setTimeout(runSearch, 300)
64
- })
65
-
66
- async function runSearch() {
67
- const q = searchQuery.value.trim()
68
- if (!q || !source.value) return
69
- searching.value = true
70
- searchError.value = null
71
- try {
72
- // Scope to the selected container's repo so hits stay in-repo and a pasted
73
- // URL / bare issue number resolves to the exact issue.
74
- searchResults.value = await tasks.search(source.value, q, containerId.value)
75
- } catch (e) {
76
- searchResults.value = []
77
- searchError.value = e instanceof Error ? e.message : String(e)
78
- } finally {
79
- searching.value = false
80
- }
81
- }
82
-
83
- // Search hits not yet imported (imported ones already render in the list below).
84
- const importedIds = computed(
85
- () => new Set(tasks.tasks.filter((t) => t.source === source.value).map((t) => t.externalId)),
86
- )
87
- const freshHits = computed(() =>
88
- searchResults.value.filter((r) => !importedIds.value.has(r.externalId)),
89
- )
90
-
91
46
  // Containers a new task can be created in: every service frame and module on the
92
47
  // board. Modules are labelled with their parent frame so the choice is unambiguous.
93
48
  const containerItems = computed(() =>
@@ -104,69 +59,28 @@ const containerItems = computed(() =>
104
59
  watch(open, (isOpen) => {
105
60
  if (isOpen) {
106
61
  ref_.value = ''
107
- searchQuery.value = ''
108
- searchResults.value = []
109
- searchError.value = null
110
62
  source.value = ui.taskImport?.source ?? tasks.offeredSources[0]?.source ?? undefined
111
- // Opened from a service frame → preselect it as the create-in target (and the
112
- // search's repo scope); otherwise fall back to the first container on the board.
63
+ // Opened from a service frame → preselect it as the create-in target; otherwise
64
+ // fall back to the first container on the board.
113
65
  containerId.value = ui.taskImport?.containerId ?? containerItems.value[0]?.value
114
66
  tasks.loadTasks().catch(() => {})
115
67
  }
116
68
  })
117
69
 
118
- // Selecting an issue hands off to the add-task form, prefilled with the issue title
119
- // and the issue staged as linked context (so agents see its description + comments).
120
- // The user still confirms pipeline / preset there before the task is created. The
121
- // issue body is carried when already in hand (imported issues); for a search hit it's
122
- // resolved in the add-task form (by importing). Either way the form shows it read-only
123
- // and folds it into the new task's description, so the original description is visible
124
- // and included — the user adds their own notes on top.
125
- function selectIssue(
126
- issue: { externalId: string; title: string; status?: string; description?: string },
127
- needsImport: boolean,
128
- ) {
129
- if (!source.value || !containerId.value) return
130
- const prefill: AddTaskPrefill = {
131
- title: issue.title,
132
- context: [
133
- {
134
- kind: 'task',
135
- source: source.value,
136
- externalId: issue.externalId,
137
- title: `${issue.externalId} · ${issue.title}`,
138
- subtitle: issue.status || undefined,
139
- icon: descriptor.value?.icon,
140
- description: issue.description || undefined,
141
- needsImport,
142
- },
143
- ],
144
- }
70
+ // Choosing an issue in the picker hands off to the add-task form, prefilled with the
71
+ // issue title and the issue staged as linked context (so agents see its description +
72
+ // comments). The user still confirms pipeline / preset there before the task is
73
+ // created. A search hit / pasted ref carries `needsImport`, so the add-task form
74
+ // resolves its body (by importing) and folds it into the new task's description; an
75
+ // already-imported issue carries its body directly.
76
+ function createFromPick(item: PendingContext) {
77
+ if (!containerId.value) return
78
+ // The picker titles rows as "EXTERNALID · Title"; seed the task with the clean
79
+ // title. A pasted ref has no title (title === the raw ref), so leave it blank for
80
+ // the user to name in the form.
81
+ const seededTitle = item.title === item.externalId ? '' : item.title.replace(/^[^·]+·\s*/, '')
145
82
  ui.closeTaskImport()
146
- ui.openAddTask(containerId.value, prefill)
147
- }
148
-
149
- async function doImport() {
150
- const value = ref_.value.trim()
151
- if (!value || !source.value) return
152
- importing.value = true
153
- try {
154
- const task = await tasks.importTask(source.value, value)
155
- ref_.value = ''
156
- toast.add({
157
- title: t('tasks.import.imported', { title: task.title }),
158
- icon: 'i-lucide-file-down',
159
- })
160
- } catch (e) {
161
- toast.add({
162
- title: t('tasks.import.importFailed'),
163
- description: e instanceof Error ? e.message : String(e),
164
- icon: 'i-lucide-triangle-alert',
165
- color: 'error',
166
- })
167
- } finally {
168
- importing.value = false
169
- }
83
+ ui.openAddTask(containerId.value, { title: seededTitle, context: [item] })
170
84
  }
171
85
 
172
86
  // Spawn the referenced issue as an EPIC: an epic node + a task per child issue (into the
@@ -227,54 +141,15 @@ async function doSpawnEpic() {
227
141
  </div>
228
142
  </div>
229
143
 
144
+ <!-- No service frame yet → nowhere to create a task. -->
145
+ <p v-else-if="!containerItems.length" class="text-center text-xs text-slate-500">
146
+ {{ t('tasks.import.needFrameFirst') }}
147
+ </p>
148
+
230
149
  <!-- Main form -->
231
150
  <div v-else class="space-y-4">
232
- <UFormField v-if="sourceItems.length > 1" :label="t('tasks.import.source')">
233
- <USelect v-model="source" :items="sourceItems" class="w-full" />
234
- </UFormField>
235
-
236
- <div class="flex items-end gap-2">
237
- <UFormField :label="descriptor?.refLabel ?? t('tasks.import.refLabel')" class="flex-1">
238
- <UInput
239
- v-model="ref_"
240
- :placeholder="descriptor?.refPlaceholder"
241
- class="w-full"
242
- @keydown.enter="doImport"
243
- />
244
- </UFormField>
245
- <UButton
246
- color="primary"
247
- icon="i-lucide-file-down"
248
- :loading="importing"
249
- :disabled="!ref_.trim()"
250
- @click="doImport"
251
- >
252
- {{ t('tasks.import.import') }}
253
- </UButton>
254
- <UButton
255
- color="primary"
256
- variant="soft"
257
- icon="i-lucide-layers"
258
- :loading="importing"
259
- :disabled="!ref_.trim() || !containerId"
260
- :title="
261
- containerId
262
- ? t('tasks.import.asEpicTitleReady')
263
- : t('tasks.import.asEpicTitleNeedsContainer')
264
- "
265
- @click="doSpawnEpic"
266
- >
267
- {{ t('tasks.import.asEpic') }}
268
- </UButton>
269
- </div>
270
-
271
- <!-- Container for epic children when spawning from a pasted ref (the shared
272
- "Create tasks in" selector below covers the search-results case). -->
273
- <UFormField
274
- v-if="containerItems.length && !freshHits.length"
275
- :label="t('tasks.import.epicChildrenContainer')"
276
- class="w-72"
277
- >
151
+ <!-- Where the new task lands (preselected when opened from a service frame). -->
152
+ <UFormField :label="t('tasks.import.createTasksIn')">
278
153
  <USelect
279
154
  v-model="containerId"
280
155
  :items="containerItems"
@@ -283,94 +158,52 @@ async function doSpawnEpic() {
283
158
  />
284
159
  </UFormField>
285
160
 
286
- <!-- Browse: search the tracker by title so an issue can be turned into a
287
- task without knowing its key. -->
288
- <UFormField v-if="searchable" :label="t('tasks.import.searchIssues')">
289
- <UInput
290
- v-model="searchQuery"
291
- :icon="searching ? 'i-lucide-loader-circle' : 'i-lucide-search'"
292
- :ui="{ leadingIcon: searching ? 'animate-spin' : '' }"
293
- :placeholder="t('tasks.import.searchPlaceholder')"
294
- class="w-full"
161
+ <!-- Find an issue and create a task from it. Same picker the add-task form
162
+ uses for context issues: search by title, pick an already-imported one,
163
+ or paste a URL/key — choosing one opens the prefilled add-task form. The
164
+ search is scoped to the chosen container's repo (so a GitHub search stays
165
+ in that service's repo and a pasted URL / bare number resolves there), and
166
+ the source selector is always shown so it's clear which tracker is in use. -->
167
+ <UFormField :label="t('tasks.import.searchIssues')">
168
+ <ContextIssuePicker
169
+ v-model:source="source"
170
+ :scope-block-id="containerId"
171
+ always-show-source
172
+ @pick="createFromPick"
295
173
  />
296
174
  </UFormField>
297
175
 
298
- <!-- Shared target container for every "Create task" action below. -->
299
- <UFormField
300
- v-if="containerItems.length && freshHits.length"
301
- :label="t('tasks.import.createTasksIn')"
302
- class="w-72"
303
- >
304
- <USelect
305
- v-model="containerId"
306
- :items="containerItems"
307
- :placeholder="t('tasks.import.pickContainer')"
308
- class="w-full"
309
- />
310
- </UFormField>
311
- <p
312
- v-else-if="!containerItems.length && freshHits.length"
313
- class="text-[11px] text-slate-500"
314
- >
315
- {{ t('tasks.import.needFrameFirst') }}
316
- </p>
317
-
318
- <!-- Search results (not yet imported): click a hit to create a task from it
319
- (opens the prefilled add-task form); the icon button views it on GitHub. -->
320
- <div v-if="searchError" class="text-[11px] text-amber-400">
321
- {{ t('tasks.import.searchFailed', { error: searchError }) }}
322
- </div>
323
- <div v-if="freshHits.length" class="space-y-2">
324
- <h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
325
- {{ t('tasks.import.searchResults') }}
326
- </h3>
327
- <div
328
- v-for="hit in freshHits"
329
- :key="`hit:${hit.source}:${hit.externalId}`"
330
- class="flex items-start justify-between gap-2 rounded-lg border border-slate-800 bg-slate-900/60 p-3 transition-colors hover:border-primary-500/60 hover:bg-slate-900"
331
- >
332
- <button
333
- type="button"
334
- class="min-w-0 flex-1 text-start disabled:cursor-not-allowed disabled:opacity-60"
335
- :disabled="!containerId"
176
+ <!-- Secondary: spawn a parent/epic issue as a whole linked task group. -->
177
+ <div class="space-y-2 border-t border-slate-800 pt-3">
178
+ <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
179
+ {{ t('tasks.import.asEpic') }}
180
+ </span>
181
+ <div class="flex items-end gap-2">
182
+ <UFormField :label="descriptor?.refLabel ?? t('tasks.import.refLabel')" class="flex-1">
183
+ <UInput
184
+ v-model="ref_"
185
+ :placeholder="descriptor?.refPlaceholder"
186
+ class="w-full"
187
+ @keydown.enter="doSpawnEpic"
188
+ />
189
+ </UFormField>
190
+ <UButton
191
+ color="primary"
192
+ variant="soft"
193
+ icon="i-lucide-layers"
194
+ :loading="importing"
195
+ :disabled="!ref_.trim() || !containerId"
336
196
  :title="
337
197
  containerId
338
- ? t('tasks.import.createFromIssue')
339
- : t('tasks.import.pickContainerFirst')
198
+ ? t('tasks.import.asEpicTitleReady')
199
+ : t('tasks.import.asEpicTitleNeedsContainer')
340
200
  "
341
- @click="selectIssue(hit, true)"
201
+ @click="doSpawnEpic"
342
202
  >
343
- <span class="block truncate text-sm font-medium text-white">
344
- {{ hit.externalId }} · {{ hit.title }}
345
- </span>
346
- <span v-if="hit.excerpt" class="mt-0.5 line-clamp-2 block text-xs text-slate-500">
347
- {{ hit.excerpt }}
348
- </span>
349
- </button>
350
- <div class="flex shrink-0 items-center gap-2">
351
- <UBadge v-if="hit.status" color="neutral" variant="soft" size="xs">
352
- {{ hit.status }}
353
- </UBadge>
354
- <UButton
355
- color="neutral"
356
- variant="ghost"
357
- size="xs"
358
- icon="i-lucide-external-link"
359
- :to="hit.url"
360
- target="_blank"
361
- rel="noopener"
362
- :aria-label="t('tasks.import.viewOnGitHub', { id: hit.externalId })"
363
- />
364
- </div>
203
+ {{ t('tasks.import.asEpic') }}
204
+ </UButton>
365
205
  </div>
366
206
  </div>
367
-
368
- <p
369
- v-if="!freshHits.length && !searchQuery.trim()"
370
- class="text-center text-xs text-slate-500"
371
- >
372
- {{ t('tasks.import.emptyHint') }}
373
- </p>
374
207
  </div>
375
208
  </template>
376
209
  </UModal>
@@ -62,7 +62,11 @@ export function tasksApi({ send, ws }: ApiContext) {
62
62
  checkTaskSource: (workspaceId: string, source: TaskSourceKind) =>
63
63
  send(diagnoseTaskSourceContract, { pathPrefix: ws(workspaceId), pathParams: { source } }),
64
64
 
65
- listTasks: (workspaceId: string) => send(listTasksContract, { pathPrefix: ws(workspaceId) }),
65
+ // `blockId` scopes the listed issues to that block's service repo for a
66
+ // repo-backed source (GitHub Issues), exactly as search does; omitted → the
67
+ // whole workspace.
68
+ listTasks: (workspaceId: string, blockId?: string) =>
69
+ send(listTasksContract, { pathPrefix: ws(workspaceId), queryParams: { blockId } }),
66
70
 
67
71
  importTask: (workspaceId: string, source: TaskSourceKind, body: { ref: string }) =>
68
72
  send(importTaskContract, { pathPrefix: ws(workspaceId), pathParams: { source }, body }),
@@ -119,6 +119,18 @@ export const useTasksStore = defineStore('tasks', () => {
119
119
  tasks.value = await api.listTasks(workspace.requireId())
120
120
  }
121
121
 
122
+ /**
123
+ * Fetch imported issues scoped to a block's service repo (GitHub only — a
124
+ * repo-backed source narrows to that service's linked repo, exactly as `search`
125
+ * does; repo-less sources are unaffected). Returns the list WITHOUT touching the
126
+ * shared `tasks` state, so a repo-scoped view (the issue picker) can hold its own
127
+ * list without narrowing the workspace-wide one other views rely on. Omit
128
+ * `blockId` for the whole workspace.
129
+ */
130
+ async function listTasksForBlock(blockId?: string): Promise<SourceTask[]> {
131
+ return api.listTasks(workspace.requireId(), blockId)
132
+ }
133
+
122
134
  /** Import (fetch + persist) an issue by key or URL from a source. */
123
135
  async function importTask(source: TaskSourceKind, ref: string): Promise<SourceTask> {
124
136
  loading.value = true
@@ -218,6 +230,7 @@ export const useTasksStore = defineStore('tasks', () => {
218
230
  disconnect,
219
231
  setEnabled,
220
232
  loadTasks,
233
+ listTasksForBlock,
221
234
  importTask,
222
235
  search,
223
236
  linkToBlock,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.74.1",
3
+ "version": "0.74.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",
@@ -34,7 +34,7 @@
34
34
  "valibot": "^1.4.2",
35
35
  "vue": "^3.5.39",
36
36
  "wretch": "^3.0.9",
37
- "@cat-factory/contracts": "0.81.1"
37
+ "@cat-factory/contracts": "0.81.2"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@toad-contracts/testing": "0.3.2",