@cat-factory/app 0.22.0 → 0.23.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.
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// button is disabled with a hint. Linking needs the block id,
|
|
12
12
|
// so chosen items are staged locally and import-and-linked once the task is created
|
|
13
13
|
// (see useContextLinking) — the same context the agents see for every step of the run.
|
|
14
|
-
import type { CreateTaskType, TaskTypeFields } from '~/types/domain'
|
|
14
|
+
import type { CreateTaskType, TaskSourceKind, TaskTypeFields } from '~/types/domain'
|
|
15
15
|
import ContextDocumentPicker from '~/components/documents/ContextDocumentPicker.vue'
|
|
16
16
|
import ContextIssuePicker from '~/components/tasks/ContextIssuePicker.vue'
|
|
17
17
|
|
|
@@ -193,6 +193,61 @@ const issuesConnected = computed(() => tasks.available && tasks.anyOffered)
|
|
|
193
193
|
const pendingDocs = computed(() => pendingContext.value.filter((c) => c.kind === 'document'))
|
|
194
194
|
const pendingIssues = computed(() => pendingContext.value.filter((c) => c.kind === 'task'))
|
|
195
195
|
|
|
196
|
+
// Linked issues whose body is in hand, surfaced read-only above the description so the
|
|
197
|
+
// user SEES the original issue description is included in the task (and can add notes on
|
|
198
|
+
// top). The bodies are folded into the saved description on submit (see `add`).
|
|
199
|
+
const linkedIssueBodies = computed(() =>
|
|
200
|
+
pendingIssues.value
|
|
201
|
+
.filter((i) => (i.description ?? '').trim().length > 0)
|
|
202
|
+
.map((i) => ({ key: contextKey(i), title: i.title, body: (i.description ?? '').trim() })),
|
|
203
|
+
)
|
|
204
|
+
const hasLinkedIssueBody = computed(() => linkedIssueBodies.value.length > 0)
|
|
205
|
+
// True while we're fetching a search-hit issue's body so the read-only preview can show
|
|
206
|
+
// a placeholder instead of silently appearing late.
|
|
207
|
+
const resolvingIssueBodies = ref(false)
|
|
208
|
+
|
|
209
|
+
// A staged issue picked from search results carries no body yet (`needsImport`, and the
|
|
210
|
+
// search result has no description). Resolve it once the form opens — from the local cache
|
|
211
|
+
// when already imported, else by importing it (idempotent; we'd import on add anyway) — so
|
|
212
|
+
// its description can be shown read-only and folded into the task. Best-effort: a failure
|
|
213
|
+
// just leaves that issue without a preview, still linked on add.
|
|
214
|
+
async function resolvePendingIssueBodies() {
|
|
215
|
+
const unresolved = pendingContext.value.filter(
|
|
216
|
+
(c) => c.kind === 'task' && !(c.description ?? '').trim(),
|
|
217
|
+
)
|
|
218
|
+
if (!unresolved.length) return
|
|
219
|
+
resolvingIssueBodies.value = true
|
|
220
|
+
try {
|
|
221
|
+
const resolved: Record<string, string> = {}
|
|
222
|
+
for (const item of unresolved) {
|
|
223
|
+
const source = item.source as TaskSourceKind
|
|
224
|
+
const cached = tasks.tasks.find(
|
|
225
|
+
(t) => t.source === source && t.externalId === item.externalId,
|
|
226
|
+
)
|
|
227
|
+
if ((cached?.description ?? '').trim()) {
|
|
228
|
+
resolved[contextKey(item)] = cached!.description
|
|
229
|
+
continue
|
|
230
|
+
}
|
|
231
|
+
if (!item.needsImport) continue
|
|
232
|
+
try {
|
|
233
|
+
const imported = await tasks.importTask(source, item.externalId)
|
|
234
|
+
if ((imported.description ?? '').trim()) resolved[contextKey(item)] = imported.description
|
|
235
|
+
} catch {
|
|
236
|
+
// Unreadable/forbidden issue — skip the preview; it still links on add.
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (Object.keys(resolved).length) {
|
|
240
|
+
// The issue is now imported, so it links directly on add (needsImport → false).
|
|
241
|
+
pendingContext.value = pendingContext.value.map((c) => {
|
|
242
|
+
const body = resolved[contextKey(c)]
|
|
243
|
+
return body ? { ...c, description: body, needsImport: false } : c
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
} finally {
|
|
247
|
+
resolvingIssueBodies.value = false
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
196
251
|
function addPending(item: PendingContext) {
|
|
197
252
|
if (pendingContext.value.some((c) => contextKey(c) === contextKey(item))) return
|
|
198
253
|
pendingContext.value = [...pendingContext.value, item]
|
|
@@ -241,6 +296,8 @@ watch(open, (isOpen) => {
|
|
|
241
296
|
}
|
|
242
297
|
documents.loadDocuments().catch(() => {})
|
|
243
298
|
tasks.loadTasks().catch(() => {})
|
|
299
|
+
// Fetch any staged search-hit issue's body so its description shows read-only below.
|
|
300
|
+
resolvePendingIssueBodies().catch(() => {})
|
|
244
301
|
})
|
|
245
302
|
|
|
246
303
|
// A recurring task only needs a target frame (its details are filled in the schedule
|
|
@@ -264,21 +321,23 @@ async function add() {
|
|
|
264
321
|
saving.value = true
|
|
265
322
|
try {
|
|
266
323
|
const typeFields = buildTypeFields()
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
324
|
+
// The saved description includes each linked issue's body (shown read-only above)
|
|
325
|
+
// followed by the user's own notes, so the original issue description is part of the
|
|
326
|
+
// task — not only reachable via the context link.
|
|
327
|
+
const notes = description.value.trim()
|
|
328
|
+
const fullDescription =
|
|
329
|
+
[...linkedIssueBodies.value.map((b) => b.body), notes].filter(Boolean).join('\n\n') ||
|
|
330
|
+
undefined
|
|
331
|
+
const block = await board.addTask(containerId, title.value.trim(), fullDescription, {
|
|
332
|
+
taskType: taskType.value as CreateTaskType,
|
|
333
|
+
...(typeFields ? { taskTypeFields: typeFields } : {}),
|
|
334
|
+
...(mergePresetId.value ? { mergePresetId: mergePresetId.value } : {}),
|
|
335
|
+
...(modelPresetId.value ? { modelPresetId: modelPresetId.value } : {}),
|
|
336
|
+
...(pipelineId.value ? { pipelineId: pipelineId.value } : {}),
|
|
337
|
+
...(Object.keys(agentConfigValues.value).length
|
|
338
|
+
? { agentConfig: agentConfigValues.value }
|
|
339
|
+
: {}),
|
|
340
|
+
})
|
|
282
341
|
if (block) {
|
|
283
342
|
const failed = await linkPending(block.id, pendingContext.value)
|
|
284
343
|
if (failed > 0) {
|
|
@@ -352,12 +411,37 @@ async function add() {
|
|
|
352
411
|
/>
|
|
353
412
|
</UFormField>
|
|
354
413
|
|
|
355
|
-
|
|
414
|
+
<!-- Linked issue description(s), read-only: shown so the user sees the original
|
|
415
|
+
issue description is included in the task. It's folded into the saved
|
|
416
|
+
description (before their notes) on add. -->
|
|
417
|
+
<UFormField
|
|
418
|
+
v-for="issue in linkedIssueBodies"
|
|
419
|
+
:key="issue.key"
|
|
420
|
+
:label="`${issue.title} (from issue, included)`"
|
|
421
|
+
>
|
|
422
|
+
<UTextarea
|
|
423
|
+
:model-value="issue.body"
|
|
424
|
+
:rows="4"
|
|
425
|
+
autoresize
|
|
426
|
+
readonly
|
|
427
|
+
class="w-full"
|
|
428
|
+
:ui="{ base: 'cursor-default text-slate-300' }"
|
|
429
|
+
/>
|
|
430
|
+
</UFormField>
|
|
431
|
+
<p v-if="resolvingIssueBodies" class="text-[11px] text-slate-500">
|
|
432
|
+
Loading the linked issue's description…
|
|
433
|
+
</p>
|
|
434
|
+
|
|
435
|
+
<UFormField :label="hasLinkedIssueBody ? 'Additional notes' : 'Description'">
|
|
356
436
|
<UTextarea
|
|
357
437
|
v-model="description"
|
|
358
438
|
:rows="4"
|
|
359
439
|
autoresize
|
|
360
|
-
placeholder="
|
|
440
|
+
:placeholder="
|
|
441
|
+
hasLinkedIssueBody
|
|
442
|
+
? 'Add anything else the agent should know — appended to the issue description above…'
|
|
443
|
+
: 'Describe the work — context, acceptance criteria, anything the agent should know…'
|
|
444
|
+
"
|
|
361
445
|
class="w-full"
|
|
362
446
|
/>
|
|
363
447
|
</UFormField>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// It only *stages* a choice: the caller collects PendingContext items and links
|
|
7
7
|
// them once the block exists (see useContextLinking). A search hit / pasted ref
|
|
8
8
|
// carries `needsImport: true` so it's fetched + persisted before linking.
|
|
9
|
-
import type { TaskSearchResult, TaskSourceKind } from '~/types/domain'
|
|
9
|
+
import type { SourceTask, TaskSearchResult, TaskSourceKind } from '~/types/domain'
|
|
10
10
|
|
|
11
11
|
const props = defineProps<{
|
|
12
12
|
/** contextKeys already staged by the caller, so they're filtered out / not re-offered. */
|
|
@@ -120,15 +120,18 @@ const empty = computed(
|
|
|
120
120
|
refRow.value === null,
|
|
121
121
|
)
|
|
122
122
|
|
|
123
|
-
function pickImported(
|
|
123
|
+
function pickImported(task: SourceTask) {
|
|
124
124
|
if (!source.value) return
|
|
125
125
|
emit('pick', {
|
|
126
126
|
kind: 'task',
|
|
127
127
|
source: source.value,
|
|
128
|
-
externalId,
|
|
129
|
-
title: `${externalId} · ${title}`,
|
|
130
|
-
subtitle: status || undefined,
|
|
128
|
+
externalId: task.externalId,
|
|
129
|
+
title: `${task.externalId} · ${task.title}`,
|
|
130
|
+
subtitle: task.status || undefined,
|
|
131
131
|
icon: icon.value,
|
|
132
|
+
// Already imported, so its body is in hand — carry it so the add-task form can
|
|
133
|
+
// show it read-only and fold it into the new task's description.
|
|
134
|
+
description: task.description || undefined,
|
|
132
135
|
needsImport: false,
|
|
133
136
|
})
|
|
134
137
|
}
|
|
@@ -200,7 +203,7 @@ onMounted(() => {
|
|
|
200
203
|
:key="`imp:${t.externalId}`"
|
|
201
204
|
type="button"
|
|
202
205
|
class="flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-xs text-slate-300 hover:bg-slate-800/70"
|
|
203
|
-
@click="pickImported(t
|
|
206
|
+
@click="pickImported(t)"
|
|
204
207
|
>
|
|
205
208
|
<UIcon :name="icon" class="h-3.5 w-3.5 shrink-0 text-indigo-400" />
|
|
206
209
|
<span class="truncate">{{ t.externalId }} · {{ t.title }}</span>
|
|
@@ -118,10 +118,13 @@ watch(open, (isOpen) => {
|
|
|
118
118
|
|
|
119
119
|
// Selecting an issue hands off to the add-task form, prefilled with the issue title
|
|
120
120
|
// and the issue staged as linked context (so agents see its description + comments).
|
|
121
|
-
// The user still confirms pipeline / preset there before the task is created
|
|
122
|
-
//
|
|
121
|
+
// The user still confirms pipeline / preset there before the task is created. The
|
|
122
|
+
// issue body is carried when already in hand (imported issues); for a search hit it's
|
|
123
|
+
// resolved in the add-task form (by importing). Either way the form shows it read-only
|
|
124
|
+
// and folds it into the new task's description, so the original description is visible
|
|
125
|
+
// and included — the user adds their own notes on top.
|
|
123
126
|
function selectIssue(
|
|
124
|
-
issue: { externalId: string; title: string; status?: string },
|
|
127
|
+
issue: { externalId: string; title: string; status?: string; description?: string },
|
|
125
128
|
needsImport: boolean,
|
|
126
129
|
) {
|
|
127
130
|
if (!source.value || !containerId.value) return
|
|
@@ -135,6 +138,7 @@ function selectIssue(
|
|
|
135
138
|
title: `${issue.externalId} · ${issue.title}`,
|
|
136
139
|
subtitle: issue.status || undefined,
|
|
137
140
|
icon: descriptor.value?.icon,
|
|
141
|
+
description: issue.description || undefined,
|
|
138
142
|
needsImport,
|
|
139
143
|
},
|
|
140
144
|
],
|
|
@@ -19,6 +19,13 @@ export interface PendingContext {
|
|
|
19
19
|
subtitle?: string
|
|
20
20
|
/** Lucide icon for the row. */
|
|
21
21
|
icon?: string
|
|
22
|
+
/**
|
|
23
|
+
* The item's body/description (Markdown), when known. Populated for an
|
|
24
|
+
* already-imported issue at pick time and resolved (by importing) for a search
|
|
25
|
+
* hit when the add-task form opens, so the form can surface it read-only and
|
|
26
|
+
* fold it into the new task's description. Absent until resolved.
|
|
27
|
+
*/
|
|
28
|
+
description?: string
|
|
22
29
|
/** True when the item must be imported before it can be linked. */
|
|
23
30
|
needsImport: boolean
|
|
24
31
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|