@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
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Local-mode-only settings: the warm-container pool + per-repo checkout reuse. These are
|
|
3
|
+
// a per-DEPLOYMENT singleton stored in the DB (they replaced the LOCAL_POOL_* / HARNESS_*
|
|
4
|
+
// env vars), so a developer tunes them here instead of editing .env. The warm pool keeps
|
|
5
|
+
// idle harness containers ready and re-leases one (preferring repo affinity) to each run —
|
|
6
|
+
// far faster startup than a cold container per run. Saving applies the new sizing to the
|
|
7
|
+
// running service immediately (the pool is resized live — no restart needed); in-flight
|
|
8
|
+
// runs keep the container they already hold, and the checkout config applies to containers
|
|
9
|
+
// started after the save.
|
|
10
|
+
import { reactive, ref, watch } from 'vue'
|
|
11
|
+
|
|
12
|
+
const ui = useUiStore()
|
|
13
|
+
const store = useLocalSettingsStore()
|
|
14
|
+
const toast = useToast()
|
|
15
|
+
|
|
16
|
+
const open = computed({
|
|
17
|
+
get: () => ui.localModeSettingsOpen,
|
|
18
|
+
set: (v: boolean) => (v ? ui.openLocalModeSettings() : ui.closeLocalModeSettings()),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const saving = ref(false)
|
|
22
|
+
|
|
23
|
+
// Editable draft. `idleMinutes` and `cleanKeep` are friendlier renderings of the stored
|
|
24
|
+
// `pool.idleTtlMs` (ms) and `checkout.cleanKeep` (string[]).
|
|
25
|
+
const draft = reactive({
|
|
26
|
+
size: 0,
|
|
27
|
+
minWarm: 0,
|
|
28
|
+
max: null as number | null,
|
|
29
|
+
idleMinutes: 10,
|
|
30
|
+
workspaceRoot: '/workspace',
|
|
31
|
+
cleanKeep: 'node_modules,.venv,target,.gradle,.pnpm-store',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
function syncDraft() {
|
|
35
|
+
const s = store.settings
|
|
36
|
+
if (!s) return
|
|
37
|
+
draft.size = s.pool.size
|
|
38
|
+
draft.minWarm = s.pool.minWarm
|
|
39
|
+
draft.max = s.pool.max
|
|
40
|
+
draft.idleMinutes = Math.round(s.pool.idleTtlMs / 60_000)
|
|
41
|
+
draft.workspaceRoot = s.checkout.workspaceRoot
|
|
42
|
+
draft.cleanKeep = s.checkout.cleanKeep.join(',')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Load + hydrate the draft whenever the panel opens.
|
|
46
|
+
watch(open, (isOpen) => {
|
|
47
|
+
if (isOpen) void store.load().then(syncDraft)
|
|
48
|
+
})
|
|
49
|
+
watch(() => store.settings, syncDraft)
|
|
50
|
+
|
|
51
|
+
async function save() {
|
|
52
|
+
const cleanKeep = draft.cleanKeep
|
|
53
|
+
.split(',')
|
|
54
|
+
.map((s) => s.trim())
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
saving.value = true
|
|
57
|
+
try {
|
|
58
|
+
await store.save({
|
|
59
|
+
pool: {
|
|
60
|
+
size: Math.max(0, Math.floor(draft.size)),
|
|
61
|
+
minWarm: Math.max(0, Math.floor(draft.minWarm)),
|
|
62
|
+
max: draft.max == null ? null : Math.max(0, Math.floor(draft.max)),
|
|
63
|
+
idleTtlMs: Math.max(0, Math.floor(draft.idleMinutes * 60_000)),
|
|
64
|
+
},
|
|
65
|
+
checkout: { workspaceRoot: draft.workspaceRoot.trim() || '/workspace', cleanKeep },
|
|
66
|
+
})
|
|
67
|
+
toast.add({ title: 'Local settings saved', icon: 'i-lucide-check', color: 'success' })
|
|
68
|
+
} catch (e) {
|
|
69
|
+
toast.add({
|
|
70
|
+
title: 'Could not save local settings',
|
|
71
|
+
description: e instanceof Error ? e.message : String(e),
|
|
72
|
+
icon: 'i-lucide-triangle-alert',
|
|
73
|
+
color: 'error',
|
|
74
|
+
})
|
|
75
|
+
} finally {
|
|
76
|
+
saving.value = false
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<template>
|
|
82
|
+
<UModal v-model:open="open" title="Local mode" :ui="{ content: 'max-w-2xl' }">
|
|
83
|
+
<template #body>
|
|
84
|
+
<div class="space-y-6">
|
|
85
|
+
<p class="text-xs text-slate-400">
|
|
86
|
+
Tuning for the local container runner — stored on this machine's deployment (it replaced
|
|
87
|
+
the <code>LOCAL_POOL_*</code> / <code>HARNESS_*</code> env vars). Saving resizes the warm
|
|
88
|
+
pool live — no restart needed; in-flight runs keep the container they already hold.
|
|
89
|
+
</p>
|
|
90
|
+
|
|
91
|
+
<!-- Warm container pool -->
|
|
92
|
+
<section class="space-y-3">
|
|
93
|
+
<div>
|
|
94
|
+
<h4 class="text-sm font-semibold text-slate-200">Warm container pool</h4>
|
|
95
|
+
<p class="text-[11px] text-slate-400">
|
|
96
|
+
Keep idle harness containers ready and re-lease one (preferring a container that
|
|
97
|
+
already holds the run's repo) instead of cold-starting per run. Pool size 0 disables
|
|
98
|
+
it. Requires a Docker-family runtime (Docker/Podman/OrbStack/Colima); ignored on Apple
|
|
99
|
+
<code>container</code>.
|
|
100
|
+
</p>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
103
|
+
<UFormField label="Pool size" help="Max idle warm containers. 0 = pooling off.">
|
|
104
|
+
<UInput v-model.number="draft.size" type="number" :min="0" size="sm" />
|
|
105
|
+
</UFormField>
|
|
106
|
+
<UFormField label="Pre-warm at boot" help="Containers started when the service boots.">
|
|
107
|
+
<UInput v-model.number="draft.minWarm" type="number" :min="0" size="sm" />
|
|
108
|
+
</UFormField>
|
|
109
|
+
<UFormField label="Max containers" help="Hard cap (leased + idle). Blank = pool size.">
|
|
110
|
+
<UInput
|
|
111
|
+
v-model.number="draft.max"
|
|
112
|
+
type="number"
|
|
113
|
+
:min="0"
|
|
114
|
+
size="sm"
|
|
115
|
+
placeholder="(pool size)"
|
|
116
|
+
/>
|
|
117
|
+
</UFormField>
|
|
118
|
+
<UFormField label="Idle timeout (minutes)" help="Evict an idle pooled container after.">
|
|
119
|
+
<UInput v-model.number="draft.idleMinutes" type="number" :min="0" size="sm" />
|
|
120
|
+
</UFormField>
|
|
121
|
+
</div>
|
|
122
|
+
</section>
|
|
123
|
+
|
|
124
|
+
<!-- Checkout reuse -->
|
|
125
|
+
<section class="space-y-3 border-t border-slate-800 pt-6">
|
|
126
|
+
<div>
|
|
127
|
+
<h4 class="text-sm font-semibold text-slate-200">Checkout reuse</h4>
|
|
128
|
+
<p class="text-[11px] text-slate-400">
|
|
129
|
+
When a warm container already holds the run's repo, the harness reuses its per-repo
|
|
130
|
+
checkout (clean sweep + fetch + switch branch) instead of cloning fresh.
|
|
131
|
+
</p>
|
|
132
|
+
</div>
|
|
133
|
+
<UFormField
|
|
134
|
+
label="Workspace root"
|
|
135
|
+
help="Absolute in-container directory the reused checkout lives under."
|
|
136
|
+
>
|
|
137
|
+
<UInput v-model="draft.workspaceRoot" size="sm" placeholder="/workspace" />
|
|
138
|
+
</UFormField>
|
|
139
|
+
<UFormField
|
|
140
|
+
label="Keep on clean (comma-separated)"
|
|
141
|
+
help="Dependency-cache directories the per-run clean sweep preserves."
|
|
142
|
+
>
|
|
143
|
+
<UInput
|
|
144
|
+
v-model="draft.cleanKeep"
|
|
145
|
+
size="sm"
|
|
146
|
+
placeholder="node_modules,.venv,target,.gradle,.pnpm-store"
|
|
147
|
+
/>
|
|
148
|
+
</UFormField>
|
|
149
|
+
</section>
|
|
150
|
+
|
|
151
|
+
<div class="flex justify-end">
|
|
152
|
+
<UButton color="primary" icon="i-lucide-save" :loading="saving" @click="save">
|
|
153
|
+
Save
|
|
154
|
+
</UButton>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</template>
|
|
158
|
+
</UModal>
|
|
159
|
+
</template>
|
|
@@ -8,6 +8,7 @@ import { onMounted, ref } from 'vue'
|
|
|
8
8
|
|
|
9
9
|
const fragments = useFragmentsStore()
|
|
10
10
|
const defaults = useServiceFragmentDefaultsStore()
|
|
11
|
+
const ui = useUiStore()
|
|
11
12
|
const toast = useToast()
|
|
12
13
|
|
|
13
14
|
const busy = ref(false)
|
|
@@ -105,5 +106,25 @@ function remove(id: string) {
|
|
|
105
106
|
<p v-else class="text-[11px] text-slate-500">
|
|
106
107
|
None — new services start with no service-level fragments.
|
|
107
108
|
</p>
|
|
109
|
+
|
|
110
|
+
<div class="flex flex-wrap gap-x-4 gap-y-1 border-t border-slate-800 pt-3 text-[11px]">
|
|
111
|
+
<span class="text-slate-500">
|
|
112
|
+
Need to add or edit the fragments themselves (hand-authored, linked docs, repos)?
|
|
113
|
+
</span>
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
class="font-medium text-primary-400 hover:underline"
|
|
117
|
+
@click="ui.openFragmentLibrary()"
|
|
118
|
+
>
|
|
119
|
+
Manage this board's fragment library →
|
|
120
|
+
</button>
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
class="font-medium text-primary-400 hover:underline"
|
|
124
|
+
@click="ui.openAccountSettings('fragments')"
|
|
125
|
+
>
|
|
126
|
+
Manage account fragments →
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
108
129
|
</div>
|
|
109
130
|
</template>
|
|
@@ -53,10 +53,19 @@ export function fragmentsApi({ http, ws, scope }: ApiContext) {
|
|
|
53
53
|
body: CreateDocumentFragmentInput,
|
|
54
54
|
) => http<PromptFragment>(`${scope(kind, id)}/document-fragments`, { method: 'POST', body }),
|
|
55
55
|
|
|
56
|
-
// Force an immediate live re-resolve of a document-backed fragment.
|
|
57
|
-
|
|
56
|
+
// Force an immediate live re-resolve of a document-backed fragment. At the
|
|
57
|
+
// account scope the backend needs a `viaWorkspaceId` (the workspace whose
|
|
58
|
+
// document-source connection to fetch through); it is ignored at workspace scope.
|
|
59
|
+
refreshFragment: (
|
|
60
|
+
kind: FragmentOwnerKind,
|
|
61
|
+
id: string,
|
|
62
|
+
fragmentId: string,
|
|
63
|
+
viaWorkspaceId?: string,
|
|
64
|
+
) =>
|
|
58
65
|
http<PromptFragment>(
|
|
59
|
-
`${scope(kind, id)}/prompt-fragments/${encodeURIComponent(fragmentId)}/refresh
|
|
66
|
+
`${scope(kind, id)}/prompt-fragments/${encodeURIComponent(fragmentId)}/refresh${
|
|
67
|
+
viaWorkspaceId ? `?viaWorkspaceId=${encodeURIComponent(viaWorkspaceId)}` : ''
|
|
68
|
+
}`,
|
|
60
69
|
{ method: 'POST' },
|
|
61
70
|
),
|
|
62
71
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { LocalSettings, UpdateLocalSettingsInput } from '~/types/localSettings'
|
|
2
|
+
import type { ApiContext } from './context'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Local-mode operational settings (warm-container-pool sizing + per-repo checkout reuse) —
|
|
6
|
+
* a per-deployment singleton. Wired only on the local-mode service; both calls 503 on the
|
|
7
|
+
* Worker / stock Node facades (the store hides the panel then). No secrets, so the read view
|
|
8
|
+
* is the plain config and the write replaces it wholesale.
|
|
9
|
+
*/
|
|
10
|
+
export function localSettingsApi({ http }: ApiContext) {
|
|
11
|
+
return {
|
|
12
|
+
getLocalSettings: () => http<LocalSettings>('/local-settings'),
|
|
13
|
+
|
|
14
|
+
updateLocalSettings: (body: UpdateLocalSettingsInput) =>
|
|
15
|
+
http<LocalSettings>('/local-settings', { method: 'PUT', body }),
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -11,6 +11,7 @@ import { fragmentsApi } from './api/fragments'
|
|
|
11
11
|
import { githubApi } from './api/github'
|
|
12
12
|
import { humanTestApi } from './api/humanTest'
|
|
13
13
|
import { kaizenApi } from './api/kaizen'
|
|
14
|
+
import { localSettingsApi } from './api/localSettings'
|
|
14
15
|
import { modelsApi } from './api/models'
|
|
15
16
|
import { notificationsApi } from './api/notifications'
|
|
16
17
|
import { presetsApi } from './api/presets'
|
|
@@ -89,6 +90,7 @@ export function useApi() {
|
|
|
89
90
|
...followUpsApi(ctx),
|
|
90
91
|
...humanTestApi(ctx),
|
|
91
92
|
...kaizenApi(ctx),
|
|
93
|
+
...localSettingsApi(ctx),
|
|
92
94
|
...specApi(ctx),
|
|
93
95
|
...notificationsApi(ctx),
|
|
94
96
|
...presetsApi(ctx),
|
package/app/pages/index.vue
CHANGED
|
@@ -35,6 +35,7 @@ import ProviderConnectionPanel from '~/components/settings/ProviderConnectionPan
|
|
|
35
35
|
import ProviderConfigBanner from '~/components/layout/ProviderConfigBanner.vue'
|
|
36
36
|
import ModelConfigurationPanel from '~/components/settings/ModelConfigurationPanel.vue'
|
|
37
37
|
import LocalModelEndpointsPanel from '~/components/settings/LocalModelEndpointsPanel.vue'
|
|
38
|
+
import LocalModeSettingsPanel from '~/components/settings/LocalModeSettingsPanel.vue'
|
|
38
39
|
import SandboxPanel from '~/components/sandbox/SandboxPanel.vue'
|
|
39
40
|
import UserSecretsSection from '~/components/settings/UserSecretsSection.vue'
|
|
40
41
|
import OpenRouterCatalogPanel from '~/components/settings/OpenRouterCatalogPanel.vue'
|
|
@@ -194,6 +195,7 @@ watch(
|
|
|
194
195
|
<ProviderConnectionPanel />
|
|
195
196
|
<ModelConfigurationPanel />
|
|
196
197
|
<LocalModelEndpointsPanel />
|
|
198
|
+
<LocalModeSettingsPanel />
|
|
197
199
|
<SandboxPanel />
|
|
198
200
|
<UserSecretsSection />
|
|
199
201
|
<OpenRouterCatalogPanel />
|
|
@@ -3,6 +3,7 @@ import { computed, ref } from 'vue'
|
|
|
3
3
|
import type {
|
|
4
4
|
CreateDocumentFragmentInput,
|
|
5
5
|
CreatePromptFragmentInput,
|
|
6
|
+
FragmentOwnerKind,
|
|
6
7
|
FragmentSource,
|
|
7
8
|
LinkFragmentSourceInput,
|
|
8
9
|
PromptFragment,
|
|
@@ -12,40 +13,62 @@ import type {
|
|
|
12
13
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
|
-
* Prompt-fragment library state (ADR 0006), scoped to
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* Prompt-fragment library state (ADR 0006), scoped to a single owner — a board
|
|
17
|
+
* (`workspace`) or an account. Holds that owner's own (raw) tier fragments, its
|
|
18
|
+
* linked guideline repos, and — for the **workspace** tier only — the merged
|
|
19
|
+
* catalog an agent actually sees (built-in ∪ account ∪ workspace). `available`
|
|
20
|
+
* mirrors the backend's opt-in gate: a 503 from the probe means the feature is off
|
|
21
|
+
* and the UI hides its entry.
|
|
22
|
+
*
|
|
23
|
+
* Two entry points share this setup: `useFragmentLibraryStore` (the workspace
|
|
24
|
+
* singleton that follows the active board, used by the navbar + the board modal)
|
|
25
|
+
* and `useFragmentLibrary(kind, ownerId)` (an owner-keyed store, used for the
|
|
26
|
+
* account tier). The account tier has no resolved/merged catalog and, for
|
|
27
|
+
* document-backed fragments, needs a `viaWorkspaceId` (document-source credentials
|
|
28
|
+
* are per-workspace).
|
|
21
29
|
*/
|
|
22
|
-
|
|
30
|
+
function fragmentLibrarySetup(kind: FragmentOwnerKind, resolveOwnerId: () => string | null) {
|
|
23
31
|
const api = useApi()
|
|
24
|
-
|
|
32
|
+
|
|
33
|
+
/** The merged/resolved catalog only exists at the workspace tier. */
|
|
34
|
+
const hasResolved = kind === 'workspace'
|
|
25
35
|
|
|
26
36
|
/** null = not probed yet; true/false = library on/off. */
|
|
27
37
|
const available = ref<boolean | null>(null)
|
|
28
|
-
/** This
|
|
38
|
+
/** This owner's hand-authored + sourced fragments (its own tier, raw). */
|
|
29
39
|
const fragments = ref<PromptFragment[]>([])
|
|
30
|
-
/** The merged catalog an agent sees (
|
|
40
|
+
/** The merged catalog an agent sees (workspace tier only; empty otherwise). */
|
|
31
41
|
const resolved = ref<ResolvedFragment[]>([])
|
|
32
|
-
/** Linked guideline repos for this
|
|
42
|
+
/** Linked guideline repos for this owner. */
|
|
33
43
|
const sources = ref<FragmentSource[]>([])
|
|
34
44
|
/** Per-source "changes available" counts from the last status check. */
|
|
35
45
|
const sourceChanges = ref<Record<string, number>>({})
|
|
36
46
|
const loading = ref(false)
|
|
47
|
+
/**
|
|
48
|
+
* Account-tier document fragments only: the workspace whose stored
|
|
49
|
+
* document-source connection is used to fetch/refresh the page (credentials are
|
|
50
|
+
* per-workspace). Set by the caller; ignored at the workspace scope (the owner
|
|
51
|
+
* board is used directly).
|
|
52
|
+
*/
|
|
53
|
+
const viaWorkspaceId = ref<string | undefined>(undefined)
|
|
37
54
|
|
|
38
55
|
const builtinCount = computed(() => resolved.value.filter((f) => f.tier === 'builtin').length)
|
|
39
56
|
|
|
40
|
-
|
|
57
|
+
function requireOwnerId(): string {
|
|
58
|
+
const id = resolveOwnerId()
|
|
59
|
+
if (!id) throw new Error('No fragment-library owner')
|
|
60
|
+
return id
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Probe the feature + load this owner's tier, sources and (ws) resolved catalog. */
|
|
41
64
|
async function probe() {
|
|
42
|
-
|
|
43
|
-
|
|
65
|
+
const id = resolveOwnerId()
|
|
66
|
+
if (!id) return
|
|
44
67
|
try {
|
|
45
68
|
const [tier, srcs, merged] = await Promise.all([
|
|
46
|
-
api.listFragments(
|
|
47
|
-
api.listFragmentSources(
|
|
48
|
-
api.getResolvedFragments(id),
|
|
69
|
+
api.listFragments(kind, id),
|
|
70
|
+
api.listFragmentSources(kind, id).catch(() => [] as FragmentSource[]),
|
|
71
|
+
hasResolved ? api.getResolvedFragments(id) : Promise.resolve([] as ResolvedFragment[]),
|
|
49
72
|
])
|
|
50
73
|
fragments.value = tier
|
|
51
74
|
sources.value = srcs
|
|
@@ -60,13 +83,14 @@ export const useFragmentLibraryStore = defineStore('fragmentLibrary', () => {
|
|
|
60
83
|
}
|
|
61
84
|
|
|
62
85
|
async function refreshResolved() {
|
|
63
|
-
|
|
86
|
+
if (!hasResolved) return
|
|
87
|
+
resolved.value = await api.getResolvedFragments(requireOwnerId())
|
|
64
88
|
}
|
|
65
89
|
|
|
66
90
|
async function create(input: CreatePromptFragmentInput) {
|
|
67
91
|
loading.value = true
|
|
68
92
|
try {
|
|
69
|
-
await api.createFragment(
|
|
93
|
+
await api.createFragment(kind, requireOwnerId(), input)
|
|
70
94
|
await Promise.all([reloadTier(), refreshResolved()])
|
|
71
95
|
} finally {
|
|
72
96
|
loading.value = false
|
|
@@ -74,7 +98,7 @@ export const useFragmentLibraryStore = defineStore('fragmentLibrary', () => {
|
|
|
74
98
|
}
|
|
75
99
|
|
|
76
100
|
async function update(fragmentId: string, patch: UpdatePromptFragmentInput) {
|
|
77
|
-
await api.updateFragment(
|
|
101
|
+
await api.updateFragment(kind, requireOwnerId(), fragmentId, patch)
|
|
78
102
|
await Promise.all([reloadTier(), refreshResolved()])
|
|
79
103
|
}
|
|
80
104
|
|
|
@@ -82,7 +106,10 @@ export const useFragmentLibraryStore = defineStore('fragmentLibrary', () => {
|
|
|
82
106
|
async function createDocumentFragment(input: CreateDocumentFragmentInput) {
|
|
83
107
|
loading.value = true
|
|
84
108
|
try {
|
|
85
|
-
await api.createDocumentFragment(
|
|
109
|
+
await api.createDocumentFragment(kind, requireOwnerId(), {
|
|
110
|
+
...input,
|
|
111
|
+
...(viaWorkspaceId.value ? { viaWorkspaceId: viaWorkspaceId.value } : {}),
|
|
112
|
+
})
|
|
86
113
|
await Promise.all([reloadTier(), refreshResolved()])
|
|
87
114
|
} finally {
|
|
88
115
|
loading.value = false
|
|
@@ -93,31 +120,31 @@ export const useFragmentLibraryStore = defineStore('fragmentLibrary', () => {
|
|
|
93
120
|
async function refreshDocumentFragment(fragmentId: string) {
|
|
94
121
|
loading.value = true
|
|
95
122
|
try {
|
|
96
|
-
await api.refreshFragment(
|
|
123
|
+
await api.refreshFragment(kind, requireOwnerId(), fragmentId, viaWorkspaceId.value)
|
|
97
124
|
await Promise.all([reloadTier(), refreshResolved()])
|
|
98
125
|
} finally {
|
|
99
126
|
loading.value = false
|
|
100
127
|
}
|
|
101
128
|
}
|
|
102
129
|
|
|
103
|
-
/** Tombstone a fragment at
|
|
130
|
+
/** Tombstone a fragment at this tier (suppresses an inherited one). */
|
|
104
131
|
async function remove(fragmentId: string) {
|
|
105
|
-
await api.deleteFragment(
|
|
132
|
+
await api.deleteFragment(kind, requireOwnerId(), fragmentId)
|
|
106
133
|
await Promise.all([reloadTier(), refreshResolved()])
|
|
107
134
|
}
|
|
108
135
|
|
|
109
136
|
async function reloadTier() {
|
|
110
|
-
fragments.value = await api.listFragments(
|
|
137
|
+
fragments.value = await api.listFragments(kind, requireOwnerId())
|
|
111
138
|
}
|
|
112
139
|
|
|
113
140
|
async function linkSource(input: LinkFragmentSourceInput) {
|
|
114
|
-
const source = await api.linkFragmentSource(
|
|
141
|
+
const source = await api.linkFragmentSource(kind, requireOwnerId(), input)
|
|
115
142
|
sources.value = [source, ...sources.value]
|
|
116
143
|
return source
|
|
117
144
|
}
|
|
118
145
|
|
|
119
146
|
async function unlinkSource(sourceId: string) {
|
|
120
|
-
await api.unlinkFragmentSource(
|
|
147
|
+
await api.unlinkFragmentSource(kind, requireOwnerId(), sourceId)
|
|
121
148
|
sources.value = sources.value.filter((s) => s.id !== sourceId)
|
|
122
149
|
await refreshResolved()
|
|
123
150
|
}
|
|
@@ -126,7 +153,7 @@ export const useFragmentLibraryStore = defineStore('fragmentLibrary', () => {
|
|
|
126
153
|
async function syncSource(sourceId: string) {
|
|
127
154
|
loading.value = true
|
|
128
155
|
try {
|
|
129
|
-
const result = await api.syncFragmentSource(
|
|
156
|
+
const result = await api.syncFragmentSource(kind, requireOwnerId(), sourceId)
|
|
130
157
|
delete sourceChanges.value[sourceId]
|
|
131
158
|
await Promise.all([reloadSources(), refreshResolved()])
|
|
132
159
|
return result
|
|
@@ -137,7 +164,7 @@ export const useFragmentLibraryStore = defineStore('fragmentLibrary', () => {
|
|
|
137
164
|
|
|
138
165
|
/** Cheap "check for changes" for a source; caches the changed count. */
|
|
139
166
|
async function checkSource(sourceId: string) {
|
|
140
|
-
const status = await api.fragmentSourceStatus(
|
|
167
|
+
const status = await api.fragmentSourceStatus(kind, requireOwnerId(), sourceId)
|
|
141
168
|
sourceChanges.value = {
|
|
142
169
|
...sourceChanges.value,
|
|
143
170
|
[sourceId]: status.changed ? status.changedCount : 0,
|
|
@@ -146,16 +173,19 @@ export const useFragmentLibraryStore = defineStore('fragmentLibrary', () => {
|
|
|
146
173
|
}
|
|
147
174
|
|
|
148
175
|
async function reloadSources() {
|
|
149
|
-
sources.value = await api.listFragmentSources(
|
|
176
|
+
sources.value = await api.listFragmentSources(kind, requireOwnerId())
|
|
150
177
|
}
|
|
151
178
|
|
|
152
179
|
return {
|
|
180
|
+
kind,
|
|
181
|
+
hasResolved,
|
|
153
182
|
available,
|
|
154
183
|
fragments,
|
|
155
184
|
resolved,
|
|
156
185
|
sources,
|
|
157
186
|
sourceChanges,
|
|
158
187
|
loading,
|
|
188
|
+
viaWorkspaceId,
|
|
159
189
|
builtinCount,
|
|
160
190
|
probe,
|
|
161
191
|
refreshResolved,
|
|
@@ -169,4 +199,23 @@ export const useFragmentLibraryStore = defineStore('fragmentLibrary', () => {
|
|
|
169
199
|
syncSource,
|
|
170
200
|
checkSource,
|
|
171
201
|
}
|
|
172
|
-
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The workspace-tier library for the **active** board — a singleton that resolves
|
|
206
|
+
* the owner lazily, so it follows board switches and is shared by the navbar
|
|
207
|
+
* (SideBar/CommandBar probes) and the board fragment modal.
|
|
208
|
+
*/
|
|
209
|
+
export const useFragmentLibraryStore = defineStore('fragmentLibrary', () =>
|
|
210
|
+
fragmentLibrarySetup('workspace', () => useWorkspaceStore().workspaceId),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* An owner-keyed library store, used for the **account** tier (and reusable for any
|
|
215
|
+
* explicit owner). Keyed by `(kind, ownerId)` so each account gets isolated state.
|
|
216
|
+
*/
|
|
217
|
+
export function useFragmentLibrary(kind: FragmentOwnerKind, ownerId: string) {
|
|
218
|
+
return defineStore(`fragmentLibrary:${kind}:${ownerId}`, () =>
|
|
219
|
+
fragmentLibrarySetup(kind, () => ownerId),
|
|
220
|
+
)()
|
|
221
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import type { LocalSettings, UpdateLocalSettingsInput } from '~/types/localSettings'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Local-mode operational settings (warm-container-pool sizing + per-repo checkout reuse) —
|
|
7
|
+
* a per-deployment singleton that replaced the old LOCAL_POOL_* / HARNESS_* env vars. No
|
|
8
|
+
* secrets, so the store holds the full config. `available` mirrors the backend opt-in: a 503
|
|
9
|
+
* (not the local-mode service) hides the panel. Loaded on demand from the settings panel.
|
|
10
|
+
*/
|
|
11
|
+
export const useLocalSettingsStore = defineStore('localSettings', () => {
|
|
12
|
+
const api = useApi()
|
|
13
|
+
|
|
14
|
+
const settings = ref<LocalSettings | null>(null)
|
|
15
|
+
const loading = ref(false)
|
|
16
|
+
const available = ref<boolean | null>(null)
|
|
17
|
+
|
|
18
|
+
async function load() {
|
|
19
|
+
loading.value = true
|
|
20
|
+
try {
|
|
21
|
+
settings.value = await api.getLocalSettings()
|
|
22
|
+
available.value = true
|
|
23
|
+
} catch (e) {
|
|
24
|
+
// 503 ⇒ not the local-mode service ⇒ hide the panel.
|
|
25
|
+
if (
|
|
26
|
+
e &&
|
|
27
|
+
typeof e === 'object' &&
|
|
28
|
+
'statusCode' in e &&
|
|
29
|
+
(e as { statusCode?: number }).statusCode === 503
|
|
30
|
+
) {
|
|
31
|
+
available.value = false
|
|
32
|
+
settings.value = null
|
|
33
|
+
} else {
|
|
34
|
+
throw e
|
|
35
|
+
}
|
|
36
|
+
} finally {
|
|
37
|
+
loading.value = false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function save(input: UpdateLocalSettingsInput) {
|
|
42
|
+
settings.value = await api.updateLocalSettings(input)
|
|
43
|
+
available.value = true
|
|
44
|
+
return settings.value
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { settings, loading, available, load, save }
|
|
48
|
+
})
|
package/app/stores/ui.ts
CHANGED
|
@@ -96,9 +96,13 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
96
96
|
// `workspaceSettingsTab` lets other surfaces deep-link straight to a tab.
|
|
97
97
|
const workspaceSettingsOpen = ref(false)
|
|
98
98
|
const workspaceSettingsTab = ref('workspace')
|
|
99
|
-
// Account
|
|
100
|
-
//
|
|
99
|
+
// Account-settings modal: a single tabbed window for the per-account configuration —
|
|
100
|
+
// the team panel (members + roles + invitations + email sender + account API keys,
|
|
101
|
+
// `AccountTeamSettings`) and the account-tier prompt-fragment library. Account-scoped
|
|
102
|
+
// (distinct from workspace settings). `accountSettingsTab` lets other surfaces deep-link
|
|
103
|
+
// straight to a tab.
|
|
101
104
|
const accountSettingsOpen = ref(false)
|
|
105
|
+
const accountSettingsTab = ref('team')
|
|
102
106
|
// Observability integration: the post-release-health connection panel (Datadog
|
|
103
107
|
// today, pluggable). NB: distinct from `observabilityInstanceId` below, which is the
|
|
104
108
|
// LLM per-call observability panel.
|
|
@@ -112,6 +116,9 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
112
116
|
const vendorCredentialsOpen = ref(false)
|
|
113
117
|
// Per-user settings panel: the signed-in user's own-machine local model runners.
|
|
114
118
|
const localModelsOpen = ref(false)
|
|
119
|
+
// Local-mode-only settings panel: the warm-container pool sizing + per-repo checkout reuse
|
|
120
|
+
// (a per-deployment singleton that replaced the LOCAL_POOL_* / HARNESS_* env vars).
|
|
121
|
+
const localModeSettingsOpen = ref(false)
|
|
115
122
|
// The Sandbox (parallel prompt/model testing) surface — an opt-in, on-demand window.
|
|
116
123
|
const sandboxOpen = ref(false)
|
|
117
124
|
const userSecretsOpen = ref(false)
|
|
@@ -383,13 +390,17 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
383
390
|
function setWorkspaceSettingsTab(tab: string) {
|
|
384
391
|
workspaceSettingsTab.value = tab
|
|
385
392
|
}
|
|
386
|
-
function openAccountSettings() {
|
|
393
|
+
function openAccountSettings(tab = 'team') {
|
|
387
394
|
cameFromIntegrations.value = false
|
|
395
|
+
accountSettingsTab.value = tab
|
|
388
396
|
accountSettingsOpen.value = true
|
|
389
397
|
}
|
|
390
398
|
function closeAccountSettings() {
|
|
391
399
|
accountSettingsOpen.value = false
|
|
392
400
|
}
|
|
401
|
+
function setAccountSettingsTab(tab: string) {
|
|
402
|
+
accountSettingsTab.value = tab
|
|
403
|
+
}
|
|
393
404
|
function openObservabilityConnection() {
|
|
394
405
|
cameFromIntegrations.value = false
|
|
395
406
|
observabilityConnectionOpen.value = true
|
|
@@ -424,6 +435,13 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
424
435
|
function closeLocalModels() {
|
|
425
436
|
localModelsOpen.value = false
|
|
426
437
|
}
|
|
438
|
+
function openLocalModeSettings() {
|
|
439
|
+
cameFromIntegrations.value = false
|
|
440
|
+
localModeSettingsOpen.value = true
|
|
441
|
+
}
|
|
442
|
+
function closeLocalModeSettings() {
|
|
443
|
+
localModeSettingsOpen.value = false
|
|
444
|
+
}
|
|
427
445
|
function openSandbox() {
|
|
428
446
|
sandboxOpen.value = true
|
|
429
447
|
}
|
|
@@ -566,11 +584,13 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
566
584
|
workspaceSettingsOpen,
|
|
567
585
|
workspaceSettingsTab,
|
|
568
586
|
accountSettingsOpen,
|
|
587
|
+
accountSettingsTab,
|
|
569
588
|
observabilityConnectionOpen,
|
|
570
589
|
providerConnectionKind,
|
|
571
590
|
modelConfigOpen,
|
|
572
591
|
vendorCredentialsOpen,
|
|
573
592
|
localModelsOpen,
|
|
593
|
+
localModeSettingsOpen,
|
|
574
594
|
sandboxOpen,
|
|
575
595
|
userSecretsOpen,
|
|
576
596
|
openRouterOpen,
|
|
@@ -630,6 +650,7 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
630
650
|
setWorkspaceSettingsTab,
|
|
631
651
|
openAccountSettings,
|
|
632
652
|
closeAccountSettings,
|
|
653
|
+
setAccountSettingsTab,
|
|
633
654
|
openObservabilityConnection,
|
|
634
655
|
closeObservabilityConnection,
|
|
635
656
|
openProviderConnection,
|
|
@@ -640,6 +661,8 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
640
661
|
closeVendorCredentials,
|
|
641
662
|
openLocalModels,
|
|
642
663
|
closeLocalModels,
|
|
664
|
+
openLocalModeSettings,
|
|
665
|
+
closeLocalModeSettings,
|
|
643
666
|
openSandbox,
|
|
644
667
|
closeSandbox,
|
|
645
668
|
openUserSecrets,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Local-mode operational settings (warm-container-pool sizing + per-repo checkout reuse).
|
|
2
|
+
// Mirrors `@cat-factory/contracts` localSettings. A per-deployment singleton, edited in the
|
|
3
|
+
// dedicated local-mode settings panel — these replaced the old LOCAL_POOL_* / HARNESS_* env
|
|
4
|
+
// vars. There are no secrets, so the read view is the plain config. Local-mode-only (the
|
|
5
|
+
// warm pool is the local Docker-family runner's differentiator).
|
|
6
|
+
|
|
7
|
+
export interface LocalPoolSettings {
|
|
8
|
+
/** Max idle warm containers kept for re-lease. 0 disables pooling (cold-start per run). */
|
|
9
|
+
size: number
|
|
10
|
+
/** Containers pre-warmed when the service starts. */
|
|
11
|
+
minWarm: number
|
|
12
|
+
/** Hard cap on total containers (leased + idle). `null` ⇒ defaults to `size`. */
|
|
13
|
+
max: number | null
|
|
14
|
+
/** How long an idle pooled container is kept before eviction (ms). */
|
|
15
|
+
idleTtlMs: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface LocalCheckoutSettings {
|
|
19
|
+
/** Absolute in-container dir the reused per-repo checkout lives under. */
|
|
20
|
+
workspaceRoot: string
|
|
21
|
+
/** Dep-cache directories the per-run clean sweep keeps (so deps aren't reinstalled). */
|
|
22
|
+
cleanKeep: string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface LocalSettings {
|
|
26
|
+
pool: LocalPoolSettings
|
|
27
|
+
checkout: LocalCheckoutSettings
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Admin write: the full settings blob fully replaces the stored config. */
|
|
31
|
+
export type UpdateLocalSettingsInput = LocalSettings
|