@cat-factory/app 0.33.0 → 0.35.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.
- package/app/components/fragments/FragmentLibraryManager.vue +511 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +12 -458
- package/app/components/layout/AccountFragmentSettings.vue +37 -0
- package/app/components/layout/BoardSwitcher.vue +4 -3
- package/app/components/layout/CommandBar.vue +9 -0
- package/app/components/layout/IntegrationsHub.vue +13 -0
- package/app/components/settings/AccountSettingsPanel.vue +40 -7
- package/app/components/settings/LocalModeSettingsPanel.vue +159 -0
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +21 -0
- package/app/composables/api/fragments.ts +12 -3
- package/app/composables/api/localSettings.ts +17 -0
- package/app/composables/useApi.ts +2 -0
- package/app/pages/index.vue +2 -0
- package/app/stores/fragmentLibrary.ts +80 -31
- package/app/stores/localSettings.ts +48 -0
- package/app/stores/ui.ts +26 -3
- package/app/types/localSettings.ts +31 -0
- package/package.json +1 -1
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
import type { DocumentSourceKind, ResolvedFragment } from '~/types/domain'
|
|
2
|
+
// The board (workspace-tier) prompt-fragment library modal (ADR 0006). A thin shell
|
|
3
|
+
// around the shared FragmentLibraryManager at the active board's scope — including
|
|
4
|
+
// the resolved/merged catalog view so the built-in ∪ account ∪ workspace inheritance
|
|
5
|
+
// is visible. Opened from the navbar / command bar via the ui store.
|
|
6
|
+
import FragmentLibraryManager from '~/components/fragments/FragmentLibraryManager.vue'
|
|
8
7
|
|
|
9
8
|
const ui = useUiStore()
|
|
10
|
-
const
|
|
11
|
-
const documents = useDocumentsStore()
|
|
12
|
-
const toast = useToast()
|
|
9
|
+
const workspace = useWorkspaceStore()
|
|
13
10
|
|
|
14
11
|
const open = computed({
|
|
15
12
|
get: () => ui.fragmentLibraryOpen,
|
|
@@ -17,460 +14,17 @@ const open = computed({
|
|
|
17
14
|
if (!v) ui.closeFragmentLibrary()
|
|
18
15
|
},
|
|
19
16
|
})
|
|
20
|
-
|
|
21
|
-
watch(open, (isOpen) => {
|
|
22
|
-
if (isOpen) {
|
|
23
|
-
void library.probe()
|
|
24
|
-
void documents.probe()
|
|
25
|
-
}
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
type Tab = 'catalog' | 'authored' | 'documents' | 'sources'
|
|
29
|
-
const tab = ref<Tab>('catalog')
|
|
30
|
-
|
|
31
|
-
const tierLabel: Record<ResolvedFragment['tier'], string> = {
|
|
32
|
-
builtin: 'Built-in',
|
|
33
|
-
account: 'Account',
|
|
34
|
-
workspace: 'This board',
|
|
35
|
-
}
|
|
36
|
-
// `as const` keeps the literal color names (assignable to UBadge's `color`
|
|
37
|
-
// union) instead of widening to `string`; `satisfies` still checks the shape.
|
|
38
|
-
const tierColor = {
|
|
39
|
-
builtin: 'neutral',
|
|
40
|
-
account: 'info',
|
|
41
|
-
workspace: 'primary',
|
|
42
|
-
} as const satisfies Record<ResolvedFragment['tier'], string>
|
|
43
|
-
|
|
44
|
-
function notifyError(title: string, e: unknown) {
|
|
45
|
-
toast.add({
|
|
46
|
-
title,
|
|
47
|
-
description: e instanceof Error ? e.message : String(e),
|
|
48
|
-
icon: 'i-lucide-triangle-alert',
|
|
49
|
-
color: 'error',
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ---- create a hand-authored fragment --------------------------------------
|
|
54
|
-
const draft = ref({ title: '', summary: '', body: '', tags: '' })
|
|
55
|
-
const draftValid = computed(
|
|
56
|
-
() => draft.value.title.trim() && draft.value.summary.trim() && draft.value.body.trim(),
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
async function createFragment() {
|
|
60
|
-
if (!draftValid.value) return
|
|
61
|
-
try {
|
|
62
|
-
await library.create({
|
|
63
|
-
title: draft.value.title.trim(),
|
|
64
|
-
summary: draft.value.summary.trim(),
|
|
65
|
-
body: draft.value.body.trim(),
|
|
66
|
-
tags: draft.value.tags
|
|
67
|
-
.split(',')
|
|
68
|
-
.map((t) => t.trim())
|
|
69
|
-
.filter(Boolean),
|
|
70
|
-
})
|
|
71
|
-
draft.value = { title: '', summary: '', body: '', tags: '' }
|
|
72
|
-
toast.add({ title: 'Fragment added', icon: 'i-lucide-check' })
|
|
73
|
-
} catch (e) {
|
|
74
|
-
notifyError('Could not add fragment', e)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function removeFragment(id: string) {
|
|
79
|
-
try {
|
|
80
|
-
await library.remove(id)
|
|
81
|
-
toast.add({ title: 'Fragment removed', icon: 'i-lucide-trash-2' })
|
|
82
|
-
} catch (e) {
|
|
83
|
-
notifyError('Could not remove fragment', e)
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ---- document-backed (living) fragments -----------------------------------
|
|
88
|
-
// Link a Confluence/Notion page or GitHub file as a fragment that is re-resolved
|
|
89
|
-
// from the source at run time (a living source of truth, not a frozen snapshot).
|
|
90
|
-
const docDraft = ref({ source: '' as DocumentSourceKind | '', ref: '', tags: '' })
|
|
91
|
-
const docDraftValid = computed(() => docDraft.value.source && docDraft.value.ref.trim())
|
|
92
|
-
|
|
93
|
-
/** The board's existing document-backed fragments (workspace tier). */
|
|
94
|
-
const documentFragments = computed(() => library.fragments.filter((f) => f.documentRef))
|
|
95
|
-
|
|
96
|
-
async function linkDocumentFragment() {
|
|
97
|
-
if (!docDraftValid.value) return
|
|
98
|
-
try {
|
|
99
|
-
await library.createDocumentFragment({
|
|
100
|
-
source: docDraft.value.source as DocumentSourceKind,
|
|
101
|
-
ref: docDraft.value.ref.trim(),
|
|
102
|
-
tags: docDraft.value.tags
|
|
103
|
-
.split(',')
|
|
104
|
-
.map((t) => t.trim())
|
|
105
|
-
.filter(Boolean),
|
|
106
|
-
})
|
|
107
|
-
docDraft.value = { source: '', ref: '', tags: '' }
|
|
108
|
-
toast.add({ title: 'Document linked as a living fragment', icon: 'i-lucide-link' })
|
|
109
|
-
} catch (e) {
|
|
110
|
-
notifyError('Could not link document', e)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async function refreshFragment(id: string) {
|
|
115
|
-
try {
|
|
116
|
-
await library.refreshDocumentFragment(id)
|
|
117
|
-
toast.add({ title: 'Fragment re-resolved from source', icon: 'i-lucide-refresh-cw' })
|
|
118
|
-
} catch (e) {
|
|
119
|
-
notifyError('Could not refresh fragment', e)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ---- repo sources ----------------------------------------------------------
|
|
124
|
-
const sourceDraft = ref({ repoOwner: '', repoName: '', dirPath: '', gitRef: '' })
|
|
125
|
-
const sourceValid = computed(
|
|
126
|
-
() => sourceDraft.value.repoOwner.trim() && sourceDraft.value.repoName.trim(),
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
async function linkSource() {
|
|
130
|
-
if (!sourceValid.value) return
|
|
131
|
-
try {
|
|
132
|
-
const source = await library.linkSource({
|
|
133
|
-
repoOwner: sourceDraft.value.repoOwner.trim(),
|
|
134
|
-
repoName: sourceDraft.value.repoName.trim(),
|
|
135
|
-
dirPath: sourceDraft.value.dirPath.trim() || undefined,
|
|
136
|
-
gitRef: sourceDraft.value.gitRef.trim() || undefined,
|
|
137
|
-
})
|
|
138
|
-
sourceDraft.value = { repoOwner: '', repoName: '', dirPath: '', gitRef: '' }
|
|
139
|
-
await library.syncSource(source.id)
|
|
140
|
-
toast.add({ title: 'Source linked & synced', icon: 'i-lucide-git-branch' })
|
|
141
|
-
} catch (e) {
|
|
142
|
-
notifyError('Could not link source', e)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function syncSource(id: string) {
|
|
147
|
-
try {
|
|
148
|
-
const result = await library.syncSource(id)
|
|
149
|
-
toast.add({
|
|
150
|
-
title: `Synced: ${result.upserted} updated, ${result.tombstoned} removed`,
|
|
151
|
-
icon: 'i-lucide-refresh-cw',
|
|
152
|
-
color: 'info',
|
|
153
|
-
})
|
|
154
|
-
} catch (e) {
|
|
155
|
-
notifyError('Could not sync source', e)
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function checkSource(id: string) {
|
|
160
|
-
try {
|
|
161
|
-
const status = await library.checkSource(id)
|
|
162
|
-
toast.add({
|
|
163
|
-
title: status.changed ? `${status.changedCount} change(s) available` : 'Up to date',
|
|
164
|
-
icon: status.changed ? 'i-lucide-bell-dot' : 'i-lucide-check',
|
|
165
|
-
})
|
|
166
|
-
} catch (e) {
|
|
167
|
-
notifyError('Could not check source', e)
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function unlinkSource(id: string) {
|
|
172
|
-
try {
|
|
173
|
-
await library.unlinkSource(id)
|
|
174
|
-
toast.add({ title: 'Source unlinked', icon: 'i-lucide-unplug' })
|
|
175
|
-
} catch (e) {
|
|
176
|
-
notifyError('Could not unlink source', e)
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
17
|
</script>
|
|
180
18
|
|
|
181
19
|
<template>
|
|
182
20
|
<UModal v-model:open="open" title="Prompt-fragment library" :ui="{ content: 'max-w-3xl' }">
|
|
183
21
|
<template #body>
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
<div class="flex gap-2">
|
|
192
|
-
<UButton
|
|
193
|
-
v-for="t in ['catalog', 'authored', 'documents', 'sources'] as Tab[]"
|
|
194
|
-
:key="t"
|
|
195
|
-
:color="tab === t ? 'primary' : 'neutral'"
|
|
196
|
-
:variant="tab === t ? 'solid' : 'ghost'"
|
|
197
|
-
size="sm"
|
|
198
|
-
@click="tab = t"
|
|
199
|
-
>
|
|
200
|
-
{{
|
|
201
|
-
t === 'catalog'
|
|
202
|
-
? 'Resolved catalog'
|
|
203
|
-
: t === 'authored'
|
|
204
|
-
? 'This board'
|
|
205
|
-
: t === 'documents'
|
|
206
|
-
? 'Documents'
|
|
207
|
-
: 'Repo sources'
|
|
208
|
-
}}
|
|
209
|
-
</UButton>
|
|
210
|
-
</div>
|
|
211
|
-
|
|
212
|
-
<!-- Resolved (merged) catalog -->
|
|
213
|
-
<div v-if="tab === 'catalog'" class="flex flex-col gap-2">
|
|
214
|
-
<p class="text-xs text-slate-500">
|
|
215
|
-
{{ library.resolved.length }} fragment(s) resolved ·
|
|
216
|
-
{{ library.builtinCount }} built-in.
|
|
217
|
-
</p>
|
|
218
|
-
<div
|
|
219
|
-
v-for="f in library.resolved"
|
|
220
|
-
:key="f.id"
|
|
221
|
-
class="rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
222
|
-
>
|
|
223
|
-
<div class="flex items-center gap-2">
|
|
224
|
-
<span class="font-medium text-slate-100">{{ f.title }}</span>
|
|
225
|
-
<UBadge size="xs" :color="tierColor[f.tier]" variant="subtle">
|
|
226
|
-
{{ tierLabel[f.tier] }}
|
|
227
|
-
</UBadge>
|
|
228
|
-
<UBadge
|
|
229
|
-
v-if="f.documentRef"
|
|
230
|
-
size="xs"
|
|
231
|
-
color="success"
|
|
232
|
-
variant="subtle"
|
|
233
|
-
icon="i-lucide-radio"
|
|
234
|
-
>
|
|
235
|
-
Live · {{ f.documentRef.source }}
|
|
236
|
-
</UBadge>
|
|
237
|
-
<span class="ml-auto font-mono text-[11px] text-slate-500">{{ f.id }}</span>
|
|
238
|
-
</div>
|
|
239
|
-
<p class="mt-1 text-sm text-slate-400">{{ f.summary }}</p>
|
|
240
|
-
<div v-if="f.tags?.length" class="mt-1 flex flex-wrap gap-1">
|
|
241
|
-
<UBadge v-for="tag in f.tags" :key="tag" size="xs" variant="outline" color="neutral">
|
|
242
|
-
{{ tag }}
|
|
243
|
-
</UBadge>
|
|
244
|
-
</div>
|
|
245
|
-
</div>
|
|
246
|
-
</div>
|
|
247
|
-
|
|
248
|
-
<!-- Hand-authored (workspace tier) -->
|
|
249
|
-
<div v-else-if="tab === 'authored'" class="flex flex-col gap-3">
|
|
250
|
-
<div
|
|
251
|
-
v-for="f in library.fragments"
|
|
252
|
-
:key="f.id"
|
|
253
|
-
class="flex items-start gap-2 rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
254
|
-
>
|
|
255
|
-
<div class="min-w-0">
|
|
256
|
-
<div class="flex items-center gap-2">
|
|
257
|
-
<span class="font-medium text-slate-100">{{ f.title }}</span>
|
|
258
|
-
<UBadge v-if="f.source" size="xs" color="info" variant="subtle">from repo</UBadge>
|
|
259
|
-
</div>
|
|
260
|
-
<p class="text-sm text-slate-400">{{ f.summary }}</p>
|
|
261
|
-
</div>
|
|
262
|
-
<UButton
|
|
263
|
-
icon="i-lucide-trash-2"
|
|
264
|
-
size="xs"
|
|
265
|
-
color="error"
|
|
266
|
-
variant="ghost"
|
|
267
|
-
class="ml-auto"
|
|
268
|
-
@click="removeFragment(f.id)"
|
|
269
|
-
/>
|
|
270
|
-
</div>
|
|
271
|
-
<p v-if="!library.fragments.length" class="text-sm text-slate-500">
|
|
272
|
-
No board-specific fragments yet. Add one below, or override a built-in by using its id.
|
|
273
|
-
</p>
|
|
274
|
-
|
|
275
|
-
<div class="rounded-md border border-slate-800 p-3">
|
|
276
|
-
<p class="mb-2 text-sm font-medium">Add a fragment</p>
|
|
277
|
-
<div class="flex flex-col gap-2">
|
|
278
|
-
<UInput v-model="draft.title" placeholder="Title" />
|
|
279
|
-
<UInput
|
|
280
|
-
v-model="draft.summary"
|
|
281
|
-
placeholder="One-line summary (used by the selector)"
|
|
282
|
-
/>
|
|
283
|
-
<UTextarea
|
|
284
|
-
v-model="draft.body"
|
|
285
|
-
placeholder="Guidance body (injected into the prompt)"
|
|
286
|
-
:rows="4"
|
|
287
|
-
/>
|
|
288
|
-
<UInput v-model="draft.tags" placeholder="Tags, comma-separated (e.g. backend, db)" />
|
|
289
|
-
<UButton
|
|
290
|
-
icon="i-lucide-plus"
|
|
291
|
-
size="sm"
|
|
292
|
-
:disabled="!draftValid"
|
|
293
|
-
:loading="library.loading"
|
|
294
|
-
class="self-start"
|
|
295
|
-
@click="createFragment"
|
|
296
|
-
>
|
|
297
|
-
Add fragment
|
|
298
|
-
</UButton>
|
|
299
|
-
</div>
|
|
300
|
-
</div>
|
|
301
|
-
</div>
|
|
302
|
-
|
|
303
|
-
<!-- Document-backed (living) fragments -->
|
|
304
|
-
<div v-else-if="tab === 'documents'" class="flex flex-col gap-3">
|
|
305
|
-
<p class="text-xs text-slate-500">
|
|
306
|
-
Link a Confluence/Notion page or a GitHub file as a best-practice fragment. Its guidance
|
|
307
|
-
is re-resolved from the source at run time — edit the doc and the next agent run follows
|
|
308
|
-
the new version (no re-import).
|
|
309
|
-
</p>
|
|
310
|
-
|
|
311
|
-
<div
|
|
312
|
-
v-for="f in documentFragments"
|
|
313
|
-
:key="f.id"
|
|
314
|
-
class="flex items-start gap-2 rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
315
|
-
>
|
|
316
|
-
<UIcon name="i-lucide-radio" class="mt-0.5 h-4 w-4 text-emerald-400" />
|
|
317
|
-
<div class="min-w-0">
|
|
318
|
-
<div class="flex items-center gap-2">
|
|
319
|
-
<span class="font-medium text-slate-100">{{ f.title }}</span>
|
|
320
|
-
<UBadge size="xs" color="success" variant="subtle">
|
|
321
|
-
{{ f.documentRef?.source }}
|
|
322
|
-
</UBadge>
|
|
323
|
-
</div>
|
|
324
|
-
<p class="text-sm text-slate-400">{{ f.summary }}</p>
|
|
325
|
-
<p v-if="f.resolvedAt" class="text-[11px] text-slate-500">
|
|
326
|
-
last resolved {{ new Date(f.resolvedAt).toLocaleString() }}
|
|
327
|
-
</p>
|
|
328
|
-
</div>
|
|
329
|
-
<div class="ml-auto flex gap-1">
|
|
330
|
-
<UButton
|
|
331
|
-
icon="i-lucide-refresh-cw"
|
|
332
|
-
size="xs"
|
|
333
|
-
variant="ghost"
|
|
334
|
-
:loading="library.loading"
|
|
335
|
-
title="Re-resolve from source now"
|
|
336
|
-
@click="refreshFragment(f.id)"
|
|
337
|
-
/>
|
|
338
|
-
<UButton
|
|
339
|
-
icon="i-lucide-trash-2"
|
|
340
|
-
size="xs"
|
|
341
|
-
color="error"
|
|
342
|
-
variant="ghost"
|
|
343
|
-
@click="removeFragment(f.id)"
|
|
344
|
-
/>
|
|
345
|
-
</div>
|
|
346
|
-
</div>
|
|
347
|
-
<p v-if="!documentFragments.length" class="text-sm text-slate-500">
|
|
348
|
-
No document-backed fragments yet. Link one below.
|
|
349
|
-
</p>
|
|
350
|
-
|
|
351
|
-
<div class="rounded-md border border-slate-800 p-3">
|
|
352
|
-
<p class="mb-2 text-sm font-medium">Link a document</p>
|
|
353
|
-
<div v-if="!documents.connectedSources.length" class="text-sm text-slate-500">
|
|
354
|
-
Connect a document source (Confluence, Notion or GitHub) under Integrations first.
|
|
355
|
-
</div>
|
|
356
|
-
<div v-else class="flex flex-col gap-2">
|
|
357
|
-
<div class="flex flex-wrap gap-2">
|
|
358
|
-
<UButton
|
|
359
|
-
v-for="s in documents.connectedSources"
|
|
360
|
-
:key="s.source"
|
|
361
|
-
size="xs"
|
|
362
|
-
:color="docDraft.source === s.source ? 'primary' : 'neutral'"
|
|
363
|
-
:variant="docDraft.source === s.source ? 'solid' : 'outline'"
|
|
364
|
-
@click="docDraft.source = s.source"
|
|
365
|
-
>
|
|
366
|
-
{{ s.label }}
|
|
367
|
-
</UButton>
|
|
368
|
-
</div>
|
|
369
|
-
<UInput
|
|
370
|
-
v-model="docDraft.ref"
|
|
371
|
-
placeholder="Page id or URL (e.g. a Confluence/Notion page or GitHub file URL)"
|
|
372
|
-
/>
|
|
373
|
-
<UInput v-model="docDraft.tags" placeholder="Tags, comma-separated (optional)" />
|
|
374
|
-
<UButton
|
|
375
|
-
icon="i-lucide-link"
|
|
376
|
-
size="sm"
|
|
377
|
-
:disabled="!docDraftValid"
|
|
378
|
-
:loading="library.loading"
|
|
379
|
-
class="self-start"
|
|
380
|
-
@click="linkDocumentFragment"
|
|
381
|
-
>
|
|
382
|
-
Link as living fragment
|
|
383
|
-
</UButton>
|
|
384
|
-
</div>
|
|
385
|
-
</div>
|
|
386
|
-
</div>
|
|
387
|
-
|
|
388
|
-
<!-- Repo sources -->
|
|
389
|
-
<div v-else class="flex flex-col gap-3">
|
|
390
|
-
<div
|
|
391
|
-
v-for="s in library.sources"
|
|
392
|
-
:key="s.id"
|
|
393
|
-
class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
394
|
-
>
|
|
395
|
-
<UIcon name="i-lucide-git-branch" class="h-4 w-4 text-slate-400" />
|
|
396
|
-
<div class="min-w-0">
|
|
397
|
-
<span class="font-mono text-sm text-slate-100">
|
|
398
|
-
{{ s.repoOwner }}/{{ s.repoName
|
|
399
|
-
}}<span class="text-slate-500">/{{ s.dirPath || '' }}</span>
|
|
400
|
-
</span>
|
|
401
|
-
<p class="text-xs text-slate-500">
|
|
402
|
-
{{ s.lastSyncedAt ? 'synced' : 'never synced' }} · ref {{ s.gitRef }}
|
|
403
|
-
</p>
|
|
404
|
-
</div>
|
|
405
|
-
<UBadge
|
|
406
|
-
v-if="library.sourceChanges[s.id]"
|
|
407
|
-
size="xs"
|
|
408
|
-
color="warning"
|
|
409
|
-
variant="subtle"
|
|
410
|
-
class="ml-auto"
|
|
411
|
-
>
|
|
412
|
-
{{ library.sourceChanges[s.id] }} change(s)
|
|
413
|
-
</UBadge>
|
|
414
|
-
<div class="ml-auto flex gap-1">
|
|
415
|
-
<UButton
|
|
416
|
-
icon="i-lucide-search-check"
|
|
417
|
-
size="xs"
|
|
418
|
-
variant="ghost"
|
|
419
|
-
@click="checkSource(s.id)"
|
|
420
|
-
/>
|
|
421
|
-
<UButton
|
|
422
|
-
icon="i-lucide-refresh-cw"
|
|
423
|
-
size="xs"
|
|
424
|
-
variant="ghost"
|
|
425
|
-
:loading="library.loading"
|
|
426
|
-
@click="syncSource(s.id)"
|
|
427
|
-
/>
|
|
428
|
-
<UButton
|
|
429
|
-
icon="i-lucide-unplug"
|
|
430
|
-
size="xs"
|
|
431
|
-
color="error"
|
|
432
|
-
variant="ghost"
|
|
433
|
-
@click="unlinkSource(s.id)"
|
|
434
|
-
/>
|
|
435
|
-
</div>
|
|
436
|
-
</div>
|
|
437
|
-
<p v-if="!library.sources.length" class="text-sm text-slate-500">
|
|
438
|
-
No linked guideline repos. Link one below to import its Markdown files as fragments.
|
|
439
|
-
</p>
|
|
440
|
-
|
|
441
|
-
<div class="rounded-md border border-slate-800 p-3">
|
|
442
|
-
<p class="mb-2 text-sm font-medium">Link a guideline repo</p>
|
|
443
|
-
<div class="flex flex-col gap-2">
|
|
444
|
-
<div class="flex gap-2">
|
|
445
|
-
<UInput v-model="sourceDraft.repoOwner" placeholder="owner" class="flex-1" />
|
|
446
|
-
<UInput v-model="sourceDraft.repoName" placeholder="repo" class="flex-1" />
|
|
447
|
-
</div>
|
|
448
|
-
<div class="flex gap-2">
|
|
449
|
-
<UInput
|
|
450
|
-
v-model="sourceDraft.dirPath"
|
|
451
|
-
placeholder="dir path (e.g. guidelines)"
|
|
452
|
-
class="flex-1"
|
|
453
|
-
/>
|
|
454
|
-
<UInput
|
|
455
|
-
v-model="sourceDraft.gitRef"
|
|
456
|
-
placeholder="ref (default HEAD)"
|
|
457
|
-
class="flex-1"
|
|
458
|
-
/>
|
|
459
|
-
</div>
|
|
460
|
-
<UButton
|
|
461
|
-
icon="i-lucide-link"
|
|
462
|
-
size="sm"
|
|
463
|
-
:disabled="!sourceValid"
|
|
464
|
-
:loading="library.loading"
|
|
465
|
-
class="self-start"
|
|
466
|
-
@click="linkSource"
|
|
467
|
-
>
|
|
468
|
-
Link & sync
|
|
469
|
-
</UButton>
|
|
470
|
-
</div>
|
|
471
|
-
</div>
|
|
472
|
-
</div>
|
|
473
|
-
</div>
|
|
22
|
+
<FragmentLibraryManager
|
|
23
|
+
v-if="workspace.workspaceId"
|
|
24
|
+
kind="workspace"
|
|
25
|
+
:owner-id="workspace.workspaceId"
|
|
26
|
+
show-catalog
|
|
27
|
+
/>
|
|
474
28
|
</template>
|
|
475
29
|
</UModal>
|
|
476
30
|
</template>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Account-tier prompt-fragment library (ADR 0006): best-practice guidelines shared by
|
|
3
|
+
// every board in the account, which each board inherits and can override. A body-only
|
|
4
|
+
// section rendered in the "Context fragments" tab of AccountSettingsPanel; available for
|
|
5
|
+
// ALL account types (unlike the org-only Team tab).
|
|
6
|
+
import { computed } from 'vue'
|
|
7
|
+
import FragmentLibraryManager from '~/components/fragments/FragmentLibraryManager.vue'
|
|
8
|
+
|
|
9
|
+
const props = defineProps<{ accountId: string }>()
|
|
10
|
+
const workspace = useWorkspaceStore()
|
|
11
|
+
|
|
12
|
+
// Account-tier document fragments are fetched through a board's stored
|
|
13
|
+
// document-source connection (credentials are per-workspace). Prefer the active
|
|
14
|
+
// board when it belongs to this account, else the account's first board.
|
|
15
|
+
const viaWorkspaceId = computed(() => {
|
|
16
|
+
const boards = workspace.accountWorkspaces
|
|
17
|
+
return boards.find((w) => w.id === workspace.workspaceId)?.id ?? boards[0]?.id
|
|
18
|
+
})
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<div class="space-y-6 text-sm">
|
|
23
|
+
<section>
|
|
24
|
+
<p class="mb-3 text-[11px] text-slate-400">
|
|
25
|
+
Best-practice guidelines shared by every board in this account. Each board inherits these
|
|
26
|
+
and can override or add its own. Code-aware agents (coder, reviewer, architect, fixers) fold
|
|
27
|
+
the relevant ones into their prompt.
|
|
28
|
+
</p>
|
|
29
|
+
<FragmentLibraryManager
|
|
30
|
+
kind="account"
|
|
31
|
+
:owner-id="accountId"
|
|
32
|
+
:via-workspace-id="viaWorkspaceId"
|
|
33
|
+
:show-catalog="false"
|
|
34
|
+
/>
|
|
35
|
+
</section>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
@@ -53,11 +53,12 @@ const accountItems = computed<DropdownMenuItem[][]>(() => [
|
|
|
53
53
|
})),
|
|
54
54
|
[
|
|
55
55
|
{ label: 'New organization…', icon: 'i-lucide-plus', onSelect: () => openPrompt('account') },
|
|
56
|
-
// Account
|
|
57
|
-
//
|
|
56
|
+
// Account settings: the unified per-account panel (team + roles + invitations + email
|
|
57
|
+
// sender + account-tier fragment library). The panel itself handles personal accounts
|
|
58
|
+
// (prompting to create an org for the team tab), so this is not gated.
|
|
58
59
|
{
|
|
59
60
|
label: 'Account settings…',
|
|
60
|
-
icon: 'i-lucide-
|
|
61
|
+
icon: 'i-lucide-settings',
|
|
61
62
|
onSelect: () => ui.openAccountSettings(),
|
|
62
63
|
},
|
|
63
64
|
// Admins can set the account-wide default provider new services inherit.
|
|
@@ -173,6 +173,15 @@ const commands = computed<Command[]>(() => {
|
|
|
173
173
|
keywords: 'fragment best practice guideline service default code-aware',
|
|
174
174
|
run: () => ui.openWorkspaceSettings('fragments'),
|
|
175
175
|
})
|
|
176
|
+
list.push({
|
|
177
|
+
id: 'account-settings',
|
|
178
|
+
label: 'Account settings',
|
|
179
|
+
group: 'Account',
|
|
180
|
+
icon: 'i-lucide-settings',
|
|
181
|
+
keywords:
|
|
182
|
+
'account team members roles invitations email api keys fragment best practice library context organization personal',
|
|
183
|
+
run: () => ui.openAccountSettings(),
|
|
184
|
+
})
|
|
176
185
|
list.push({
|
|
177
186
|
id: 'local-models',
|
|
178
187
|
label: 'My local runners',
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// Sections gate on the same `available` probes the navbar used, so a system that
|
|
9
9
|
// the backend has turned off simply doesn't appear here.
|
|
10
10
|
const ui = useUiStore()
|
|
11
|
+
const auth = useAuthStore()
|
|
11
12
|
const github = useGitHubStore()
|
|
12
13
|
const slack = useSlackStore()
|
|
13
14
|
const documents = useDocumentsStore()
|
|
@@ -256,6 +257,18 @@ const groups = computed<IntegrationGroup[]>(() => {
|
|
|
256
257
|
onClick: () => go(() => ui.openProviderConnection('runner-pool')),
|
|
257
258
|
})
|
|
258
259
|
}
|
|
260
|
+
// Local-mode-only: the warm-container pool + checkout reuse for the local runner. Shown
|
|
261
|
+
// only on the local-mode service (the controller 503s elsewhere, and `auth.localMode`
|
|
262
|
+
// is set from /auth/config).
|
|
263
|
+
if (auth.localMode?.enabled) {
|
|
264
|
+
infra.push({
|
|
265
|
+
key: 'local-mode',
|
|
266
|
+
icon: 'i-lucide-container',
|
|
267
|
+
label: 'Local mode',
|
|
268
|
+
description: 'Warm container pool + per-repo checkout reuse for the local runner.',
|
|
269
|
+
onClick: () => go(ui.openLocalModeSettings),
|
|
270
|
+
})
|
|
271
|
+
}
|
|
259
272
|
if (infra.length) out.push({ title: 'Infrastructure', items: infra })
|
|
260
273
|
|
|
261
274
|
return out
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// Account
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// Account settings — a single tabbed modal for the per-account configuration, distinct
|
|
3
|
+
// from Workspace settings. Hosts the team panel (members + roles, invitations, email
|
|
4
|
+
// sender, account-wide API keys; org-scoped, with a create-org CTA on a personal account)
|
|
5
|
+
// and the account-tier prompt-fragment library (available for every account type).
|
|
6
|
+
// Opened from the SideBar Configuration section, the account switcher and the command
|
|
7
|
+
// bar; bound to the `ui` store so any surface can open it and deep-link to a tab.
|
|
6
8
|
import AccountTeamSettings from '~/components/layout/AccountTeamSettings.vue'
|
|
9
|
+
import AccountFragmentSettings from '~/components/layout/AccountFragmentSettings.vue'
|
|
7
10
|
|
|
8
11
|
const ui = useUiStore()
|
|
9
12
|
const accounts = useAccountsStore()
|
|
@@ -12,13 +15,43 @@ const open = computed({
|
|
|
12
15
|
get: () => ui.accountSettingsOpen,
|
|
13
16
|
set: (v: boolean) => (v ? ui.openAccountSettings() : ui.closeAccountSettings()),
|
|
14
17
|
})
|
|
18
|
+
|
|
19
|
+
// Driven by the ui store so other surfaces (command bar, the workspace-settings
|
|
20
|
+
// cross-link) can deep-link straight to a tab.
|
|
21
|
+
const activeTab = computed({
|
|
22
|
+
get: () => ui.accountSettingsTab,
|
|
23
|
+
set: (v: string) => ui.setAccountSettingsTab(v),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const tabs = [
|
|
27
|
+
{ value: 'team', label: 'Team & access', icon: 'i-lucide-users', slot: 'team' },
|
|
28
|
+
{
|
|
29
|
+
value: 'fragments',
|
|
30
|
+
label: 'Context fragments',
|
|
31
|
+
icon: 'i-lucide-book-marked',
|
|
32
|
+
slot: 'fragments',
|
|
33
|
+
},
|
|
34
|
+
]
|
|
15
35
|
</script>
|
|
16
36
|
|
|
17
37
|
<template>
|
|
18
|
-
<UModal v-model:open="open" title="Account
|
|
38
|
+
<UModal v-model:open="open" title="Account settings" :ui="{ content: 'max-w-3xl' }">
|
|
19
39
|
<template #body>
|
|
20
|
-
<
|
|
21
|
-
<
|
|
40
|
+
<p v-if="!accounts.activeAccountId" class="text-sm text-slate-400">No account selected.</p>
|
|
41
|
+
<UTabs
|
|
42
|
+
v-else
|
|
43
|
+
v-model="activeTab"
|
|
44
|
+
:items="tabs"
|
|
45
|
+
variant="link"
|
|
46
|
+
:ui="{ root: 'gap-4', list: 'overflow-x-auto' }"
|
|
47
|
+
>
|
|
48
|
+
<template #team>
|
|
49
|
+
<AccountTeamSettings :account-id="accounts.activeAccountId" />
|
|
50
|
+
</template>
|
|
51
|
+
<template #fragments>
|
|
52
|
+
<AccountFragmentSettings :account-id="accounts.activeAccountId" />
|
|
53
|
+
</template>
|
|
54
|
+
</UTabs>
|
|
22
55
|
</template>
|
|
23
56
|
</UModal>
|
|
24
57
|
</template>
|