@cat-factory/app 0.6.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.
Files changed (189) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/app/app.config.ts +8 -0
  4. package/app/app.vue +11 -0
  5. package/app/assets/css/main.css +100 -0
  6. package/app/components/auth/AuthGate.vue +24 -0
  7. package/app/components/auth/LoginScreen.vue +143 -0
  8. package/app/components/auth/UserMenu.vue +39 -0
  9. package/app/components/board/AddTaskModal.vue +444 -0
  10. package/app/components/board/AgentFailureCard.vue +97 -0
  11. package/app/components/board/AgentStopButton.vue +61 -0
  12. package/app/components/board/BoardCanvas.vue +183 -0
  13. package/app/components/board/ContextPicker.vue +367 -0
  14. package/app/components/board/RecurringPipelineModal.vue +219 -0
  15. package/app/components/board/TaskDependencyEdges.vue +132 -0
  16. package/app/components/board/nodes/AgentChip.vue +59 -0
  17. package/app/components/board/nodes/BlockNode.vue +433 -0
  18. package/app/components/board/nodes/DecisionBadge.vue +27 -0
  19. package/app/components/board/nodes/DraggableTask.vue +48 -0
  20. package/app/components/board/nodes/ModuleFrame.vue +97 -0
  21. package/app/components/board/nodes/TaskCard.vue +359 -0
  22. package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
  23. package/app/components/bootstrap/BootstrapModal.vue +665 -0
  24. package/app/components/clarity/ClarityReviewWindow.vue +611 -0
  25. package/app/components/consensus/ConsensusSessionWindow.vue +210 -0
  26. package/app/components/documents/DocumentImportModal.vue +161 -0
  27. package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
  28. package/app/components/documents/SpawnPreviewModal.vue +161 -0
  29. package/app/components/documents/TaskContextDocs.vue +83 -0
  30. package/app/components/focus/BlockFocusView.vue +171 -0
  31. package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
  32. package/app/components/gates/GateResultView.vue +282 -0
  33. package/app/components/github/AddServiceFromRepoModal.vue +354 -0
  34. package/app/components/github/GitHubConnect.vue +183 -0
  35. package/app/components/github/GitHubOnboarding.vue +45 -0
  36. package/app/components/github/GitHubPanel.vue +584 -0
  37. package/app/components/github/RepoTreeBrowser.vue +171 -0
  38. package/app/components/layout/AccountTeamSettings.vue +237 -0
  39. package/app/components/layout/BoardSwitcher.vue +280 -0
  40. package/app/components/layout/BoardToolbar.vue +156 -0
  41. package/app/components/layout/CommandBar.vue +336 -0
  42. package/app/components/layout/GitHubPatBanner.vue +73 -0
  43. package/app/components/layout/NotificationsInbox.vue +175 -0
  44. package/app/components/layout/SideBar.vue +314 -0
  45. package/app/components/layout/SpendWarningBanner.vue +107 -0
  46. package/app/components/observability/StepMetricsBar.vue +102 -0
  47. package/app/components/palettes/AgentPalette.vue +86 -0
  48. package/app/components/panels/AgentStepDetail.vue +737 -0
  49. package/app/components/panels/DecisionModal.vue +71 -0
  50. package/app/components/panels/InspectorPanel.vue +465 -0
  51. package/app/components/panels/ObservabilityPanel.vue +351 -0
  52. package/app/components/panels/StepMetadataCard.vue +253 -0
  53. package/app/components/panels/StepRestartControl.vue +90 -0
  54. package/app/components/panels/StepResultViewHost.vue +40 -0
  55. package/app/components/panels/StepTestReport.vue +84 -0
  56. package/app/components/panels/inspector/ContainerSummary.vue +74 -0
  57. package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
  58. package/app/components/panels/inspector/ServiceFragments.vue +82 -0
  59. package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
  60. package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
  61. package/app/components/panels/inspector/TaskDependencies.vue +70 -0
  62. package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
  63. package/app/components/panels/inspector/TaskExecution.vue +364 -0
  64. package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
  65. package/app/components/panels/inspector/TaskStructure.vue +96 -0
  66. package/app/components/pipeline/AgentKindIcon.vue +30 -0
  67. package/app/components/pipeline/IterationCapPrompt.vue +70 -0
  68. package/app/components/pipeline/PipelineBuilder.vue +817 -0
  69. package/app/components/pipeline/PipelineProgress.vue +484 -0
  70. package/app/components/providers/ApiKeysSection.vue +273 -0
  71. package/app/components/providers/PersonalCredentialModal.vue +128 -0
  72. package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
  73. package/app/components/providers/VendorCredentialsModal.vue +197 -0
  74. package/app/components/recurring/RecurrenceEditor.vue +124 -0
  75. package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
  76. package/app/components/settings/DatadogPanel.vue +213 -0
  77. package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
  78. package/app/components/settings/MergeThresholdsPanel.vue +378 -0
  79. package/app/components/settings/ModelDefaultsPanel.vue +250 -0
  80. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
  81. package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
  82. package/app/components/slack/SlackPanel.vue +299 -0
  83. package/app/components/tasks/TaskContextIssues.vue +88 -0
  84. package/app/components/tasks/TaskImportModal.vue +207 -0
  85. package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
  86. package/app/components/testing/TestReportWindow.vue +404 -0
  87. package/app/composables/api/accounts.ts +81 -0
  88. package/app/composables/api/auth.ts +45 -0
  89. package/app/composables/api/board.ts +101 -0
  90. package/app/composables/api/bootstrap.ts +62 -0
  91. package/app/composables/api/context.ts +25 -0
  92. package/app/composables/api/documents.ts +74 -0
  93. package/app/composables/api/execution.ts +127 -0
  94. package/app/composables/api/fragments.ts +71 -0
  95. package/app/composables/api/github.ts +131 -0
  96. package/app/composables/api/models.ts +127 -0
  97. package/app/composables/api/notifications.ts +23 -0
  98. package/app/composables/api/presets.ts +29 -0
  99. package/app/composables/api/recurring.ts +68 -0
  100. package/app/composables/api/releaseHealth.ts +43 -0
  101. package/app/composables/api/reviews.ts +146 -0
  102. package/app/composables/api/slack.ts +54 -0
  103. package/app/composables/api/tasks.ts +72 -0
  104. package/app/composables/api/workspaces.ts +36 -0
  105. package/app/composables/useApi.ts +89 -0
  106. package/app/composables/useBlockDrag.ts +90 -0
  107. package/app/composables/useBlockQueries.ts +154 -0
  108. package/app/composables/useBoardFlow.ts +11 -0
  109. package/app/composables/useContextLinking.ts +65 -0
  110. package/app/composables/useDepLabels.ts +26 -0
  111. package/app/composables/useFrameResize.ts +54 -0
  112. package/app/composables/useResultView.ts +48 -0
  113. package/app/composables/useReviewStage.ts +40 -0
  114. package/app/composables/useSemanticZoom.ts +31 -0
  115. package/app/composables/useStepApproval.ts +233 -0
  116. package/app/composables/useStepProse.ts +78 -0
  117. package/app/composables/useStepTimer.ts +63 -0
  118. package/app/composables/useTaskExpansion.ts +92 -0
  119. package/app/composables/useWorkspaceStream.ts +155 -0
  120. package/app/docs/architecture.md +31 -0
  121. package/app/pages/index.vue +141 -0
  122. package/app/stores/accounts.ts +152 -0
  123. package/app/stores/agentConfig.ts +35 -0
  124. package/app/stores/agentRuns.ts +122 -0
  125. package/app/stores/agents.ts +40 -0
  126. package/app/stores/apiKeys.ts +108 -0
  127. package/app/stores/auth.ts +166 -0
  128. package/app/stores/board.spec.ts +205 -0
  129. package/app/stores/board.ts +286 -0
  130. package/app/stores/bootstrap.ts +97 -0
  131. package/app/stores/clarity.ts +196 -0
  132. package/app/stores/consensus.ts +60 -0
  133. package/app/stores/documents.ts +176 -0
  134. package/app/stores/execution.ts +273 -0
  135. package/app/stores/fragmentLibrary.ts +147 -0
  136. package/app/stores/fragments.ts +40 -0
  137. package/app/stores/github.ts +305 -0
  138. package/app/stores/localModels.ts +51 -0
  139. package/app/stores/mergePresets.ts +58 -0
  140. package/app/stores/modelDefaults.ts +76 -0
  141. package/app/stores/models.ts +134 -0
  142. package/app/stores/notifications.ts +70 -0
  143. package/app/stores/observability.ts +144 -0
  144. package/app/stores/personalSubscriptions.ts +215 -0
  145. package/app/stores/pipelines.ts +327 -0
  146. package/app/stores/recurringPipelines.ts +112 -0
  147. package/app/stores/releaseHealth.ts +75 -0
  148. package/app/stores/requirements.spec.ts +94 -0
  149. package/app/stores/requirements.ts +208 -0
  150. package/app/stores/serviceFragmentDefaults.ts +29 -0
  151. package/app/stores/services.ts +87 -0
  152. package/app/stores/slack.ts +142 -0
  153. package/app/stores/taskExpansion.ts +36 -0
  154. package/app/stores/tasks.spec.ts +71 -0
  155. package/app/stores/tasks.ts +176 -0
  156. package/app/stores/tracker.ts +27 -0
  157. package/app/stores/ui.ts +434 -0
  158. package/app/stores/vendorCredentials.ts +54 -0
  159. package/app/stores/workspace.ts +215 -0
  160. package/app/stores/workspaceSettings.ts +36 -0
  161. package/app/types/accounts.ts +77 -0
  162. package/app/types/bootstrap.ts +83 -0
  163. package/app/types/clarity.ts +59 -0
  164. package/app/types/consensus.ts +91 -0
  165. package/app/types/documents.ts +104 -0
  166. package/app/types/domain.ts +495 -0
  167. package/app/types/execution.ts +383 -0
  168. package/app/types/fragments.ts +72 -0
  169. package/app/types/github.ts +173 -0
  170. package/app/types/localModels.ts +73 -0
  171. package/app/types/merge.ts +71 -0
  172. package/app/types/models.ts +157 -0
  173. package/app/types/notifications.ts +74 -0
  174. package/app/types/recurring.ts +69 -0
  175. package/app/types/releaseHealth.ts +31 -0
  176. package/app/types/requirements.ts +61 -0
  177. package/app/types/services.ts +27 -0
  178. package/app/types/slack.ts +57 -0
  179. package/app/types/tasks.ts +82 -0
  180. package/app/types/tracker.ts +18 -0
  181. package/app/utils/agentOutput.spec.ts +128 -0
  182. package/app/utils/agentOutput.ts +173 -0
  183. package/app/utils/catalog.spec.ts +112 -0
  184. package/app/utils/catalog.ts +455 -0
  185. package/app/utils/dnd.ts +29 -0
  186. package/app/utils/observability.ts +52 -0
  187. package/app/utils/pipelineRender.ts +151 -0
  188. package/nuxt.config.ts +55 -0
  189. package/package.json +45 -0
@@ -0,0 +1,83 @@
1
+ <script setup lang="ts">
2
+ import type { DropdownMenuItem } from '@nuxt/ui'
3
+ import type { Block, DocumentSourceKind } from '~/types/domain'
4
+
5
+ // Documents (from any source) attached to a task as agent context, shown inside
6
+ // the InspectorPanel. Linked docs are fed to agents during execution (see the
7
+ // backend's userPromptFor). Rendered only when the integration is available.
8
+ const props = defineProps<{ block: Block }>()
9
+
10
+ const documents = useDocumentsStore()
11
+ const ui = useUiStore()
12
+ const toast = useToast()
13
+
14
+ onMounted(() => {
15
+ documents.loadDocuments().catch(() => {})
16
+ })
17
+
18
+ const linked = computed(() => documents.docsForBlock(props.block.id))
19
+
20
+ async function attach(source: DocumentSourceKind, externalId: string) {
21
+ try {
22
+ await documents.linkToBlock(props.block.id, source, externalId)
23
+ toast.add({ title: 'Document attached', icon: 'i-lucide-link' })
24
+ } catch (e) {
25
+ toast.add({
26
+ title: 'Could not attach',
27
+ description: e instanceof Error ? e.message : String(e),
28
+ icon: 'i-lucide-triangle-alert',
29
+ color: 'error',
30
+ })
31
+ }
32
+ }
33
+
34
+ const attachMenu = computed<DropdownMenuItem[][]>(() => {
35
+ const linkedKeys = new Set(linked.value.map((d) => `${d.source}:${d.externalId}`))
36
+ const items: DropdownMenuItem[] = documents.documents
37
+ .filter((d) => !linkedKeys.has(`${d.source}:${d.externalId}`))
38
+ .map((d) => ({
39
+ label: d.title,
40
+ icon: documents.descriptorFor(d.source)?.icon ?? 'i-lucide-file-text',
41
+ onSelect: () => attach(d.source, d.externalId),
42
+ }))
43
+ items.push({
44
+ label: 'Import a page…',
45
+ icon: 'i-lucide-file-down',
46
+ onSelect: () => ui.openDocumentImport(null),
47
+ })
48
+ return [items]
49
+ })
50
+ </script>
51
+
52
+ <template>
53
+ <div v-if="documents.available" class="space-y-2">
54
+ <div class="flex items-center justify-between">
55
+ <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
56
+ Context documents
57
+ </span>
58
+ <UDropdownMenu :items="attachMenu" :content="{ side: 'bottom', align: 'end' }">
59
+ <UButton color="neutral" variant="soft" size="xs" icon="i-lucide-plus">Attach</UButton>
60
+ </UDropdownMenu>
61
+ </div>
62
+
63
+ <div v-if="linked.length" class="space-y-1">
64
+ <a
65
+ v-for="doc in linked"
66
+ :key="`${doc.source}:${doc.externalId}`"
67
+ :href="doc.url"
68
+ target="_blank"
69
+ rel="noopener"
70
+ class="flex items-center gap-1.5 rounded-md border border-slate-800 bg-slate-900/60 px-2 py-1.5 text-xs text-slate-300 hover:bg-slate-800/60"
71
+ >
72
+ <UIcon
73
+ :name="documents.descriptorFor(doc.source)?.icon ?? 'i-lucide-file-text'"
74
+ class="h-3.5 w-3.5 shrink-0 text-indigo-400"
75
+ />
76
+ <span class="truncate">{{ doc.title }}</span>
77
+ </a>
78
+ </div>
79
+ <p v-else class="text-[11px] text-slate-500">
80
+ Attach a requirement, RFC or PRD so agents see it while implementing this task.
81
+ </p>
82
+ </div>
83
+ </template>
@@ -0,0 +1,171 @@
1
+ <script setup lang="ts">
2
+ import { onKeyStroke } from '@vueuse/core'
3
+ import type { Block } from '~/types/domain'
4
+ import { BLOCK_TYPE_META, STATUS_META } from '~/utils/catalog'
5
+ import PipelineProgress from '~/components/pipeline/PipelineProgress.vue'
6
+
7
+ const board = useBoardStore()
8
+ const pipelines = usePipelinesStore()
9
+ const execution = useExecutionStore()
10
+ const ui = useUiStore()
11
+ const models = useModelsStore()
12
+ const workspace = useWorkspaceStore()
13
+
14
+ onMounted(() => models.ensureLoaded(workspace.workspaceId ?? undefined))
15
+
16
+ const block = computed<Block | undefined>(() =>
17
+ ui.focusBlockId ? board.getBlock(ui.focusBlockId) : undefined,
18
+ )
19
+ const instance = computed(() => execution.getInstance(block.value?.executionId))
20
+ const statusMeta = computed(() => (block.value ? STATUS_META[block.value.status] : null))
21
+ const typeMeta = computed(() => (block.value ? BLOCK_TYPE_META[block.value.type] : null))
22
+
23
+ const deps = computed(() =>
24
+ (block.value?.dependsOn ?? []).map((id) => board.getBlock(id)).filter((b): b is Block => !!b),
25
+ )
26
+
27
+ const runMenu = computed(() =>
28
+ pipelines.pipelines.map((p) => ({
29
+ label: p.name,
30
+ icon: 'i-lucide-play',
31
+ onSelect: () => block.value && execution.start(block.value.id, p),
32
+ })),
33
+ )
34
+
35
+ function close() {
36
+ ui.focus(null)
37
+ }
38
+
39
+ onKeyStroke('Escape', () => {
40
+ if (ui.focusBlockId) close()
41
+ })
42
+
43
+ function openDecisionFor(decisionId: string) {
44
+ if (instance.value) ui.openDecision(instance.value.id, decisionId)
45
+ }
46
+
47
+ function openApprovalFor(approvalId: string) {
48
+ if (instance.value) ui.openApprovalDetail(instance.value.id, approvalId)
49
+ }
50
+ </script>
51
+
52
+ <template>
53
+ <Transition name="focus-fade">
54
+ <div
55
+ v-if="block && statusMeta && typeMeta"
56
+ class="absolute inset-0 z-30 flex flex-col bg-slate-950/95 backdrop-blur"
57
+ >
58
+ <!-- header / breadcrumb -->
59
+ <header class="flex items-center gap-3 border-b border-slate-800 px-6 py-4">
60
+ <UButton
61
+ icon="i-lucide-arrow-left"
62
+ color="neutral"
63
+ variant="ghost"
64
+ size="sm"
65
+ @click="close"
66
+ >
67
+ Board
68
+ </UButton>
69
+ <UIcon name="i-lucide-chevron-right" class="h-4 w-4 text-slate-600" />
70
+ <div
71
+ class="flex h-9 w-9 items-center justify-center rounded-lg"
72
+ :style="{ backgroundColor: typeMeta.accent + '22' }"
73
+ >
74
+ <UIcon :name="typeMeta.icon" class="h-5 w-5" :style="{ color: typeMeta.accent }" />
75
+ </div>
76
+ <div>
77
+ <h1 class="text-lg font-semibold text-white">{{ block.title }}</h1>
78
+ <div class="text-xs text-slate-500">{{ typeMeta.label }} · focus view</div>
79
+ </div>
80
+ <UBadge :color="statusMeta.chip as any" variant="subtle" class="ml-2">
81
+ {{ statusMeta.label }}
82
+ </UBadge>
83
+ <div class="ml-auto flex items-center gap-2">
84
+ <UDropdownMenu :items="runMenu">
85
+ <UButton
86
+ color="primary"
87
+ variant="soft"
88
+ size="sm"
89
+ icon="i-lucide-play"
90
+ trailing-icon="i-lucide-chevron-down"
91
+ >
92
+ {{ instance ? 'Re-run pipeline' : 'Run pipeline' }}
93
+ </UButton>
94
+ </UDropdownMenu>
95
+ <UButton icon="i-lucide-x" color="neutral" variant="ghost" @click="close" />
96
+ </div>
97
+ </header>
98
+
99
+ <div class="grid flex-1 grid-cols-[1fr_300px] gap-6 overflow-hidden p-6">
100
+ <!-- main: pipeline flow -->
101
+ <section
102
+ class="flex flex-col overflow-auto rounded-2xl border border-slate-800 bg-slate-900/60 p-6"
103
+ >
104
+ <div class="mb-4 flex items-center gap-2">
105
+ <UIcon name="i-lucide-workflow" class="h-4 w-4 text-slate-500" />
106
+ <h2 class="text-sm font-semibold uppercase tracking-wide text-slate-400">
107
+ {{ instance ? instance.pipelineName : 'No pipeline running' }}
108
+ </h2>
109
+ </div>
110
+
111
+ <PipelineProgress
112
+ v-if="instance"
113
+ :instance="instance"
114
+ @open-decision="openDecisionFor"
115
+ @open-approval="openApprovalFor"
116
+ />
117
+
118
+ <div
119
+ v-else
120
+ class="flex flex-1 items-center justify-center rounded-xl border border-dashed border-slate-700 text-sm text-slate-500"
121
+ >
122
+ Run a pipeline to visualize the agents working on this block.
123
+ </div>
124
+ </section>
125
+
126
+ <!-- side: details -->
127
+ <aside
128
+ class="space-y-4 overflow-auto rounded-2xl border border-slate-800 bg-slate-900/60 p-5"
129
+ >
130
+ <div>
131
+ <div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
132
+ Description
133
+ </div>
134
+ <p class="text-sm text-slate-300">{{ block.description }}</p>
135
+ </div>
136
+ <div v-if="instance">
137
+ <div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
138
+ Overall progress
139
+ </div>
140
+ <UProgress :model-value="Math.round(block.progress * 100)" />
141
+ <div class="mt-1 text-[11px] text-slate-400">
142
+ {{ Math.round(block.progress * 100) }}%
143
+ </div>
144
+ </div>
145
+ <div>
146
+ <div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
147
+ Dependencies
148
+ </div>
149
+ <div v-if="deps.length" class="flex flex-wrap gap-1">
150
+ <UBadge v-for="d in deps" :key="d.id" color="neutral" variant="subtle" size="sm">
151
+ {{ d.title }}
152
+ </UBadge>
153
+ </div>
154
+ <div v-else class="text-[11px] text-slate-500">None</div>
155
+ </div>
156
+ </aside>
157
+ </div>
158
+ </div>
159
+ </Transition>
160
+ </template>
161
+
162
+ <style scoped>
163
+ .focus-fade-enter-active,
164
+ .focus-fade-leave-active {
165
+ transition: opacity 0.18s ease;
166
+ }
167
+ .focus-fade-enter-from,
168
+ .focus-fade-leave-to {
169
+ opacity: 0;
170
+ }
171
+ </style>
@@ -0,0 +1,340 @@
1
+ <script setup lang="ts">
2
+ // Prompt-fragment library manager (ADR 0006): curate this board's best-practice
3
+ // fragments, link repos of Markdown guidelines (with a "changes available" badge
4
+ // + resync), and review the merged catalog (built-in ∪ account ∪ workspace) an
5
+ // agent is selected from per run. Workspace-tier focused; the resolved view shows
6
+ // every tier so the inheritance is visible.
7
+ import type { ResolvedFragment } from '~/types/domain'
8
+
9
+ const ui = useUiStore()
10
+ const library = useFragmentLibraryStore()
11
+ const toast = useToast()
12
+
13
+ const open = computed({
14
+ get: () => ui.fragmentLibraryOpen,
15
+ set: (v: boolean) => {
16
+ if (!v) ui.closeFragmentLibrary()
17
+ },
18
+ })
19
+
20
+ watch(open, (isOpen) => {
21
+ if (isOpen) void library.probe()
22
+ })
23
+
24
+ type Tab = 'catalog' | 'authored' | 'sources'
25
+ const tab = ref<Tab>('catalog')
26
+
27
+ const tierLabel: Record<ResolvedFragment['tier'], string> = {
28
+ builtin: 'Built-in',
29
+ account: 'Account',
30
+ workspace: 'This board',
31
+ }
32
+ // `as const` keeps the literal color names (assignable to UBadge's `color`
33
+ // union) instead of widening to `string`; `satisfies` still checks the shape.
34
+ const tierColor = {
35
+ builtin: 'neutral',
36
+ account: 'info',
37
+ workspace: 'primary',
38
+ } as const satisfies Record<ResolvedFragment['tier'], string>
39
+
40
+ function notifyError(title: string, e: unknown) {
41
+ toast.add({
42
+ title,
43
+ description: e instanceof Error ? e.message : String(e),
44
+ icon: 'i-lucide-triangle-alert',
45
+ color: 'error',
46
+ })
47
+ }
48
+
49
+ // ---- create a hand-authored fragment --------------------------------------
50
+ const draft = ref({ title: '', summary: '', body: '', tags: '' })
51
+ const draftValid = computed(
52
+ () => draft.value.title.trim() && draft.value.summary.trim() && draft.value.body.trim(),
53
+ )
54
+
55
+ async function createFragment() {
56
+ if (!draftValid.value) return
57
+ try {
58
+ await library.create({
59
+ title: draft.value.title.trim(),
60
+ summary: draft.value.summary.trim(),
61
+ body: draft.value.body.trim(),
62
+ tags: draft.value.tags
63
+ .split(',')
64
+ .map((t) => t.trim())
65
+ .filter(Boolean),
66
+ })
67
+ draft.value = { title: '', summary: '', body: '', tags: '' }
68
+ toast.add({ title: 'Fragment added', icon: 'i-lucide-check' })
69
+ } catch (e) {
70
+ notifyError('Could not add fragment', e)
71
+ }
72
+ }
73
+
74
+ async function removeFragment(id: string) {
75
+ try {
76
+ await library.remove(id)
77
+ toast.add({ title: 'Fragment removed', icon: 'i-lucide-trash-2' })
78
+ } catch (e) {
79
+ notifyError('Could not remove fragment', e)
80
+ }
81
+ }
82
+
83
+ // ---- repo sources ----------------------------------------------------------
84
+ const sourceDraft = ref({ repoOwner: '', repoName: '', dirPath: '', gitRef: '' })
85
+ const sourceValid = computed(
86
+ () => sourceDraft.value.repoOwner.trim() && sourceDraft.value.repoName.trim(),
87
+ )
88
+
89
+ async function linkSource() {
90
+ if (!sourceValid.value) return
91
+ try {
92
+ const source = await library.linkSource({
93
+ repoOwner: sourceDraft.value.repoOwner.trim(),
94
+ repoName: sourceDraft.value.repoName.trim(),
95
+ dirPath: sourceDraft.value.dirPath.trim() || undefined,
96
+ gitRef: sourceDraft.value.gitRef.trim() || undefined,
97
+ })
98
+ sourceDraft.value = { repoOwner: '', repoName: '', dirPath: '', gitRef: '' }
99
+ await library.syncSource(source.id)
100
+ toast.add({ title: 'Source linked & synced', icon: 'i-lucide-git-branch' })
101
+ } catch (e) {
102
+ notifyError('Could not link source', e)
103
+ }
104
+ }
105
+
106
+ async function syncSource(id: string) {
107
+ try {
108
+ const result = await library.syncSource(id)
109
+ toast.add({
110
+ title: `Synced: ${result.upserted} updated, ${result.tombstoned} removed`,
111
+ icon: 'i-lucide-refresh-cw',
112
+ color: 'info',
113
+ })
114
+ } catch (e) {
115
+ notifyError('Could not sync source', e)
116
+ }
117
+ }
118
+
119
+ async function checkSource(id: string) {
120
+ try {
121
+ const status = await library.checkSource(id)
122
+ toast.add({
123
+ title: status.changed ? `${status.changedCount} change(s) available` : 'Up to date',
124
+ icon: status.changed ? 'i-lucide-bell-dot' : 'i-lucide-check',
125
+ })
126
+ } catch (e) {
127
+ notifyError('Could not check source', e)
128
+ }
129
+ }
130
+
131
+ async function unlinkSource(id: string) {
132
+ try {
133
+ await library.unlinkSource(id)
134
+ toast.add({ title: 'Source unlinked', icon: 'i-lucide-unplug' })
135
+ } catch (e) {
136
+ notifyError('Could not unlink source', e)
137
+ }
138
+ }
139
+ </script>
140
+
141
+ <template>
142
+ <UModal v-model:open="open" title="Prompt-fragment library" :ui="{ content: 'max-w-3xl' }">
143
+ <template #body>
144
+ <div class="flex flex-col gap-4">
145
+ <p class="text-sm text-slate-400">
146
+ Curate the best-practice guidelines agents follow on this board. Fragments are merged from
147
+ the built-in catalog, your account, and this board — later tiers override earlier ones —
148
+ then the relevant ones are selected for each agent run.
149
+ </p>
150
+
151
+ <div class="flex gap-2">
152
+ <UButton
153
+ v-for="t in ['catalog', 'authored', 'sources'] as Tab[]"
154
+ :key="t"
155
+ :color="tab === t ? 'primary' : 'neutral'"
156
+ :variant="tab === t ? 'solid' : 'ghost'"
157
+ size="sm"
158
+ @click="tab = t"
159
+ >
160
+ {{
161
+ t === 'catalog'
162
+ ? 'Resolved catalog'
163
+ : t === 'authored'
164
+ ? 'This board'
165
+ : 'Repo sources'
166
+ }}
167
+ </UButton>
168
+ </div>
169
+
170
+ <!-- Resolved (merged) catalog -->
171
+ <div v-if="tab === 'catalog'" class="flex flex-col gap-2">
172
+ <p class="text-xs text-slate-500">
173
+ {{ library.resolved.length }} fragment(s) resolved ·
174
+ {{ library.builtinCount }} built-in.
175
+ </p>
176
+ <div
177
+ v-for="f in library.resolved"
178
+ :key="f.id"
179
+ class="rounded-md border border-slate-800 bg-slate-900/60 p-3"
180
+ >
181
+ <div class="flex items-center gap-2">
182
+ <span class="font-medium text-slate-100">{{ f.title }}</span>
183
+ <UBadge size="xs" :color="tierColor[f.tier]" variant="subtle">
184
+ {{ tierLabel[f.tier] }}
185
+ </UBadge>
186
+ <span class="ml-auto font-mono text-[11px] text-slate-500">{{ f.id }}</span>
187
+ </div>
188
+ <p class="mt-1 text-sm text-slate-400">{{ f.summary }}</p>
189
+ <div v-if="f.tags?.length" class="mt-1 flex flex-wrap gap-1">
190
+ <UBadge v-for="tag in f.tags" :key="tag" size="xs" variant="outline" color="neutral">
191
+ {{ tag }}
192
+ </UBadge>
193
+ </div>
194
+ </div>
195
+ </div>
196
+
197
+ <!-- Hand-authored (workspace tier) -->
198
+ <div v-else-if="tab === 'authored'" class="flex flex-col gap-3">
199
+ <div
200
+ v-for="f in library.fragments"
201
+ :key="f.id"
202
+ class="flex items-start gap-2 rounded-md border border-slate-800 bg-slate-900/60 p-3"
203
+ >
204
+ <div class="min-w-0">
205
+ <div class="flex items-center gap-2">
206
+ <span class="font-medium text-slate-100">{{ f.title }}</span>
207
+ <UBadge v-if="f.source" size="xs" color="info" variant="subtle">from repo</UBadge>
208
+ </div>
209
+ <p class="text-sm text-slate-400">{{ f.summary }}</p>
210
+ </div>
211
+ <UButton
212
+ icon="i-lucide-trash-2"
213
+ size="xs"
214
+ color="error"
215
+ variant="ghost"
216
+ class="ml-auto"
217
+ @click="removeFragment(f.id)"
218
+ />
219
+ </div>
220
+ <p v-if="!library.fragments.length" class="text-sm text-slate-500">
221
+ No board-specific fragments yet. Add one below, or override a built-in by using its id.
222
+ </p>
223
+
224
+ <div class="rounded-md border border-slate-800 p-3">
225
+ <p class="mb-2 text-sm font-medium">Add a fragment</p>
226
+ <div class="flex flex-col gap-2">
227
+ <UInput v-model="draft.title" placeholder="Title" />
228
+ <UInput
229
+ v-model="draft.summary"
230
+ placeholder="One-line summary (used by the selector)"
231
+ />
232
+ <UTextarea
233
+ v-model="draft.body"
234
+ placeholder="Guidance body (injected into the prompt)"
235
+ :rows="4"
236
+ />
237
+ <UInput v-model="draft.tags" placeholder="Tags, comma-separated (e.g. backend, db)" />
238
+ <UButton
239
+ icon="i-lucide-plus"
240
+ size="sm"
241
+ :disabled="!draftValid"
242
+ :loading="library.loading"
243
+ class="self-start"
244
+ @click="createFragment"
245
+ >
246
+ Add fragment
247
+ </UButton>
248
+ </div>
249
+ </div>
250
+ </div>
251
+
252
+ <!-- Repo sources -->
253
+ <div v-else class="flex flex-col gap-3">
254
+ <div
255
+ v-for="s in library.sources"
256
+ :key="s.id"
257
+ class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-900/60 p-3"
258
+ >
259
+ <UIcon name="i-lucide-git-branch" class="h-4 w-4 text-slate-400" />
260
+ <div class="min-w-0">
261
+ <span class="font-mono text-sm text-slate-100">
262
+ {{ s.repoOwner }}/{{ s.repoName
263
+ }}<span class="text-slate-500">/{{ s.dirPath || '' }}</span>
264
+ </span>
265
+ <p class="text-xs text-slate-500">
266
+ {{ s.lastSyncedAt ? 'synced' : 'never synced' }} · ref {{ s.gitRef }}
267
+ </p>
268
+ </div>
269
+ <UBadge
270
+ v-if="library.sourceChanges[s.id]"
271
+ size="xs"
272
+ color="warning"
273
+ variant="subtle"
274
+ class="ml-auto"
275
+ >
276
+ {{ library.sourceChanges[s.id] }} change(s)
277
+ </UBadge>
278
+ <div class="ml-auto flex gap-1">
279
+ <UButton
280
+ icon="i-lucide-search-check"
281
+ size="xs"
282
+ variant="ghost"
283
+ @click="checkSource(s.id)"
284
+ />
285
+ <UButton
286
+ icon="i-lucide-refresh-cw"
287
+ size="xs"
288
+ variant="ghost"
289
+ :loading="library.loading"
290
+ @click="syncSource(s.id)"
291
+ />
292
+ <UButton
293
+ icon="i-lucide-unplug"
294
+ size="xs"
295
+ color="error"
296
+ variant="ghost"
297
+ @click="unlinkSource(s.id)"
298
+ />
299
+ </div>
300
+ </div>
301
+ <p v-if="!library.sources.length" class="text-sm text-slate-500">
302
+ No linked guideline repos. Link one below to import its Markdown files as fragments.
303
+ </p>
304
+
305
+ <div class="rounded-md border border-slate-800 p-3">
306
+ <p class="mb-2 text-sm font-medium">Link a guideline repo</p>
307
+ <div class="flex flex-col gap-2">
308
+ <div class="flex gap-2">
309
+ <UInput v-model="sourceDraft.repoOwner" placeholder="owner" class="flex-1" />
310
+ <UInput v-model="sourceDraft.repoName" placeholder="repo" class="flex-1" />
311
+ </div>
312
+ <div class="flex gap-2">
313
+ <UInput
314
+ v-model="sourceDraft.dirPath"
315
+ placeholder="dir path (e.g. guidelines)"
316
+ class="flex-1"
317
+ />
318
+ <UInput
319
+ v-model="sourceDraft.gitRef"
320
+ placeholder="ref (default HEAD)"
321
+ class="flex-1"
322
+ />
323
+ </div>
324
+ <UButton
325
+ icon="i-lucide-link"
326
+ size="sm"
327
+ :disabled="!sourceValid"
328
+ :loading="library.loading"
329
+ class="self-start"
330
+ @click="linkSource"
331
+ >
332
+ Link & sync
333
+ </UButton>
334
+ </div>
335
+ </div>
336
+ </div>
337
+ </div>
338
+ </template>
339
+ </UModal>
340
+ </template>