@cat-factory/app 0.49.2 → 0.50.1

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.
@@ -1,25 +1,24 @@
1
1
  <script setup lang="ts">
2
- // The single tabbed "Infrastructure" window. It merges what used to be two separate
3
- // Integrations-Hub entries the self-hosted runner pool (container agents) and the
4
- // ephemeral-environment provider (Tester environments) into one surface, because the
5
- // same custom pool typically backs both jobs, so configuring them together reflects
6
- // reality. Each provider gets its own tab (ProviderConnectionTab); a tab whose backend
7
- // integration is disabled (503) simply doesn't render.
8
- //
9
- // The local-mode delegation toggles are cross-cutting (one per concern), so they live at
10
- // the TOP of the window rather than buried in one tab — removing the old awkward
11
- // cross-link hint that pointed from the runner-pool screen back to the env screen.
2
+ // The single tabbed "Infrastructure" window now a TOP-LEVEL navbar destination (no longer
3
+ // reached through the Integrations hub). Two topical tabs:
4
+ // - "Agent containers"where repo-operating agent containers run. Shows the execution
5
+ // backend selector, the runner-pool connection (ProviderConnectionTab), and in local
6
+ // mode the warm-container-pool + checkout-reuse settings (the local agent-container
7
+ // runtime, folded in from the former LocalModeSettingsPanel).
8
+ // - "Test environments" — where the Tester's ephemeral environments run. Shows the test-env
9
+ // backend selector and the environment-provider connection.
10
+ // Local-specific affordances render inline, gated on `auth.localMode?.enabled`. A tab whose
11
+ // backend integration is disabled (503) simply doesn't render.
12
12
  import { computed, ref, watch } from 'vue'
13
13
  import type { ProviderConnectionKind } from '~/types/providerConnections'
14
- import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
15
14
  import ProviderConnectionTab from '~/components/settings/ProviderConnectionTab.vue'
15
+ import ExecutionBackendSelector from '~/components/settings/ExecutionBackendSelector.vue'
16
+ import LocalContainerPoolSettings from '~/components/settings/LocalContainerPoolSettings.vue'
16
17
 
17
18
  const { t } = useI18n()
18
19
  const ui = useUiStore()
19
20
  const store = useProviderConnectionsStore()
20
21
  const auth = useAuthStore()
21
- const settings = useWorkspaceSettingsStore()
22
- const toast = useToast()
23
22
 
24
23
  const open = computed({
25
24
  get: () => ui.infrastructureOpen,
@@ -27,7 +26,8 @@ const open = computed({
27
26
  if (!v) ui.closeProviderConnection()
28
27
  },
29
28
  })
30
- const back = useIntegrationBack(open)
29
+
30
+ const isLocal = computed(() => auth.localMode?.enabled === true)
31
31
 
32
32
  // Each concern gates on its own availability probe; an unavailable tab isn't offered.
33
33
  const agentsAvailable = computed(() => store.isAvailable('runner-pool'))
@@ -38,7 +38,7 @@ const tabs = computed(() => {
38
38
  if (agentsAvailable.value)
39
39
  out.push({
40
40
  value: 'runner-pool',
41
- label: t('settings.providerConnection.tabs.containerAgents'),
41
+ label: t('settings.providerConnection.tabs.agentContainers'),
42
42
  icon: 'i-lucide-server-cog',
43
43
  slot: 'runner-pool',
44
44
  })
@@ -54,8 +54,8 @@ const tabs = computed(() => {
54
54
 
55
55
  const activeTab = ref<ProviderConnectionKind>(ui.infrastructureTab)
56
56
 
57
- // Honour the deep-linked tab each time the window opens (e.g. the banner's per-kind
58
- // "Configure…" button), falling back to the first available tab if the requested one is off.
57
+ // Honour the deep-linked tab each time the window opens, falling back to the first available
58
+ // tab if the requested one is off.
59
59
  watch(
60
60
  open,
61
61
  (isOpen) => {
@@ -69,9 +69,7 @@ watch(
69
69
  )
70
70
  // When availability resolves after open, re-pin onto a valid tab — but keep honouring the
71
71
  // deep-linked request. The two availability probes resolve independently, so `tabs` can pass
72
- // through a transient single-tab list (e.g. the runner-pool probe lands a tick before the
73
- // environment one). We must NOT let that transient list steal focus from a still-loading
74
- // requested tab, so only fall back to the first tab once loading has fully settled.
72
+ // through a transient single-tab list; only fall back to the first tab once loading settled.
75
73
  watch([tabs, () => store.loaded], () => {
76
74
  const list = tabs.value
77
75
  if (list.some((x) => x.value === activeTab.value)) return
@@ -82,38 +80,6 @@ watch([tabs, () => store.loaded], () => {
82
80
  activeTab.value = list[0]!.value
83
81
  }
84
82
  })
85
-
86
- // --- Local-mode infrastructure delegation (cross-cutting; shown only in local mode) ---
87
- // In local mode this is where a developer chooses, per workspace, whether to run on this
88
- // machine (host Docker for agents, in-container docker-compose for the Tester) or delegate
89
- // to an external service. Each toggle is enabled only once its provider is registered.
90
- const isLocal = computed(() => auth.localMode?.enabled === true)
91
- const runnerPoolRegistered = computed(() => !!store.connectionFor('runner-pool'))
92
- const envRegistered = computed(() => !!store.connectionFor('environment'))
93
- const savingDelegation = ref(false)
94
-
95
- async function setDelegation(patch: {
96
- delegateAgentsToRunnerPool?: boolean
97
- delegateTestEnvToProvider?: boolean
98
- }) {
99
- savingDelegation.value = true
100
- try {
101
- await settings.update(patch)
102
- } catch (e) {
103
- toast.add({
104
- title: t('settings.providerConnection.delegation.updateFailed'),
105
- description: e instanceof Error ? e.message : String(e),
106
- icon: 'i-lucide-triangle-alert',
107
- color: 'error',
108
- })
109
- } finally {
110
- savingDelegation.value = false
111
- }
112
- }
113
-
114
- function selectTab(kind: ProviderConnectionKind) {
115
- activeTab.value = kind
116
- }
117
83
  </script>
118
84
 
119
85
  <template>
@@ -122,80 +88,8 @@ function selectTab(kind: ProviderConnectionKind) {
122
88
  :title="t('settings.providerConnection.windowTitle')"
123
89
  :ui="{ content: 'max-w-xl' }"
124
90
  >
125
- <template #title>
126
- <IntegrationBackTitle :title="t('settings.providerConnection.windowTitle')" @back="back" />
127
- </template>
128
91
  <template #body>
129
92
  <div class="space-y-4">
130
- <!-- Local-mode delegation: the local-vs-external choice for BOTH container agents
131
- AND the Tester's ephemeral environments, made once here at the top. -->
132
- <section
133
- v-if="isLocal"
134
- class="space-y-3 rounded-lg border border-slate-700 bg-slate-900/40 p-3"
135
- >
136
- <div>
137
- <h3 class="text-sm font-semibold text-slate-200">
138
- {{ t('settings.providerConnection.delegation.title') }}
139
- </h3>
140
- <p class="mt-1 text-[11px] text-slate-400">
141
- {{ t('settings.providerConnection.delegation.intro') }}
142
- </p>
143
- </div>
144
-
145
- <!-- Container agents → self-hosted runner pool -->
146
- <div class="space-y-1">
147
- <label class="flex items-center gap-2">
148
- <USwitch
149
- size="sm"
150
- :model-value="settings.settings.delegateAgentsToRunnerPool"
151
- :disabled="savingDelegation || !runnerPoolRegistered"
152
- @update:model-value="(v) => setDelegation({ delegateAgentsToRunnerPool: v })"
153
- />
154
- <span class="text-sm text-slate-200">
155
- {{ t('settings.providerConnection.delegation.agentsToggle') }}
156
- </span>
157
- </label>
158
- <p class="pl-9 text-[11px] text-slate-400">
159
- {{ t('settings.providerConnection.delegation.agentsHint') }}
160
- <template v-if="!runnerPoolRegistered">
161
- <i18n-t
162
- keypath="settings.providerConnection.delegation.registerPoolPrompt"
163
- tag="span"
164
- scope="global"
165
- >
166
- <template #link>
167
- <button
168
- type="button"
169
- class="text-sky-400 underline underline-offset-2 hover:text-sky-300"
170
- @click="selectTab('runner-pool')"
171
- >
172
- {{ t('settings.providerConnection.delegation.registerPoolLink') }}
173
- </button>
174
- </template>
175
- </i18n-t>
176
- </template>
177
- </p>
178
- </div>
179
-
180
- <!-- Tester environments → environment provider -->
181
- <div class="space-y-1">
182
- <label class="flex items-center gap-2">
183
- <USwitch
184
- size="sm"
185
- :model-value="settings.settings.delegateTestEnvToProvider"
186
- :disabled="savingDelegation || !envRegistered"
187
- @update:model-value="(v) => setDelegation({ delegateTestEnvToProvider: v })"
188
- />
189
- <span class="text-sm text-slate-200">
190
- {{ t('settings.providerConnection.delegation.envToggle') }}
191
- </span>
192
- </label>
193
- <p class="pl-9 text-[11px] text-slate-400">
194
- {{ t('settings.providerConnection.delegation.envHint') }}
195
- </p>
196
- </div>
197
- </section>
198
-
199
93
  <UTabs
200
94
  v-if="tabs.length"
201
95
  v-model="activeTab"
@@ -205,10 +99,26 @@ function selectTab(kind: ProviderConnectionKind) {
205
99
  data-testid="infrastructure-tabs"
206
100
  >
207
101
  <template #runner-pool>
208
- <ProviderConnectionTab kind="runner-pool" />
102
+ <div class="space-y-4">
103
+ <!-- Where agent containers run (writable in local mode; read-only elsewhere). -->
104
+ <ExecutionBackendSelector axis="execution" />
105
+ <ProviderConnectionTab kind="runner-pool" />
106
+ <!-- Local mode: the warm-pool + checkout reuse ARE the host agent-container
107
+ runtime, so they live here rather than in a separate menu. -->
108
+ <section v-if="isLocal" class="border-t border-slate-800 pt-4">
109
+ <h3 class="mb-3 text-sm font-semibold text-slate-200">
110
+ {{ t('settings.localMode.title') }}
111
+ </h3>
112
+ <LocalContainerPoolSettings />
113
+ </section>
114
+ </div>
209
115
  </template>
210
116
  <template #environment>
211
- <ProviderConnectionTab kind="environment" />
117
+ <div class="space-y-4">
118
+ <!-- Where Tester environments run (writable in local mode; read-only elsewhere). -->
119
+ <ExecutionBackendSelector axis="testEnv" />
120
+ <ProviderConnectionTab kind="environment" />
121
+ </div>
212
122
  </template>
213
123
  </UTabs>
214
124
 
@@ -0,0 +1,181 @@
1
+ <script setup lang="ts">
2
+ // Local-mode-only: the warm-container pool + per-repo checkout reuse. These are a
3
+ // per-DEPLOYMENT singleton stored in the DB (they replaced the LOCAL_POOL_* / HARNESS_* env
4
+ // vars), so a developer tunes them here instead of editing .env. The warm pool keeps idle
5
+ // harness containers ready and re-leases one (preferring repo affinity) to each run — far
6
+ // faster startup than a cold container per run. Saving applies the new sizing to the running
7
+ // service immediately (live resize, no restart); in-flight runs keep the container they hold,
8
+ // and the checkout config applies to containers started after the save.
9
+ //
10
+ // Previously a standalone modal (LocalModeSettingsPanel); now folded into the Agent-containers
11
+ // tab of the Infrastructure window, since the warm pool IS the local agent-container runtime.
12
+ import { reactive, ref, watch } from 'vue'
13
+
14
+ const { t } = useI18n()
15
+ const store = useLocalSettingsStore()
16
+ const toast = useToast()
17
+
18
+ const saving = ref(false)
19
+
20
+ // Editable draft. `idleMinutes` and `cleanKeep` are friendlier renderings of the stored
21
+ // `pool.idleTtlMs` (ms) and `checkout.cleanKeep` (string[]).
22
+ const draft = reactive({
23
+ size: 0,
24
+ minWarm: 0,
25
+ max: null as number | null,
26
+ idleMinutes: 10,
27
+ workspaceRoot: '/workspace',
28
+ cleanKeep: 'node_modules,.venv,target,.gradle,.pnpm-store',
29
+ })
30
+
31
+ function syncDraft() {
32
+ const s = store.settings
33
+ if (!s) return
34
+ draft.size = s.pool.size
35
+ draft.minWarm = s.pool.minWarm
36
+ draft.max = s.pool.max
37
+ draft.idleMinutes = Math.round(s.pool.idleTtlMs / 60_000)
38
+ draft.workspaceRoot = s.checkout.workspaceRoot
39
+ draft.cleanKeep = s.checkout.cleanKeep.join(',')
40
+ }
41
+
42
+ // Load + hydrate the draft on mount (the tab only mounts in local mode).
43
+ void store.load().then(syncDraft)
44
+ watch(() => store.settings, syncDraft)
45
+
46
+ async function save() {
47
+ const cleanKeep = draft.cleanKeep
48
+ .split(',')
49
+ .map((s) => s.trim())
50
+ .filter(Boolean)
51
+ saving.value = true
52
+ try {
53
+ await store.save({
54
+ pool: {
55
+ size: Math.max(0, Math.floor(draft.size)),
56
+ minWarm: Math.max(0, Math.floor(draft.minWarm)),
57
+ max: draft.max == null ? null : Math.max(0, Math.floor(draft.max)),
58
+ idleTtlMs: Math.max(0, Math.floor(draft.idleMinutes * 60_000)),
59
+ },
60
+ checkout: { workspaceRoot: draft.workspaceRoot.trim() || '/workspace', cleanKeep },
61
+ })
62
+ toast.add({
63
+ title: t('settings.localMode.toast.saved'),
64
+ icon: 'i-lucide-check',
65
+ color: 'success',
66
+ })
67
+ } catch (e) {
68
+ toast.add({
69
+ title: t('settings.localMode.toast.saveFailed'),
70
+ description: e instanceof Error ? e.message : String(e),
71
+ icon: 'i-lucide-triangle-alert',
72
+ color: 'error',
73
+ })
74
+ } finally {
75
+ saving.value = false
76
+ }
77
+ }
78
+ </script>
79
+
80
+ <template>
81
+ <div class="space-y-6" data-testid="local-container-pool-settings">
82
+ <i18n-t
83
+ keypath="settings.localMode.intro"
84
+ tag="p"
85
+ class="text-xs text-slate-400"
86
+ scope="global"
87
+ >
88
+ <template #poolVars>
89
+ <code>LOCAL_POOL_*</code>
90
+ </template>
91
+ <template #harnessVars>
92
+ <code>HARNESS_*</code>
93
+ </template>
94
+ </i18n-t>
95
+
96
+ <!-- Warm container pool -->
97
+ <section class="space-y-3">
98
+ <div>
99
+ <h4 class="text-sm font-semibold text-slate-200">
100
+ {{ t('settings.localMode.pool.heading') }}
101
+ </h4>
102
+ <i18n-t
103
+ keypath="settings.localMode.pool.description"
104
+ tag="p"
105
+ class="text-[11px] text-slate-400"
106
+ scope="global"
107
+ >
108
+ <template #appleContainer>
109
+ <code>container</code>
110
+ </template>
111
+ </i18n-t>
112
+ </div>
113
+ <div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
114
+ <UFormField
115
+ :label="t('settings.localMode.pool.size.label')"
116
+ :help="t('settings.localMode.pool.size.help')"
117
+ >
118
+ <UInput v-model.number="draft.size" type="number" :min="0" size="sm" />
119
+ </UFormField>
120
+ <UFormField
121
+ :label="t('settings.localMode.pool.minWarm.label')"
122
+ :help="t('settings.localMode.pool.minWarm.help')"
123
+ >
124
+ <UInput v-model.number="draft.minWarm" type="number" :min="0" size="sm" />
125
+ </UFormField>
126
+ <UFormField
127
+ :label="t('settings.localMode.pool.max.label')"
128
+ :help="t('settings.localMode.pool.max.help')"
129
+ >
130
+ <UInput
131
+ v-model.number="draft.max"
132
+ type="number"
133
+ :min="0"
134
+ size="sm"
135
+ :placeholder="t('settings.localMode.pool.max.placeholder')"
136
+ />
137
+ </UFormField>
138
+ <UFormField
139
+ :label="t('settings.localMode.pool.idleTimeout.label')"
140
+ :help="t('settings.localMode.pool.idleTimeout.help')"
141
+ >
142
+ <UInput v-model.number="draft.idleMinutes" type="number" :min="0" size="sm" />
143
+ </UFormField>
144
+ </div>
145
+ </section>
146
+
147
+ <!-- Checkout reuse -->
148
+ <section class="space-y-3 border-t border-slate-800 pt-6">
149
+ <div>
150
+ <h4 class="text-sm font-semibold text-slate-200">
151
+ {{ t('settings.localMode.checkout.heading') }}
152
+ </h4>
153
+ <p class="text-[11px] text-slate-400">
154
+ {{ t('settings.localMode.checkout.description') }}
155
+ </p>
156
+ </div>
157
+ <UFormField
158
+ :label="t('settings.localMode.checkout.workspaceRoot.label')"
159
+ :help="t('settings.localMode.checkout.workspaceRoot.help')"
160
+ >
161
+ <UInput v-model="draft.workspaceRoot" size="sm" placeholder="/workspace" />
162
+ </UFormField>
163
+ <UFormField
164
+ :label="t('settings.localMode.checkout.cleanKeep.label')"
165
+ :help="t('settings.localMode.checkout.cleanKeep.help')"
166
+ >
167
+ <UInput
168
+ v-model="draft.cleanKeep"
169
+ size="sm"
170
+ placeholder="node_modules,.venv,target,.gradle,.pnpm-store"
171
+ />
172
+ </UFormField>
173
+ </section>
174
+
175
+ <div class="flex justify-end">
176
+ <UButton color="primary" icon="i-lucide-save" :loading="saving" @click="save">
177
+ {{ t('common.save') }}
178
+ </UButton>
179
+ </div>
180
+ </div>
181
+ </template>
@@ -440,18 +440,31 @@ function fieldHelp(key: string): string | undefined {
440
440
  </div>
441
441
  </div>
442
442
 
443
- <!-- MANIFEST-driven provider: the full in-app manifest editor. -->
444
- <ProviderManifestEditor
443
+ <!-- MANIFEST-driven provider: the raw JSON manifest editor. Collapsed by default — it's
444
+ the advanced path, needed ONLY to integrate a custom API-based scheduler. The common
445
+ backends (local Docker, Cloudflare Containers, Kubernetes) don't need it. -->
446
+ <details
445
447
  v-else
446
- :kind="kind"
447
- :saved-manifest="descriptor.savedManifest"
448
- :connected="!!connection"
449
- :supports-test="descriptor.supportsTest"
450
- :testing="testing"
451
- :busy="busy"
452
- :test-result="testResult"
453
- @test="testManifest"
454
- @save="saveManifest"
455
- />
448
+ class="rounded-lg border border-slate-700 bg-slate-900/40 p-3"
449
+ :open="!!connection"
450
+ >
451
+ <summary class="cursor-pointer text-sm font-medium text-slate-200">
452
+ {{ t('settings.providerConnection.advancedManifest.summary') }}
453
+ </summary>
454
+ <p class="mt-2 mb-3 text-[11px] text-slate-400">
455
+ {{ t('settings.providerConnection.advancedManifest.intro') }}
456
+ </p>
457
+ <ProviderManifestEditor
458
+ :kind="kind"
459
+ :saved-manifest="descriptor.savedManifest"
460
+ :connected="!!connection"
461
+ :supports-test="descriptor.supportsTest"
462
+ :testing="testing"
463
+ :busy="busy"
464
+ :test-result="testResult"
465
+ @test="testManifest"
466
+ @save="saveManifest"
467
+ />
468
+ </details>
456
469
  </div>
457
470
  </template>
@@ -79,9 +79,6 @@ const ModelConfigurationPanel = defineAsyncComponent(
79
79
  const LocalModelEndpointsPanel = defineAsyncComponent(
80
80
  () => import('~/components/settings/LocalModelEndpointsPanel.vue'),
81
81
  )
82
- const LocalModeSettingsPanel = defineAsyncComponent(
83
- () => import('~/components/settings/LocalModeSettingsPanel.vue'),
84
- )
85
82
  const SandboxPanel = defineAsyncComponent(() => import('~/components/sandbox/SandboxPanel.vue'))
86
83
  const UserSecretsSection = defineAsyncComponent(
87
84
  () => import('~/components/settings/UserSecretsSection.vue'),
@@ -287,7 +284,6 @@ watch(
287
284
  <InfrastructureWindow v-if="ui.infrastructureOpen" />
288
285
  <ModelConfigurationPanel v-if="ui.modelConfigOpen" />
289
286
  <LocalModelEndpointsPanel v-if="ui.localModelsOpen" />
290
- <LocalModeSettingsPanel v-if="ui.localModeSettingsOpen" />
291
287
  <SandboxPanel v-if="ui.sandboxOpen" />
292
288
  <UserSecretsSection v-if="ui.userSecretsOpen" />
293
289
  <OpenRouterCatalogPanel v-if="ui.openRouterOpen" />
@@ -1,4 +1,4 @@
1
- import type { LocalModeConfig } from '@cat-factory/contracts'
1
+ import type { InfrastructureCapabilities, LocalModeConfig } from '@cat-factory/contracts'
2
2
  import { defineStore } from 'pinia'
3
3
  import { computed, ref } from 'vue'
4
4
  import type { AuthUser } from '~/types/domain'
@@ -31,6 +31,13 @@ export const useAuthStore = defineStore(
31
31
  * setup banner). Null on every other facade.
32
32
  */
33
33
  const localMode = ref<LocalModeConfig | null>(null)
34
+ /**
35
+ * The deployment's infrastructure execution backends (which agent-container runtime + test
36
+ * environment options exist, and the deployment default active one). Drives the
37
+ * Infrastructure window's backend selector. Null until the auth handshake resolves / on a
38
+ * facade that doesn't report it.
39
+ */
40
+ const infrastructure = ref<InfrastructureCapabilities | null>(null)
34
41
  /**
35
42
  * Local mode only: the source-control provider the user last chose to sign in with
36
43
  * (its PAT lives server-side in env — this is just the non-secret choice). Persisted, so
@@ -72,6 +79,7 @@ export const useAuthStore = defineStore(
72
79
  required.value = config.enabled
73
80
  if (config.providers) providers.value = config.providers
74
81
  localMode.value = config.localMode ?? null
82
+ infrastructure.value = config.infrastructure ?? null
75
83
  } catch {
76
84
  // Backend unreachable — let the board's own error UI handle it.
77
85
  required.value = false
@@ -222,6 +230,7 @@ export const useAuthStore = defineStore(
222
230
  required,
223
231
  providers,
224
232
  localMode,
233
+ infrastructure,
225
234
  autoLoginProvider,
226
235
  ready,
227
236
  isAuthenticated,
package/app/stores/ui.ts CHANGED
@@ -125,11 +125,12 @@ export const useUiStore = defineStore('ui', () => {
125
125
  // today, pluggable). NB: distinct from `observabilityInstanceId` below, which is the
126
126
  // LLM per-call observability panel.
127
127
  const observabilityConnectionOpen = ref(false)
128
- // The single tabbed Infrastructure window (ephemeral-environment provider + self-hosted
129
- // runner pool the same custom pool typically backs both jobs, so they're configured
130
- // together). `infrastructureOpen` is the modal flag; `infrastructureTab` selects which
131
- // provider's tab is shown. `openProviderConnection(kind)` stays the entry API but now
132
- // selects the matching tab instead of mounting a per-kind standalone panel.
128
+ // The single tabbed Infrastructure window — a TOP-LEVEL navbar destination (no longer
129
+ // reached via the Integrations hub). Two topical tabs: "Agent containers" (the execution
130
+ // backend + self-hosted runner pool, plus the local-mode warm pool/checkout) and "Test
131
+ // environments" (the ephemeral-environment provider). `infrastructureOpen` is the modal
132
+ // flag; `infrastructureTab` selects the tab. `openInfrastructure()` is the navbar entry;
133
+ // `openProviderConnection(kind)` remains for deep-links (a banner's "Configure…" button).
133
134
  const infrastructureOpen = ref(false)
134
135
  const infrastructureTab = ref<'environment' | 'runner-pool'>('runner-pool')
135
136
  const modelConfigOpen = ref(false)
@@ -140,9 +141,6 @@ export const useUiStore = defineStore('ui', () => {
140
141
  const vendorCredentialsTab = ref('pool')
141
142
  // Per-user settings panel: the signed-in user's own-machine local model runners.
142
143
  const localModelsOpen = ref(false)
143
- // Local-mode-only settings panel: the warm-container pool sizing + per-repo checkout reuse
144
- // (a per-deployment singleton that replaced the LOCAL_POOL_* / HARNESS_* env vars).
145
- const localModeSettingsOpen = ref(false)
146
144
  // The Sandbox (parallel prompt/model testing) surface — an opt-in, on-demand window.
147
145
  const sandboxOpen = ref(false)
148
146
  const userSecretsOpen = ref(false)
@@ -479,6 +477,13 @@ export const useUiStore = defineStore('ui', () => {
479
477
  function closeObservabilityConnection() {
480
478
  observabilityConnectionOpen.value = false
481
479
  }
480
+ // Top-level navbar entry into the Infrastructure window. No hub-return marker (it isn't
481
+ // reached from the Integrations hub), so the window shows no "Back to Integrations" control.
482
+ function openInfrastructure(tab: 'environment' | 'runner-pool' = 'runner-pool') {
483
+ resetHubReturn()
484
+ infrastructureTab.value = tab
485
+ infrastructureOpen.value = true
486
+ }
482
487
  function openProviderConnection(kind: 'environment' | 'runner-pool') {
483
488
  resetHubReturn()
484
489
  infrastructureTab.value = kind
@@ -511,13 +516,6 @@ export const useUiStore = defineStore('ui', () => {
511
516
  function closeLocalModels() {
512
517
  localModelsOpen.value = false
513
518
  }
514
- function openLocalModeSettings() {
515
- resetHubReturn()
516
- localModeSettingsOpen.value = true
517
- }
518
- function closeLocalModeSettings() {
519
- localModeSettingsOpen.value = false
520
- }
521
519
  function openSandbox() {
522
520
  sandboxOpen.value = true
523
521
  }
@@ -669,11 +667,11 @@ export const useUiStore = defineStore('ui', () => {
669
667
  observabilityConnectionOpen,
670
668
  infrastructureOpen,
671
669
  infrastructureTab,
670
+ openInfrastructure,
672
671
  modelConfigOpen,
673
672
  vendorCredentialsOpen,
674
673
  vendorCredentialsTab,
675
674
  localModelsOpen,
676
- localModeSettingsOpen,
677
675
  sandboxOpen,
678
676
  userSecretsOpen,
679
677
  openRouterOpen,
@@ -754,8 +752,6 @@ export const useUiStore = defineStore('ui', () => {
754
752
  closeVendorCredentials,
755
753
  openLocalModels,
756
754
  closeLocalModels,
757
- openLocalModeSettings,
758
- closeLocalModeSettings,
759
755
  openSandbox,
760
756
  closeSandbox,
761
757
  openUserSecrets,