@cat-factory/app 0.34.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.
@@ -8,6 +8,7 @@
8
8
  // Sections gate on the same `available` probes the navbar used, so a system that
9
9
  // the backend has turned off simply doesn't appear here.
10
10
  const ui = useUiStore()
11
+ const auth = useAuthStore()
11
12
  const github = useGitHubStore()
12
13
  const slack = useSlackStore()
13
14
  const documents = useDocumentsStore()
@@ -256,6 +257,18 @@ const groups = computed<IntegrationGroup[]>(() => {
256
257
  onClick: () => go(() => ui.openProviderConnection('runner-pool')),
257
258
  })
258
259
  }
260
+ // Local-mode-only: the warm-container pool + checkout reuse for the local runner. Shown
261
+ // only on the local-mode service (the controller 503s elsewhere, and `auth.localMode`
262
+ // is set from /auth/config).
263
+ if (auth.localMode?.enabled) {
264
+ infra.push({
265
+ key: 'local-mode',
266
+ icon: 'i-lucide-container',
267
+ label: 'Local mode',
268
+ description: 'Warm container pool + per-repo checkout reuse for the local runner.',
269
+ onClick: () => go(ui.openLocalModeSettings),
270
+ })
271
+ }
259
272
  if (infra.length) out.push({ title: 'Infrastructure', items: infra })
260
273
 
261
274
  return out
@@ -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>
@@ -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),
@@ -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 />
@@ -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
@@ -116,6 +116,9 @@ export const useUiStore = defineStore('ui', () => {
116
116
  const vendorCredentialsOpen = ref(false)
117
117
  // Per-user settings panel: the signed-in user's own-machine local model runners.
118
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)
119
122
  // The Sandbox (parallel prompt/model testing) surface — an opt-in, on-demand window.
120
123
  const sandboxOpen = ref(false)
121
124
  const userSecretsOpen = ref(false)
@@ -432,6 +435,13 @@ export const useUiStore = defineStore('ui', () => {
432
435
  function closeLocalModels() {
433
436
  localModelsOpen.value = false
434
437
  }
438
+ function openLocalModeSettings() {
439
+ cameFromIntegrations.value = false
440
+ localModeSettingsOpen.value = true
441
+ }
442
+ function closeLocalModeSettings() {
443
+ localModeSettingsOpen.value = false
444
+ }
435
445
  function openSandbox() {
436
446
  sandboxOpen.value = true
437
447
  }
@@ -580,6 +590,7 @@ export const useUiStore = defineStore('ui', () => {
580
590
  modelConfigOpen,
581
591
  vendorCredentialsOpen,
582
592
  localModelsOpen,
593
+ localModeSettingsOpen,
583
594
  sandboxOpen,
584
595
  userSecretsOpen,
585
596
  openRouterOpen,
@@ -650,6 +661,8 @@ export const useUiStore = defineStore('ui', () => {
650
661
  closeVendorCredentials,
651
662
  openLocalModels,
652
663
  closeLocalModels,
664
+ openLocalModeSettings,
665
+ closeLocalModeSettings,
653
666
  openSandbox,
654
667
  closeSandbox,
655
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
4
4
  "description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
5
5
  "repository": {
6
6
  "type": "git",