@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.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/app/app.config.ts +8 -0
- package/app/app.vue +11 -0
- package/app/assets/css/main.css +100 -0
- package/app/components/auth/AuthGate.vue +24 -0
- package/app/components/auth/LoginScreen.vue +143 -0
- package/app/components/auth/UserMenu.vue +39 -0
- package/app/components/board/AddTaskModal.vue +444 -0
- package/app/components/board/AgentFailureCard.vue +97 -0
- package/app/components/board/AgentStopButton.vue +61 -0
- package/app/components/board/BoardCanvas.vue +183 -0
- package/app/components/board/ContextPicker.vue +367 -0
- package/app/components/board/RecurringPipelineModal.vue +219 -0
- package/app/components/board/TaskDependencyEdges.vue +132 -0
- package/app/components/board/nodes/AgentChip.vue +59 -0
- package/app/components/board/nodes/BlockNode.vue +433 -0
- package/app/components/board/nodes/DecisionBadge.vue +27 -0
- package/app/components/board/nodes/DraggableTask.vue +48 -0
- package/app/components/board/nodes/ModuleFrame.vue +97 -0
- package/app/components/board/nodes/TaskCard.vue +359 -0
- package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
- package/app/components/bootstrap/BootstrapModal.vue +665 -0
- package/app/components/clarity/ClarityReviewWindow.vue +611 -0
- package/app/components/consensus/ConsensusSessionWindow.vue +210 -0
- package/app/components/documents/DocumentImportModal.vue +161 -0
- package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
- package/app/components/documents/SpawnPreviewModal.vue +161 -0
- package/app/components/documents/TaskContextDocs.vue +83 -0
- package/app/components/focus/BlockFocusView.vue +171 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
- package/app/components/gates/GateResultView.vue +282 -0
- package/app/components/github/AddServiceFromRepoModal.vue +354 -0
- package/app/components/github/GitHubConnect.vue +183 -0
- package/app/components/github/GitHubOnboarding.vue +45 -0
- package/app/components/github/GitHubPanel.vue +584 -0
- package/app/components/github/RepoTreeBrowser.vue +171 -0
- package/app/components/layout/AccountTeamSettings.vue +237 -0
- package/app/components/layout/BoardSwitcher.vue +280 -0
- package/app/components/layout/BoardToolbar.vue +156 -0
- package/app/components/layout/CommandBar.vue +336 -0
- package/app/components/layout/GitHubPatBanner.vue +73 -0
- package/app/components/layout/NotificationsInbox.vue +175 -0
- package/app/components/layout/SideBar.vue +314 -0
- package/app/components/layout/SpendWarningBanner.vue +107 -0
- package/app/components/observability/StepMetricsBar.vue +102 -0
- package/app/components/palettes/AgentPalette.vue +86 -0
- package/app/components/panels/AgentStepDetail.vue +737 -0
- package/app/components/panels/DecisionModal.vue +71 -0
- package/app/components/panels/InspectorPanel.vue +465 -0
- package/app/components/panels/ObservabilityPanel.vue +351 -0
- package/app/components/panels/StepMetadataCard.vue +253 -0
- package/app/components/panels/StepRestartControl.vue +90 -0
- package/app/components/panels/StepResultViewHost.vue +40 -0
- package/app/components/panels/StepTestReport.vue +84 -0
- package/app/components/panels/inspector/ContainerSummary.vue +74 -0
- package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
- package/app/components/panels/inspector/ServiceFragments.vue +82 -0
- package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
- package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
- package/app/components/panels/inspector/TaskDependencies.vue +70 -0
- package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
- package/app/components/panels/inspector/TaskExecution.vue +364 -0
- package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
- package/app/components/panels/inspector/TaskStructure.vue +96 -0
- package/app/components/pipeline/AgentKindIcon.vue +30 -0
- package/app/components/pipeline/IterationCapPrompt.vue +70 -0
- package/app/components/pipeline/PipelineBuilder.vue +817 -0
- package/app/components/pipeline/PipelineProgress.vue +484 -0
- package/app/components/providers/ApiKeysSection.vue +273 -0
- package/app/components/providers/PersonalCredentialModal.vue +128 -0
- package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
- package/app/components/providers/VendorCredentialsModal.vue +197 -0
- package/app/components/recurring/RecurrenceEditor.vue +124 -0
- package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
- package/app/components/settings/DatadogPanel.vue +213 -0
- package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
- package/app/components/settings/MergeThresholdsPanel.vue +378 -0
- package/app/components/settings/ModelDefaultsPanel.vue +250 -0
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
- package/app/components/slack/SlackPanel.vue +299 -0
- package/app/components/tasks/TaskContextIssues.vue +88 -0
- package/app/components/tasks/TaskImportModal.vue +207 -0
- package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
- package/app/components/testing/TestReportWindow.vue +404 -0
- package/app/composables/api/accounts.ts +81 -0
- package/app/composables/api/auth.ts +45 -0
- package/app/composables/api/board.ts +101 -0
- package/app/composables/api/bootstrap.ts +62 -0
- package/app/composables/api/context.ts +25 -0
- package/app/composables/api/documents.ts +74 -0
- package/app/composables/api/execution.ts +127 -0
- package/app/composables/api/fragments.ts +71 -0
- package/app/composables/api/github.ts +131 -0
- package/app/composables/api/models.ts +127 -0
- package/app/composables/api/notifications.ts +23 -0
- package/app/composables/api/presets.ts +29 -0
- package/app/composables/api/recurring.ts +68 -0
- package/app/composables/api/releaseHealth.ts +43 -0
- package/app/composables/api/reviews.ts +146 -0
- package/app/composables/api/slack.ts +54 -0
- package/app/composables/api/tasks.ts +72 -0
- package/app/composables/api/workspaces.ts +36 -0
- package/app/composables/useApi.ts +89 -0
- package/app/composables/useBlockDrag.ts +90 -0
- package/app/composables/useBlockQueries.ts +154 -0
- package/app/composables/useBoardFlow.ts +11 -0
- package/app/composables/useContextLinking.ts +65 -0
- package/app/composables/useDepLabels.ts +26 -0
- package/app/composables/useFrameResize.ts +54 -0
- package/app/composables/useResultView.ts +48 -0
- package/app/composables/useReviewStage.ts +40 -0
- package/app/composables/useSemanticZoom.ts +31 -0
- package/app/composables/useStepApproval.ts +233 -0
- package/app/composables/useStepProse.ts +78 -0
- package/app/composables/useStepTimer.ts +63 -0
- package/app/composables/useTaskExpansion.ts +92 -0
- package/app/composables/useWorkspaceStream.ts +155 -0
- package/app/docs/architecture.md +31 -0
- package/app/pages/index.vue +141 -0
- package/app/stores/accounts.ts +152 -0
- package/app/stores/agentConfig.ts +35 -0
- package/app/stores/agentRuns.ts +122 -0
- package/app/stores/agents.ts +40 -0
- package/app/stores/apiKeys.ts +108 -0
- package/app/stores/auth.ts +166 -0
- package/app/stores/board.spec.ts +205 -0
- package/app/stores/board.ts +286 -0
- package/app/stores/bootstrap.ts +97 -0
- package/app/stores/clarity.ts +196 -0
- package/app/stores/consensus.ts +60 -0
- package/app/stores/documents.ts +176 -0
- package/app/stores/execution.ts +273 -0
- package/app/stores/fragmentLibrary.ts +147 -0
- package/app/stores/fragments.ts +40 -0
- package/app/stores/github.ts +305 -0
- package/app/stores/localModels.ts +51 -0
- package/app/stores/mergePresets.ts +58 -0
- package/app/stores/modelDefaults.ts +76 -0
- package/app/stores/models.ts +134 -0
- package/app/stores/notifications.ts +70 -0
- package/app/stores/observability.ts +144 -0
- package/app/stores/personalSubscriptions.ts +215 -0
- package/app/stores/pipelines.ts +327 -0
- package/app/stores/recurringPipelines.ts +112 -0
- package/app/stores/releaseHealth.ts +75 -0
- package/app/stores/requirements.spec.ts +94 -0
- package/app/stores/requirements.ts +208 -0
- package/app/stores/serviceFragmentDefaults.ts +29 -0
- package/app/stores/services.ts +87 -0
- package/app/stores/slack.ts +142 -0
- package/app/stores/taskExpansion.ts +36 -0
- package/app/stores/tasks.spec.ts +71 -0
- package/app/stores/tasks.ts +176 -0
- package/app/stores/tracker.ts +27 -0
- package/app/stores/ui.ts +434 -0
- package/app/stores/vendorCredentials.ts +54 -0
- package/app/stores/workspace.ts +215 -0
- package/app/stores/workspaceSettings.ts +36 -0
- package/app/types/accounts.ts +77 -0
- package/app/types/bootstrap.ts +83 -0
- package/app/types/clarity.ts +59 -0
- package/app/types/consensus.ts +91 -0
- package/app/types/documents.ts +104 -0
- package/app/types/domain.ts +495 -0
- package/app/types/execution.ts +383 -0
- package/app/types/fragments.ts +72 -0
- package/app/types/github.ts +173 -0
- package/app/types/localModels.ts +73 -0
- package/app/types/merge.ts +71 -0
- package/app/types/models.ts +157 -0
- package/app/types/notifications.ts +74 -0
- package/app/types/recurring.ts +69 -0
- package/app/types/releaseHealth.ts +31 -0
- package/app/types/requirements.ts +61 -0
- package/app/types/services.ts +27 -0
- package/app/types/slack.ts +57 -0
- package/app/types/tasks.ts +82 -0
- package/app/types/tracker.ts +18 -0
- package/app/utils/agentOutput.spec.ts +128 -0
- package/app/utils/agentOutput.ts +173 -0
- package/app/utils/catalog.spec.ts +112 -0
- package/app/utils/catalog.ts +455 -0
- package/app/utils/dnd.ts +29 -0
- package/app/utils/observability.ts +52 -0
- package/app/utils/pipelineRender.ts +151 -0
- package/nuxt.config.ts +55 -0
- package/package.json +45 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Datadog post-release-health settings. Two sections:
|
|
3
|
+
// - Connection (per-workspace): site + API/app keys (write-only, never read back).
|
|
4
|
+
// - Monitor/SLO mappings (per service-frame block): which monitors/SLOs the
|
|
5
|
+
// `post-release-health` gate watches after that service's PRs ship.
|
|
6
|
+
import { computed, reactive, ref, watch } from 'vue'
|
|
7
|
+
|
|
8
|
+
const ui = useUiStore()
|
|
9
|
+
const store = useReleaseHealthStore()
|
|
10
|
+
const toast = useToast()
|
|
11
|
+
|
|
12
|
+
const open = computed({
|
|
13
|
+
get: () => ui.datadogOpen,
|
|
14
|
+
set: (v: boolean) => (v ? ui.openDatadog() : ui.closeDatadog()),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const conn = reactive({ site: 'datadoghq.com', apiKey: '', appKey: '' })
|
|
18
|
+
const busy = ref(false)
|
|
19
|
+
|
|
20
|
+
// New-mapping form.
|
|
21
|
+
const draft = reactive({ blockId: '', monitorIds: '', sloIds: '', envTag: '' })
|
|
22
|
+
|
|
23
|
+
function notifyError(title: string, e: unknown) {
|
|
24
|
+
toast.add({
|
|
25
|
+
title,
|
|
26
|
+
description: e instanceof Error ? e.message : String(e),
|
|
27
|
+
icon: 'i-lucide-triangle-alert',
|
|
28
|
+
color: 'error',
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
watch(
|
|
33
|
+
() => open.value,
|
|
34
|
+
async (isOpen) => {
|
|
35
|
+
if (!isOpen) return
|
|
36
|
+
try {
|
|
37
|
+
await store.load()
|
|
38
|
+
if (store.connection.site) conn.site = store.connection.site
|
|
39
|
+
} catch (e) {
|
|
40
|
+
notifyError('Could not load Datadog settings', e)
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
function parseIds(csv: string): string[] {
|
|
46
|
+
return csv
|
|
47
|
+
.split(',')
|
|
48
|
+
.map((s) => s.trim())
|
|
49
|
+
.filter((s) => s.length > 0)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function saveConnection() {
|
|
53
|
+
busy.value = true
|
|
54
|
+
try {
|
|
55
|
+
await store.saveConnection({ site: conn.site, apiKey: conn.apiKey, appKey: conn.appKey })
|
|
56
|
+
conn.apiKey = ''
|
|
57
|
+
conn.appKey = ''
|
|
58
|
+
toast.add({ title: 'Datadog connected', icon: 'i-lucide-check', color: 'success' })
|
|
59
|
+
} catch (e) {
|
|
60
|
+
notifyError('Could not save the Datadog connection', e)
|
|
61
|
+
} finally {
|
|
62
|
+
busy.value = false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function disconnect() {
|
|
67
|
+
busy.value = true
|
|
68
|
+
try {
|
|
69
|
+
await store.removeConnection()
|
|
70
|
+
} catch (e) {
|
|
71
|
+
notifyError('Could not disconnect Datadog', e)
|
|
72
|
+
} finally {
|
|
73
|
+
busy.value = false
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function addMapping() {
|
|
78
|
+
if (!draft.blockId.trim()) return
|
|
79
|
+
busy.value = true
|
|
80
|
+
try {
|
|
81
|
+
await store.saveConfig(draft.blockId.trim(), {
|
|
82
|
+
monitorIds: parseIds(draft.monitorIds),
|
|
83
|
+
sloIds: parseIds(draft.sloIds),
|
|
84
|
+
envTag: draft.envTag.trim() || null,
|
|
85
|
+
})
|
|
86
|
+
draft.blockId = ''
|
|
87
|
+
draft.monitorIds = ''
|
|
88
|
+
draft.sloIds = ''
|
|
89
|
+
draft.envTag = ''
|
|
90
|
+
} catch (e) {
|
|
91
|
+
notifyError('Could not save the mapping', e)
|
|
92
|
+
} finally {
|
|
93
|
+
busy.value = false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function removeMapping(blockId: string) {
|
|
98
|
+
busy.value = true
|
|
99
|
+
try {
|
|
100
|
+
await store.removeConfig(blockId)
|
|
101
|
+
} catch (e) {
|
|
102
|
+
notifyError('Could not remove the mapping', e)
|
|
103
|
+
} finally {
|
|
104
|
+
busy.value = false
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<template>
|
|
110
|
+
<UModal v-model:open="open" title="Datadog post-release health" :ui="{ content: 'max-w-2xl' }">
|
|
111
|
+
<template #body>
|
|
112
|
+
<div class="space-y-6">
|
|
113
|
+
<p class="text-sm text-slate-400">
|
|
114
|
+
After a release ships, the <code>post-release-health</code> gate watches the configured
|
|
115
|
+
Datadog monitors/SLOs. On a regression it spawns an on-call agent to investigate (a human
|
|
116
|
+
decides whether to revert).
|
|
117
|
+
</p>
|
|
118
|
+
|
|
119
|
+
<!-- Connection -->
|
|
120
|
+
<section class="space-y-3 rounded-lg border border-slate-700 p-3">
|
|
121
|
+
<div class="flex items-center justify-between">
|
|
122
|
+
<h3 class="text-sm font-semibold">Connection</h3>
|
|
123
|
+
<UBadge :color="store.connection.connected ? 'success' : 'neutral'" variant="soft">
|
|
124
|
+
{{
|
|
125
|
+
store.connection.connected
|
|
126
|
+
? `Connected (${store.connection.site})`
|
|
127
|
+
: 'Not connected'
|
|
128
|
+
}}
|
|
129
|
+
</UBadge>
|
|
130
|
+
</div>
|
|
131
|
+
<UFormField label="Datadog site">
|
|
132
|
+
<UInput v-model="conn.site" placeholder="datadoghq.com" />
|
|
133
|
+
</UFormField>
|
|
134
|
+
<UFormField label="API key">
|
|
135
|
+
<UInput v-model="conn.apiKey" type="password" placeholder="DD-API-KEY" />
|
|
136
|
+
</UFormField>
|
|
137
|
+
<UFormField label="Application key">
|
|
138
|
+
<UInput v-model="conn.appKey" type="password" placeholder="DD-APPLICATION-KEY" />
|
|
139
|
+
</UFormField>
|
|
140
|
+
<div class="flex gap-2">
|
|
141
|
+
<UButton
|
|
142
|
+
:loading="busy"
|
|
143
|
+
:disabled="!conn.apiKey || !conn.appKey"
|
|
144
|
+
@click="saveConnection"
|
|
145
|
+
>
|
|
146
|
+
Save connection
|
|
147
|
+
</UButton>
|
|
148
|
+
<UButton
|
|
149
|
+
v-if="store.connection.connected"
|
|
150
|
+
color="error"
|
|
151
|
+
variant="soft"
|
|
152
|
+
:loading="busy"
|
|
153
|
+
@click="disconnect"
|
|
154
|
+
>
|
|
155
|
+
Disconnect
|
|
156
|
+
</UButton>
|
|
157
|
+
</div>
|
|
158
|
+
</section>
|
|
159
|
+
|
|
160
|
+
<!-- Monitor/SLO mappings -->
|
|
161
|
+
<section class="space-y-3 rounded-lg border border-slate-700 p-3">
|
|
162
|
+
<h3 class="text-sm font-semibold">Monitor / SLO mappings</h3>
|
|
163
|
+
<p class="text-xs text-slate-400">
|
|
164
|
+
Map a service frame's block id to the Datadog monitor and SLO ids that gate its
|
|
165
|
+
releases. Comma-separate multiple ids.
|
|
166
|
+
</p>
|
|
167
|
+
<div
|
|
168
|
+
v-for="c in store.configs"
|
|
169
|
+
:key="c.blockId"
|
|
170
|
+
class="flex items-start justify-between gap-2 rounded border border-slate-800 p-2 text-xs"
|
|
171
|
+
>
|
|
172
|
+
<div class="space-y-0.5">
|
|
173
|
+
<div class="font-mono text-slate-300">{{ c.blockId }}</div>
|
|
174
|
+
<div class="text-slate-400">
|
|
175
|
+
monitors: {{ c.monitorIds.join(', ') || '—' }} · slos:
|
|
176
|
+
{{ c.sloIds.join(', ') || '—' }}
|
|
177
|
+
<span v-if="c.envTag"> · env: {{ c.envTag }}</span>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
<UButton
|
|
181
|
+
icon="i-lucide-trash-2"
|
|
182
|
+
color="error"
|
|
183
|
+
variant="ghost"
|
|
184
|
+
size="xs"
|
|
185
|
+
:loading="busy"
|
|
186
|
+
@click="removeMapping(c.blockId)"
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div class="space-y-2 rounded border border-dashed border-slate-700 p-2">
|
|
191
|
+
<UFormField label="Service frame block id">
|
|
192
|
+
<UInput v-model="draft.blockId" placeholder="blk_…" />
|
|
193
|
+
</UFormField>
|
|
194
|
+
<div class="grid grid-cols-2 gap-2">
|
|
195
|
+
<UFormField label="Monitor ids">
|
|
196
|
+
<UInput v-model="draft.monitorIds" placeholder="123, 456" />
|
|
197
|
+
</UFormField>
|
|
198
|
+
<UFormField label="SLO ids">
|
|
199
|
+
<UInput v-model="draft.sloIds" placeholder="abc, def" />
|
|
200
|
+
</UFormField>
|
|
201
|
+
</div>
|
|
202
|
+
<UFormField label="Env tag (optional)">
|
|
203
|
+
<UInput v-model="draft.envTag" placeholder="prod" />
|
|
204
|
+
</UFormField>
|
|
205
|
+
<UButton :loading="busy" :disabled="!draft.blockId" @click="addMapping">
|
|
206
|
+
Add mapping
|
|
207
|
+
</UButton>
|
|
208
|
+
</div>
|
|
209
|
+
</section>
|
|
210
|
+
</div>
|
|
211
|
+
</template>
|
|
212
|
+
</UModal>
|
|
213
|
+
</template>
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Per-user settings: "My local runners" — the signed-in user's own-machine LLM endpoints
|
|
3
|
+
// (Ollama, LM Studio, llama.cpp, vLLM, or any OpenAI-compatible server). A runner lives on
|
|
4
|
+
// a person's box, so these are stored per-user (not pooled). Pick a runner type (prefills the
|
|
5
|
+
// default base URL), optionally a bearer key, then "Test connection" to discover the models it
|
|
6
|
+
// serves and tick which to enable. Save persists the endpoint; the enabled models then surface
|
|
7
|
+
// automatically in the per-workspace model picker. One endpoint per runner type.
|
|
8
|
+
import { computed, ref, watch } from 'vue'
|
|
9
|
+
import { LOCAL_RUNNER_DEFAULTS, LOCAL_RUNNER_LABELS, type LocalRunner } from '~/types/localModels'
|
|
10
|
+
|
|
11
|
+
const ui = useUiStore()
|
|
12
|
+
const store = useLocalModelsStore()
|
|
13
|
+
const toast = useToast()
|
|
14
|
+
|
|
15
|
+
const open = computed({
|
|
16
|
+
get: () => ui.localModelsOpen,
|
|
17
|
+
set: (v: boolean) => (v ? ui.openLocalModels() : ui.closeLocalModels()),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// Load the user's endpoints whenever the panel opens (loaded independently of the
|
|
21
|
+
// workspace snapshot, like personal subscriptions).
|
|
22
|
+
watch(open, (isOpen) => {
|
|
23
|
+
if (isOpen) void store.load()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const RUNNERS: { value: LocalRunner; label: string }[] = (
|
|
27
|
+
Object.keys(LOCAL_RUNNER_LABELS) as LocalRunner[]
|
|
28
|
+
).map((value) => ({ value, label: LOCAL_RUNNER_LABELS[value] }))
|
|
29
|
+
|
|
30
|
+
// ---- add / edit draft ------------------------------------------------------
|
|
31
|
+
const provider = ref<LocalRunner>('ollama')
|
|
32
|
+
const label = ref('')
|
|
33
|
+
const baseUrl = ref(LOCAL_RUNNER_DEFAULTS.ollama ?? '')
|
|
34
|
+
const apiKey = ref('')
|
|
35
|
+
// The models discovered by the last "Test connection", plus the user's tick selection.
|
|
36
|
+
const discovered = ref<string[]>([])
|
|
37
|
+
const selected = ref<string[]>([])
|
|
38
|
+
const testError = ref<string | null>(null)
|
|
39
|
+
const tested = ref(false)
|
|
40
|
+
const testing = ref(false)
|
|
41
|
+
const busy = ref(false)
|
|
42
|
+
|
|
43
|
+
const existing = computed(() => store.endpoints.find((e) => e.provider === provider.value))
|
|
44
|
+
|
|
45
|
+
// Switching runner type prefills the default base URL and resets the discovered models —
|
|
46
|
+
// editing an already-connected runner loads its stored config instead.
|
|
47
|
+
watch(provider, (p) => {
|
|
48
|
+
const e = store.endpoints.find((x) => x.provider === p)
|
|
49
|
+
if (e) {
|
|
50
|
+
label.value = e.label
|
|
51
|
+
baseUrl.value = e.baseUrl
|
|
52
|
+
discovered.value = [...e.models]
|
|
53
|
+
selected.value = [...e.models]
|
|
54
|
+
} else {
|
|
55
|
+
label.value = ''
|
|
56
|
+
baseUrl.value = LOCAL_RUNNER_DEFAULTS[p] ?? ''
|
|
57
|
+
discovered.value = []
|
|
58
|
+
selected.value = []
|
|
59
|
+
}
|
|
60
|
+
apiKey.value = ''
|
|
61
|
+
testError.value = null
|
|
62
|
+
tested.value = false
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
function notifyError(title: string, e: unknown) {
|
|
66
|
+
toast.add({
|
|
67
|
+
title,
|
|
68
|
+
description: e instanceof Error ? e.message : String(e),
|
|
69
|
+
icon: 'i-lucide-triangle-alert',
|
|
70
|
+
color: 'error',
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function test() {
|
|
75
|
+
if (!baseUrl.value.trim()) return
|
|
76
|
+
testing.value = true
|
|
77
|
+
testError.value = null
|
|
78
|
+
try {
|
|
79
|
+
const result = await store.test({
|
|
80
|
+
provider: provider.value,
|
|
81
|
+
baseUrl: baseUrl.value.trim(),
|
|
82
|
+
apiKey: apiKey.value.trim() || undefined,
|
|
83
|
+
})
|
|
84
|
+
tested.value = true
|
|
85
|
+
discovered.value = result.models
|
|
86
|
+
if (result.reachable) {
|
|
87
|
+
// Keep any previously-enabled models that are still served, else default to all.
|
|
88
|
+
const keep = selected.value.filter((m) => result.models.includes(m))
|
|
89
|
+
selected.value = keep.length ? keep : [...result.models]
|
|
90
|
+
testError.value = null
|
|
91
|
+
} else {
|
|
92
|
+
testError.value = result.error ?? 'Could not reach the runner.'
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {
|
|
95
|
+
testError.value = e instanceof Error ? e.message : String(e)
|
|
96
|
+
} finally {
|
|
97
|
+
testing.value = false
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function toggleModel(model: string, on: boolean) {
|
|
102
|
+
if (on) {
|
|
103
|
+
if (!selected.value.includes(model)) selected.value = [...selected.value, model]
|
|
104
|
+
} else {
|
|
105
|
+
selected.value = selected.value.filter((m) => m !== model)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function save() {
|
|
110
|
+
if (!baseUrl.value.trim() || !selected.value.length) return
|
|
111
|
+
busy.value = true
|
|
112
|
+
try {
|
|
113
|
+
await store.upsert({
|
|
114
|
+
provider: provider.value,
|
|
115
|
+
label: label.value.trim() || undefined,
|
|
116
|
+
baseUrl: baseUrl.value.trim(),
|
|
117
|
+
apiKey: apiKey.value.trim() || undefined,
|
|
118
|
+
models: selected.value,
|
|
119
|
+
})
|
|
120
|
+
apiKey.value = ''
|
|
121
|
+
toast.add({
|
|
122
|
+
title: `${LOCAL_RUNNER_LABELS[provider.value]} saved`,
|
|
123
|
+
icon: 'i-lucide-check',
|
|
124
|
+
color: 'success',
|
|
125
|
+
})
|
|
126
|
+
} catch (e) {
|
|
127
|
+
notifyError('Could not save runner', e)
|
|
128
|
+
} finally {
|
|
129
|
+
busy.value = false
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function remove(p: LocalRunner) {
|
|
134
|
+
busy.value = true
|
|
135
|
+
try {
|
|
136
|
+
await store.remove(p)
|
|
137
|
+
if (provider.value === p) {
|
|
138
|
+
baseUrl.value = LOCAL_RUNNER_DEFAULTS[p] ?? ''
|
|
139
|
+
label.value = ''
|
|
140
|
+
discovered.value = []
|
|
141
|
+
selected.value = []
|
|
142
|
+
tested.value = false
|
|
143
|
+
}
|
|
144
|
+
toast.add({ title: 'Runner removed', icon: 'i-lucide-check' })
|
|
145
|
+
} catch (e) {
|
|
146
|
+
notifyError('Could not remove runner', e)
|
|
147
|
+
} finally {
|
|
148
|
+
busy.value = false
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
</script>
|
|
152
|
+
|
|
153
|
+
<template>
|
|
154
|
+
<UModal v-model:open="open" title="My local runners" :ui="{ content: 'max-w-2xl' }">
|
|
155
|
+
<template #body>
|
|
156
|
+
<div class="space-y-4">
|
|
157
|
+
<p class="text-xs text-slate-400">
|
|
158
|
+
Point agents at an LLM running on <strong>your own machine</strong> — Ollama, LM Studio,
|
|
159
|
+
llama.cpp, vLLM, or any OpenAI-compatible server. A runner is stored
|
|
160
|
+
<span class="text-slate-300">just for you</span> (a runner lives on your box), and the
|
|
161
|
+
models you enable appear automatically in the model picker. The API key (most runners
|
|
162
|
+
ignore it) is write-only and never shown again.
|
|
163
|
+
</p>
|
|
164
|
+
|
|
165
|
+
<!-- connected endpoints -->
|
|
166
|
+
<div
|
|
167
|
+
v-for="e in store.endpoints"
|
|
168
|
+
:key="e.provider"
|
|
169
|
+
class="flex items-center justify-between rounded-md border border-slate-700 bg-slate-900/50 px-3 py-2 text-sm"
|
|
170
|
+
>
|
|
171
|
+
<div>
|
|
172
|
+
<span class="font-medium text-slate-200">{{ e.label }}</span>
|
|
173
|
+
<span class="ml-2 text-xs text-slate-500">{{ LOCAL_RUNNER_LABELS[e.provider] }}</span>
|
|
174
|
+
<div class="text-[11px] text-slate-500">
|
|
175
|
+
{{ e.baseUrl }} · {{ e.models.length }} model{{ e.models.length === 1 ? '' : 's' }}
|
|
176
|
+
<template v-if="e.hasApiKey"> · key set</template>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
<div class="flex items-center gap-1">
|
|
180
|
+
<UButton
|
|
181
|
+
icon="i-lucide-pencil"
|
|
182
|
+
color="neutral"
|
|
183
|
+
variant="ghost"
|
|
184
|
+
size="xs"
|
|
185
|
+
:disabled="busy"
|
|
186
|
+
title="Edit"
|
|
187
|
+
@click="provider = e.provider"
|
|
188
|
+
/>
|
|
189
|
+
<UButton
|
|
190
|
+
icon="i-lucide-trash-2"
|
|
191
|
+
color="error"
|
|
192
|
+
variant="ghost"
|
|
193
|
+
size="xs"
|
|
194
|
+
:disabled="busy"
|
|
195
|
+
@click="remove(e.provider)"
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<!-- add / edit form -->
|
|
201
|
+
<div class="rounded-lg border border-dashed border-slate-700 p-3 space-y-3">
|
|
202
|
+
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
203
|
+
{{ existing ? 'Edit runner' : 'Add a runner' }}
|
|
204
|
+
</p>
|
|
205
|
+
|
|
206
|
+
<div class="flex flex-wrap items-end gap-3">
|
|
207
|
+
<UFormField label="Runner type">
|
|
208
|
+
<USelect v-model="provider" :items="RUNNERS" value-key="value" class="w-48" />
|
|
209
|
+
</UFormField>
|
|
210
|
+
<UFormField label="Label (optional)" class="flex-1 min-w-40">
|
|
211
|
+
<UInput v-model="label" :placeholder="`My ${LOCAL_RUNNER_LABELS[provider]}`" />
|
|
212
|
+
</UFormField>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<UFormField label="Base URL">
|
|
216
|
+
<UInput v-model="baseUrl" class="font-mono" placeholder="http://localhost:11434/v1" />
|
|
217
|
+
</UFormField>
|
|
218
|
+
|
|
219
|
+
<UFormField label="API key (optional)">
|
|
220
|
+
<UInput
|
|
221
|
+
v-model="apiKey"
|
|
222
|
+
type="password"
|
|
223
|
+
class="font-mono"
|
|
224
|
+
:placeholder="
|
|
225
|
+
existing?.hasApiKey ? 'leave blank to keep stored key' : 'most runners ignore this'
|
|
226
|
+
"
|
|
227
|
+
/>
|
|
228
|
+
</UFormField>
|
|
229
|
+
|
|
230
|
+
<div class="flex items-center gap-2">
|
|
231
|
+
<UButton
|
|
232
|
+
color="neutral"
|
|
233
|
+
variant="soft"
|
|
234
|
+
size="sm"
|
|
235
|
+
icon="i-lucide-plug-zap"
|
|
236
|
+
:loading="testing"
|
|
237
|
+
:disabled="!baseUrl.trim()"
|
|
238
|
+
@click="test()"
|
|
239
|
+
>
|
|
240
|
+
Test connection
|
|
241
|
+
</UButton>
|
|
242
|
+
<span v-if="testError" class="text-xs text-rose-400">{{ testError }}</span>
|
|
243
|
+
<span v-else-if="tested && discovered.length" class="text-xs text-emerald-400">
|
|
244
|
+
Reachable · {{ discovered.length }} model{{ discovered.length === 1 ? '' : 's' }}
|
|
245
|
+
</span>
|
|
246
|
+
<span v-else-if="tested" class="text-xs text-slate-500">No models reported.</span>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<!-- discovered models multi-select -->
|
|
250
|
+
<div v-if="discovered.length" class="space-y-1.5">
|
|
251
|
+
<span class="block text-[10px] uppercase tracking-wide text-slate-500">
|
|
252
|
+
Enable models
|
|
253
|
+
</span>
|
|
254
|
+
<div class="grid grid-cols-1 gap-1.5 sm:grid-cols-2">
|
|
255
|
+
<label
|
|
256
|
+
v-for="m in discovered"
|
|
257
|
+
:key="m"
|
|
258
|
+
class="flex items-center gap-2 text-sm text-slate-300"
|
|
259
|
+
>
|
|
260
|
+
<UCheckbox
|
|
261
|
+
:model-value="selected.includes(m)"
|
|
262
|
+
@update:model-value="(v: boolean | 'indeterminate') => toggleModel(m, v === true)"
|
|
263
|
+
/>
|
|
264
|
+
<span class="truncate font-mono text-xs">{{ m }}</span>
|
|
265
|
+
</label>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<div class="flex justify-end">
|
|
270
|
+
<UButton
|
|
271
|
+
color="primary"
|
|
272
|
+
variant="soft"
|
|
273
|
+
size="sm"
|
|
274
|
+
icon="i-lucide-save"
|
|
275
|
+
:loading="busy"
|
|
276
|
+
:disabled="!baseUrl.trim() || !selected.length"
|
|
277
|
+
@click="save()"
|
|
278
|
+
>
|
|
279
|
+
{{ existing ? 'Save' : 'Add runner' }}
|
|
280
|
+
</UButton>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
</template>
|
|
285
|
+
</UModal>
|
|
286
|
+
</template>
|