@cat-factory/app 0.121.0 → 0.121.2

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.
@@ -0,0 +1,810 @@
1
+ import { ref } from 'vue'
2
+ import type { DocumentSourceKind, InfraSetupArea, TaskSourceKind } from '~/types/domain'
3
+ import type { PendingContext } from '~/composables/useContextLinking'
4
+
5
+ /** Values used to seed the add-task form when it is opened from another surface. */
6
+ export interface AddTaskPrefill {
7
+ title?: string
8
+ description?: string
9
+ /** Context items staged on the new task (e.g. the source issue), linked once created. */
10
+ context?: PendingContext[]
11
+ }
12
+
13
+ /**
14
+ * Non-secret `local-k3s` connection values captured from the `cat-factory k3s` CLI deep-link
15
+ * (`?infraSetup=local-k3s&…`). Mirrors the params `buildK3sSetupUrl` emits (the CLI-side
16
+ * `k3s-handler.ts`); the ServiceAccount token is intentionally absent — the user pastes it.
17
+ */
18
+ export interface K3sSetupPrefill {
19
+ label: string
20
+ apiServerUrl: string
21
+ namespaceTemplate: string
22
+ hostTemplate: string
23
+ // Absent when the link omitted the param, so the form keeps its engine default rather than
24
+ // forcing verification back on (which would break a self-signed local cluster).
25
+ insecureSkipTlsVerify?: boolean
26
+ }
27
+
28
+ /**
29
+ * The modal / panel slice of the UI store: every open-close flag for the dozens of modals,
30
+ * panels and hubs (document + task import, bootstrap, integrations, workspace/account settings,
31
+ * infrastructure, vendor credentials, the startup health advisories, the AI-onboarding surfaces,
32
+ * …), their deep-link params, and the hub came-from markers. Split out of the navigation +
33
+ * result-view state per refactoring candidate #4 so the god-object's modal churn is contained to
34
+ * one place. Composed into {@link useUiStore} with the same public names, so consumers are
35
+ * unchanged.
36
+ */
37
+ export function createUiModals() {
38
+ const builderOpen = ref(false)
39
+ // Pipeline-health startup advisory: lists invalid pipelines (delete / reseed) + built-ins
40
+ // with a newer catalog version (reseed). `pipelineHealthSeen` gates auto-open to once per
41
+ // session so it does not re-pop on every snapshot re-hydration.
42
+ const pipelineHealthOpen = ref(false)
43
+ const pipelineHealthSeen = ref(false)
44
+ // Merge-preset health startup advisory: lists built-ins with a newer catalog version (reseed)
45
+ // and new built-in presets the workspace can add. `riskPolicyHealthSeen` gates auto-open to
46
+ // once per session so it does not re-pop on every snapshot re-hydration (mirrors pipelines).
47
+ const riskPolicyHealthOpen = ref(false)
48
+ const riskPolicyHealthSeen = ref(false)
49
+ // Model-preset health startup advisory: lists built-ins with a newer catalog version (reseed)
50
+ // and new built-in presets the workspace can add. `modelPresetHealthSeen` gates auto-open to
51
+ // once per session so it does not re-pop on every snapshot re-hydration (mirrors pipelines).
52
+ const modelPresetHealthOpen = ref(false)
53
+ const modelPresetHealthSeen = ref(false)
54
+ const decisionContext = ref<{ instanceId: string; decisionId: string } | null>(null)
55
+
56
+ // Document-source integration modals, keyed by source. `documentImport` and
57
+ // `spawnPreview` carry an optional target frame, so structure spawned from a
58
+ // frame's inspector lands inside that frame rather than creating new top-level
59
+ // frames. `documentConnect` carries the source whose connect form to show;
60
+ // `documentImport`'s source may be null to let the modal pick a connected one.
61
+ const documentConnect = ref<{ source: DocumentSourceKind } | null>(null)
62
+ const documentImport = ref<{
63
+ source: DocumentSourceKind | null
64
+ targetFrameId: string | null
65
+ } | null>(null)
66
+ // The workspace+DocKind template / exemplar management modal (WS1). A single boolean —
67
+ // it manages every kind's links in one place.
68
+ const documentTemplates = ref(false)
69
+ const spawnPreview = ref<{
70
+ source: DocumentSourceKind
71
+ externalId: string
72
+ targetFrameId: string | null
73
+ } | null>(null)
74
+
75
+ // Task-source integration modals, keyed by source. `taskConnect` carries the
76
+ // source whose connect form to show; `taskImport`'s source may be null to let
77
+ // the modal pick a connected one (there is no spawn target — issues are linked
78
+ // to a block for context, not expanded into structure).
79
+ const taskConnect = ref<{ source: TaskSourceKind } | null>(null)
80
+ // `containerId` (a service frame) scopes the modal: it preselects that frame as
81
+ // the create-in target AND scopes the issue search to the frame's linked repo.
82
+ // Null → the unscoped "import an issue" surface (workspace-wide search).
83
+ const taskImport = ref<{ source: TaskSourceKind | null; containerId: string | null } | null>(null)
84
+
85
+ // Add-task modal: the container (service frame or module) a new task is being
86
+ // added to, or null when closed. The user types the title + description; nothing
87
+ // is launched until they explicitly start the created task.
88
+ const addTaskContainerId = ref<string | null>(null)
89
+ // Optional values to seed the add-task form with when it is opened from another
90
+ // surface (e.g. "create task from issue" prefills the title + stages the issue as
91
+ // linked context). The user still confirms pipeline / preset before adding.
92
+ const addTaskPrefill = ref<AddTaskPrefill | null>(null)
93
+
94
+ // Add-recurring-pipeline modal: the service frame a new recurring pipeline is
95
+ // being added to, or null when closed (mirrors the add-task flow — a button on
96
+ // the frame opens it, scoped to that frame).
97
+ const addRecurringFrameId = ref<string | null>(null)
98
+
99
+ // Create-initiative modal: the service frame a new initiative is being created
100
+ // under, or null when closed (mirrors the add-task flow).
101
+ const createInitiativeFrameId = ref<string | null>(null)
102
+
103
+ // Repo-bootstrap modal (manage reference architectures + launch a bootstrap).
104
+ const bootstrapOpen = ref(false)
105
+
106
+ // "Add a service from an existing GitHub repo" modal (no bootstrap run).
107
+ const addServiceOpen = ref(false)
108
+
109
+ // GitHub integration panel (connection management + repo/PR/issue browsing).
110
+ const githubOpen = ref(false)
111
+
112
+ // Slack integration panel (connect the account's Slack + per-workspace routing).
113
+ const slackOpen = ref(false)
114
+
115
+ // Prompt-fragment library panel (manage the board's best-practice catalog +
116
+ // linked guideline repos; ADR 0006).
117
+ const fragmentLibraryOpen = ref(false)
118
+
119
+ // Command bar (⌘K) — searchable launcher for every navbar action.
120
+ const commandBarOpen = ref(false)
121
+
122
+ // Keyboard-shortcuts cheatsheet (?) — a modal listing every global shortcut.
123
+ const shortcutsHelpOpen = ref(false)
124
+
125
+ // Mobile navigation drawer: on compact (< lg) viewports the SideBar is an
126
+ // off-canvas drawer toggled by a hamburger; on lg+ it is a static aside and this
127
+ // flag is ignored. Closed on any nav action so the board is revealed immediately.
128
+ const mobileNavOpen = ref(false)
129
+
130
+ // Integrations hub: a single modal listing every external system the workspace
131
+ // can enable/link (GitHub, Slack, document + task sources, Datadog, LLM vendors,
132
+ // local runners, OpenRouter). Replaces the per-integration navbar buttons; each
133
+ // row inside it opens that integration's own panel via the handlers below.
134
+ const integrationsOpen = ref(false)
135
+ // True while an integration's own panel is showing AND it was reached from the hub
136
+ // (not the command bar, sidebar, a banner or an inspector link). Drives the "Back to
137
+ // Integrations" control those panels render: it only offers a return path when there
138
+ // is one. Every direct `open*` below resets it; `openFromIntegrations` sets it.
139
+ const cameFromIntegrations = ref(false)
140
+
141
+ // Personal "My setup" hub: a user-scoped sibling of the Integrations hub, listing the
142
+ // signed-in user's own connections (GitHub PAT, local runners, personal subscriptions)
143
+ // separated out of the workspace-scoped Integrations hub. `cameFromPersonal` is the
144
+ // symmetric came-from marker, so a panel reached from here renders a "Back to My setup"
145
+ // control instead of "Back to Integrations".
146
+ const personalSetupOpen = ref(false)
147
+ const cameFromPersonal = ref(false)
148
+
149
+ // Workspace-settings modal: a single tabbed window gathering the workspace-wide
150
+ // config (workspace / merge thresholds / issue writeback / service best practices).
151
+ // `workspaceSettingsTab` lets other surfaces deep-link straight to a tab.
152
+ const workspaceSettingsOpen = ref(false)
153
+ const workspaceSettingsTab = ref('workspace')
154
+ // Account-settings modal: a single tabbed window for the per-account configuration —
155
+ // the team panel (members + roles + invitations + email sender + account API keys,
156
+ // `AccountTeamSettings`) and the account-tier prompt-fragment library. Account-scoped
157
+ // (distinct from workspace settings). `accountSettingsTab` lets other surfaces deep-link
158
+ // straight to a tab.
159
+ const accountSettingsOpen = ref(false)
160
+ const accountSettingsTab = ref('team')
161
+ // A one-shot deep-link anchor: when a surface opens account settings AND wants to land on a
162
+ // specific section within the (long) tab body, it sets this to that section's id. The owning
163
+ // panel scrolls the matching element into view once, then calls `clearAccountSettingsScrollTarget`
164
+ // so a later plain open doesn't re-scroll. Null when no section was requested.
165
+ const accountSettingsScrollTarget = ref<string | null>(null)
166
+ // Observability integration: the post-release-health connection panel (Datadog
167
+ // today, pluggable). NB: distinct from `observabilityInstanceId`, which is the
168
+ // LLM per-call observability panel (see the result-views slice).
169
+ const observabilityConnectionOpen = ref(false)
170
+ // Private package registries: the workspace's npm/GitHub-Packages entries agent
171
+ // containers install with. Opened from the Integrations hub.
172
+ const packageRegistriesOpen = ref(false)
173
+ // API access tokens: the workspace's inbound public-API keys external systems present to
174
+ // the `/api/v1` surface. Opened from the Integrations hub.
175
+ const apiTokensOpen = ref(false)
176
+ // The single tabbed Infrastructure window — a TOP-LEVEL navbar destination (no longer
177
+ // reached via the Integrations hub). Two topical tabs: "Agent containers" (the execution
178
+ // backend + self-hosted runner pool, plus the local-mode warm pool/checkout) and "Test
179
+ // environments" (the ephemeral-environment provider). `infrastructureOpen` is the modal
180
+ // flag; `infrastructureTab` selects the tab. `openInfrastructure()` is the navbar entry;
181
+ // `openProviderConnection(kind)` remains for deep-links (a banner's "Configure…" button).
182
+ const infrastructureOpen = ref(false)
183
+ const infrastructureTab = ref<'environment' | 'runner-pool'>('runner-pool')
184
+ // Non-secret prefill captured from the `cat-factory k3s` CLI deep-link (see
185
+ // `consumeK3sSetupDeepLink`). When set, the Test-environments tab's kube engine form seeds the
186
+ // `local-k3s` connection from it; the ServiceAccount token is deliberately NOT in the link (a
187
+ // secret in a URL leaks into history/logs), so the user still pastes it before Test → Save.
188
+ const k3sSetupPrefill = ref<K3sSetupPrefill | null>(null)
189
+ // Environment setup wizard (shared-stacks slice 7): the guided detect → review → preflight →
190
+ // trial → save flow for a service frame's `docker-compose` provisioning. `environmentWizardOpen`
191
+ // is the modal flag; `environmentWizardFrameId` preselects the service frame the flow targets
192
+ // (set when launched from a frame's inspector nudge; null ⇒ the wizard's pick step chooses one).
193
+ const environmentWizardOpen = ref(false)
194
+ const environmentWizardFrameId = ref<string | null>(null)
195
+ const modelConfigOpen = ref(false)
196
+ // LLM-vendor subscription credentials (the token pool powering the Claude Code
197
+ // / Codex harnesses). `vendorCredentialsTab` lets a caller deep-link to one tab —
198
+ // the user-scoped "My subscriptions" entry opens straight onto the `personal` tab.
199
+ const vendorCredentialsOpen = ref(false)
200
+ const vendorCredentialsTab = ref('pool')
201
+ // Per-user settings panel: the signed-in user's own-machine local model runners.
202
+ const localModelsOpen = ref(false)
203
+ // The Sandbox (parallel prompt/model testing) surface — an opt-in, on-demand window.
204
+ const sandboxOpen = ref(false)
205
+ const userSecretsOpen = ref(false)
206
+ // Per-workspace settings panel: the OpenRouter dynamic catalog (browse/enable gateway models).
207
+ const openRouterOpen = ref(false)
208
+
209
+ // AI-onboarding surfaces (driven by `useAiReadiness`). `aiProviderSetupOpen` is the
210
+ // "no usable AI source" dialog; `aiPresetMismatchOpen` is the "default preset points at
211
+ // unavailable models" dialog. The `*Dismissed` flags are per-session: they suppress the
212
+ // auto-open (and let the banner be dismissed) without permanently hiding the prompt — it
213
+ // re-evaluates on the next load. Both clear themselves once the underlying gap is closed.
214
+ const aiProviderSetupOpen = ref(false)
215
+ const aiPresetMismatchOpen = ref(false)
216
+ const aiSetupDismissed = ref(false)
217
+ const aiPresetDismissed = ref(false)
218
+
219
+ // Infra-setup banner: per-SESSION dismissals, one flag per area, cleared on workspace switch
220
+ // exactly like the AI-onboarding flags (a dismissal in one workspace must not suppress the
221
+ // independent prompt for another). The PERMANENT "don't notify me again" dismissal is per-USER
222
+ // and persists in localStorage from the banner component; this only covers "hide for now".
223
+ const infraSetupSessionDismissed = ref<InfraSetupArea[]>([])
224
+ function dismissInfraSetupForSession(area: InfraSetupArea) {
225
+ if (!infraSetupSessionDismissed.value.includes(area))
226
+ infraSetupSessionDismissed.value = [...infraSetupSessionDismissed.value, area]
227
+ }
228
+ function resetInfraSetupDismissals() {
229
+ infraSetupSessionDismissed.value = []
230
+ }
231
+
232
+ function openBuilder() {
233
+ builderOpen.value = true
234
+ }
235
+
236
+ /** Auto-open the pipeline-health advisory once per session (no-op after it's been shown). */
237
+ function maybeOpenPipelineHealth() {
238
+ if (pipelineHealthSeen.value) return
239
+ pipelineHealthSeen.value = true
240
+ pipelineHealthOpen.value = true
241
+ }
242
+
243
+ function openPipelineHealth() {
244
+ pipelineHealthSeen.value = true
245
+ pipelineHealthOpen.value = true
246
+ }
247
+
248
+ function closePipelineHealth() {
249
+ pipelineHealthOpen.value = false
250
+ }
251
+
252
+ /** Auto-open the merge-preset health advisory once per session (no-op after it's been shown). */
253
+ function maybeOpenRiskPolicyHealth() {
254
+ if (riskPolicyHealthSeen.value) return
255
+ riskPolicyHealthSeen.value = true
256
+ riskPolicyHealthOpen.value = true
257
+ }
258
+
259
+ function openRiskPolicyHealth() {
260
+ riskPolicyHealthSeen.value = true
261
+ riskPolicyHealthOpen.value = true
262
+ }
263
+
264
+ function closeRiskPolicyHealth() {
265
+ riskPolicyHealthOpen.value = false
266
+ }
267
+
268
+ /** Auto-open the model-preset health advisory once per session (no-op after it's been shown). */
269
+ function maybeOpenModelPresetHealth() {
270
+ if (modelPresetHealthSeen.value) return
271
+ modelPresetHealthSeen.value = true
272
+ modelPresetHealthOpen.value = true
273
+ }
274
+
275
+ function openModelPresetHealth() {
276
+ modelPresetHealthSeen.value = true
277
+ modelPresetHealthOpen.value = true
278
+ }
279
+
280
+ function closeModelPresetHealth() {
281
+ modelPresetHealthOpen.value = false
282
+ }
283
+
284
+ function openDecision(instanceId: string, decisionId: string) {
285
+ decisionContext.value = { instanceId, decisionId }
286
+ }
287
+
288
+ function closeDecision() {
289
+ decisionContext.value = null
290
+ }
291
+
292
+ function openDocumentConnect(source: DocumentSourceKind) {
293
+ resetHubReturn()
294
+ documentConnect.value = { source }
295
+ }
296
+ function closeDocumentConnect() {
297
+ documentConnect.value = null
298
+ }
299
+ function openDocumentImport(
300
+ targetFrameId: string | null = null,
301
+ source: DocumentSourceKind | null = null,
302
+ ) {
303
+ resetHubReturn()
304
+ documentImport.value = { source, targetFrameId }
305
+ }
306
+ function closeDocumentImport() {
307
+ documentImport.value = null
308
+ }
309
+ function openDocumentTemplates() {
310
+ resetHubReturn()
311
+ documentTemplates.value = true
312
+ }
313
+ function closeDocumentTemplates() {
314
+ documentTemplates.value = false
315
+ }
316
+ function openSpawnPreview(
317
+ source: DocumentSourceKind,
318
+ externalId: string,
319
+ targetFrameId: string | null = null,
320
+ ) {
321
+ spawnPreview.value = { source, externalId, targetFrameId }
322
+ }
323
+ function closeSpawnPreview() {
324
+ spawnPreview.value = null
325
+ }
326
+ function openTaskConnect(source: TaskSourceKind) {
327
+ resetHubReturn()
328
+ taskConnect.value = { source }
329
+ }
330
+ function closeTaskConnect() {
331
+ taskConnect.value = null
332
+ }
333
+ function openTaskImport(source: TaskSourceKind | null = null, containerId: string | null = null) {
334
+ resetHubReturn()
335
+ taskImport.value = { source, containerId }
336
+ }
337
+ function closeTaskImport() {
338
+ taskImport.value = null
339
+ }
340
+ function openAddTask(containerId: string, prefill: AddTaskPrefill | null = null) {
341
+ addTaskPrefill.value = prefill
342
+ addTaskContainerId.value = containerId
343
+ }
344
+ function closeAddTask() {
345
+ addTaskContainerId.value = null
346
+ addTaskPrefill.value = null
347
+ }
348
+ function openAddRecurring(frameId: string) {
349
+ addRecurringFrameId.value = frameId
350
+ }
351
+ function closeAddRecurring() {
352
+ addRecurringFrameId.value = null
353
+ }
354
+ function openCreateInitiative(frameId: string) {
355
+ createInitiativeFrameId.value = frameId
356
+ }
357
+ function closeCreateInitiative() {
358
+ createInitiativeFrameId.value = null
359
+ }
360
+ function openBootstrap() {
361
+ bootstrapOpen.value = true
362
+ }
363
+ function closeBootstrap() {
364
+ bootstrapOpen.value = false
365
+ }
366
+ function openAddService() {
367
+ addServiceOpen.value = true
368
+ }
369
+ function closeAddService() {
370
+ addServiceOpen.value = false
371
+ }
372
+ function openGitHub() {
373
+ resetHubReturn()
374
+ githubOpen.value = true
375
+ }
376
+ function closeGitHub() {
377
+ githubOpen.value = false
378
+ }
379
+ function openSlack() {
380
+ resetHubReturn()
381
+ slackOpen.value = true
382
+ }
383
+ function closeSlack() {
384
+ slackOpen.value = false
385
+ }
386
+ function openFragmentLibrary() {
387
+ fragmentLibraryOpen.value = true
388
+ }
389
+ function closeFragmentLibrary() {
390
+ fragmentLibraryOpen.value = false
391
+ }
392
+ function openCommandBar() {
393
+ commandBarOpen.value = true
394
+ }
395
+ function closeCommandBar() {
396
+ commandBarOpen.value = false
397
+ }
398
+ function toggleCommandBar() {
399
+ commandBarOpen.value = !commandBarOpen.value
400
+ }
401
+ function openShortcutsHelp() {
402
+ shortcutsHelpOpen.value = true
403
+ }
404
+ function closeShortcutsHelp() {
405
+ shortcutsHelpOpen.value = false
406
+ }
407
+ function toggleShortcutsHelp() {
408
+ shortcutsHelpOpen.value = !shortcutsHelpOpen.value
409
+ }
410
+ function openMobileNav() {
411
+ mobileNavOpen.value = true
412
+ }
413
+ function closeMobileNav() {
414
+ mobileNavOpen.value = false
415
+ }
416
+ function toggleMobileNav() {
417
+ mobileNavOpen.value = !mobileNavOpen.value
418
+ }
419
+ // Clear BOTH hub came-from markers. Every direct `open*` below calls this so that a
420
+ // panel opened outside the hubs never grows a dead Back control, and so switching from
421
+ // one hub's panel to the other's clears the stale marker.
422
+ function resetHubReturn() {
423
+ cameFromIntegrations.value = false
424
+ cameFromPersonal.value = false
425
+ }
426
+ function openIntegrations() {
427
+ // Reaching the hub itself (fresh, or via a panel's Back control) clears the
428
+ // came-from markers — we're at the hub, not inside a hub-spawned panel.
429
+ resetHubReturn()
430
+ integrationsOpen.value = true
431
+ }
432
+ function closeIntegrations() {
433
+ integrationsOpen.value = false
434
+ }
435
+ function openPersonalSetup() {
436
+ resetHubReturn()
437
+ personalSetupOpen.value = true
438
+ }
439
+ function closePersonalSetup() {
440
+ personalSetupOpen.value = false
441
+ }
442
+ // Open a user-scoped panel FROM the My-setup hub: run its open handler (which resets the
443
+ // markers), then mark that we came from My setup and dismiss it, so the panel's
444
+ // IntegrationBackTitle returns here rather than to the workspace Integrations hub.
445
+ function openFromPersonal(open: () => void) {
446
+ open()
447
+ cameFromPersonal.value = true
448
+ personalSetupOpen.value = false
449
+ }
450
+ // Open an integration's own panel FROM the hub: run its open handler (which resets
451
+ // `cameFromIntegrations`), then mark that we came from the hub and dismiss it. The
452
+ // panel reads `cameFromIntegrations` to show its Back control.
453
+ function openFromIntegrations(open: () => void) {
454
+ open()
455
+ cameFromIntegrations.value = true
456
+ integrationsOpen.value = false
457
+ }
458
+ function openWorkspaceSettings(tab = 'workspace') {
459
+ resetHubReturn()
460
+ workspaceSettingsTab.value = tab
461
+ workspaceSettingsOpen.value = true
462
+ }
463
+ function closeWorkspaceSettings() {
464
+ workspaceSettingsOpen.value = false
465
+ }
466
+ function setWorkspaceSettingsTab(tab: string) {
467
+ workspaceSettingsTab.value = tab
468
+ }
469
+ function openAccountSettings(tab = 'team') {
470
+ resetHubReturn()
471
+ accountSettingsTab.value = tab
472
+ accountSettingsOpen.value = true
473
+ }
474
+ // Deep-link to the content (binary-artifact) storage configuration, which lives near the
475
+ // bottom of the account settings' team tab (`AccountDeploymentSettings`). Used by the
476
+ // pipeline-start error prompt when a storage-reliant agent (the UI Tester) has no storage
477
+ // configured. Sets a scroll anchor so the panel brings the storage section into view rather
478
+ // than dropping the user at the top of the long team tab to hunt for it.
479
+ function openContentStorageSettings() {
480
+ accountSettingsScrollTarget.value = 'content-storage'
481
+ openAccountSettings('team')
482
+ }
483
+ function clearAccountSettingsScrollTarget() {
484
+ accountSettingsScrollTarget.value = null
485
+ }
486
+ function closeAccountSettings() {
487
+ accountSettingsOpen.value = false
488
+ accountSettingsScrollTarget.value = null
489
+ }
490
+ function setAccountSettingsTab(tab: string) {
491
+ accountSettingsTab.value = tab
492
+ }
493
+ function openObservabilityConnection() {
494
+ resetHubReturn()
495
+ observabilityConnectionOpen.value = true
496
+ }
497
+ function closeObservabilityConnection() {
498
+ observabilityConnectionOpen.value = false
499
+ }
500
+ function openPackageRegistries() {
501
+ resetHubReturn()
502
+ packageRegistriesOpen.value = true
503
+ }
504
+ function closePackageRegistries() {
505
+ packageRegistriesOpen.value = false
506
+ }
507
+ function openApiTokens() {
508
+ resetHubReturn()
509
+ apiTokensOpen.value = true
510
+ }
511
+ function closeApiTokens() {
512
+ apiTokensOpen.value = false
513
+ }
514
+ // Top-level navbar entry into the Infrastructure window. No hub-return marker (it isn't
515
+ // reached from the Integrations hub), so the window shows no "Back to Integrations" control.
516
+ function openInfrastructure(tab: 'environment' | 'runner-pool' = 'runner-pool') {
517
+ resetHubReturn()
518
+ infrastructureTab.value = tab
519
+ infrastructureOpen.value = true
520
+ }
521
+ function openProviderConnection(kind: 'environment' | 'runner-pool') {
522
+ resetHubReturn()
523
+ infrastructureTab.value = kind
524
+ infrastructureOpen.value = true
525
+ }
526
+ function closeProviderConnection() {
527
+ infrastructureOpen.value = false
528
+ // Drop any consumed CLI prefill so re-opening the window normally doesn't re-seed the form.
529
+ k3sSetupPrefill.value = null
530
+ }
531
+ // Capture a `cat-factory k3s` deep-link (`?infraSetup=local-k3s&…`) on app load: stash the
532
+ // non-secret connection values, open the Infrastructure window on the Test-environments tab so
533
+ // the kube engine form seeds from them, then strip the params from the URL (mirrors the
534
+ // `?invite=` handling in the auth store) so a reload doesn't re-trigger and the link isn't left
535
+ // in history. No-op when the query param is absent.
536
+ function consumeK3sSetupDeepLink() {
537
+ if (typeof window === 'undefined') return
538
+ const params = new URLSearchParams(window.location.search)
539
+ if (params.get('infraSetup') !== 'local-k3s') return
540
+ k3sSetupPrefill.value = {
541
+ label: params.get('label') ?? 'Local k3s',
542
+ apiServerUrl: params.get('apiServerUrl') ?? '',
543
+ namespaceTemplate: params.get('namespaceTemplate') ?? '',
544
+ hostTemplate: params.get('hostTemplate') ?? '',
545
+ // Only carry the flag the link actually set — a missing param leaves the form's engine
546
+ // default (skip-TLS on for a local self-signed cluster) untouched.
547
+ insecureSkipTlsVerify: params.has('insecureSkipTlsVerify')
548
+ ? params.get('insecureSkipTlsVerify') === '1'
549
+ : undefined,
550
+ }
551
+ resetHubReturn()
552
+ infrastructureTab.value = 'environment'
553
+ infrastructureOpen.value = true
554
+ for (const key of [
555
+ 'infraSetup',
556
+ 'label',
557
+ 'apiServerUrl',
558
+ 'namespaceTemplate',
559
+ 'hostTemplate',
560
+ 'insecureSkipTlsVerify',
561
+ ]) {
562
+ params.delete(key)
563
+ }
564
+ const qs = params.toString()
565
+ history.replaceState(null, '', window.location.pathname + (qs ? `?${qs}` : ''))
566
+ }
567
+ // Launch the environment setup wizard, optionally preselecting the service frame it targets
568
+ // (the inspector nudge passes the frame; the navbar entry opens it with the pick step active).
569
+ function openEnvironmentSetup(frameId: string | null = null) {
570
+ resetHubReturn()
571
+ environmentWizardFrameId.value = frameId
572
+ environmentWizardOpen.value = true
573
+ }
574
+ function closeEnvironmentSetup() {
575
+ environmentWizardOpen.value = false
576
+ environmentWizardFrameId.value = null
577
+ }
578
+ function openModelConfig() {
579
+ modelConfigOpen.value = true
580
+ }
581
+ function closeModelConfig() {
582
+ modelConfigOpen.value = false
583
+ }
584
+ function openVendorCredentials(tab = 'pool') {
585
+ resetHubReturn()
586
+ vendorCredentialsTab.value = tab
587
+ vendorCredentialsOpen.value = true
588
+ }
589
+ function setVendorCredentialsTab(tab: string) {
590
+ vendorCredentialsTab.value = tab
591
+ }
592
+ function closeVendorCredentials() {
593
+ vendorCredentialsOpen.value = false
594
+ }
595
+ function openLocalModels() {
596
+ resetHubReturn()
597
+ localModelsOpen.value = true
598
+ }
599
+ function closeLocalModels() {
600
+ localModelsOpen.value = false
601
+ }
602
+ function openSandbox() {
603
+ sandboxOpen.value = true
604
+ }
605
+ function closeSandbox() {
606
+ sandboxOpen.value = false
607
+ }
608
+ function openUserSecrets() {
609
+ resetHubReturn()
610
+ userSecretsOpen.value = true
611
+ }
612
+ function closeUserSecrets() {
613
+ userSecretsOpen.value = false
614
+ }
615
+ function openOpenRouter() {
616
+ resetHubReturn()
617
+ openRouterOpen.value = true
618
+ }
619
+ function closeOpenRouter() {
620
+ openRouterOpen.value = false
621
+ }
622
+ function openAiProviderSetup() {
623
+ aiProviderSetupOpen.value = true
624
+ }
625
+ function closeAiProviderSetup() {
626
+ aiProviderSetupOpen.value = false
627
+ }
628
+ function openAiPresetMismatch() {
629
+ aiPresetMismatchOpen.value = true
630
+ }
631
+ function closeAiPresetMismatch() {
632
+ aiPresetMismatchOpen.value = false
633
+ }
634
+ // Banner dismissal is distinct from closing the dialog: closing the dialog leaves the
635
+ // banner so the user can reopen it; dismissing the banner hides the whole prompt for
636
+ // the session (it re-evaluates on the next load).
637
+ function dismissAiSetup() {
638
+ aiProviderSetupOpen.value = false
639
+ aiSetupDismissed.value = true
640
+ }
641
+ function dismissAiPresetMismatch() {
642
+ aiPresetMismatchOpen.value = false
643
+ aiPresetDismissed.value = true
644
+ }
645
+ // Clear the per-session AI-onboarding state (open dialogs + dismissed flags). Called on
646
+ // workspace switch: dismissals are per-session-per-workspace, so a prompt dismissed in one
647
+ // workspace must not suppress the (independent) prompt for another workspace that also
648
+ // lacks a usable AI source / has a broken default preset.
649
+ function resetAiOnboarding() {
650
+ aiProviderSetupOpen.value = false
651
+ aiPresetMismatchOpen.value = false
652
+ aiSetupDismissed.value = false
653
+ aiPresetDismissed.value = false
654
+ }
655
+
656
+ return {
657
+ builderOpen,
658
+ pipelineHealthOpen,
659
+ pipelineHealthSeen,
660
+ riskPolicyHealthOpen,
661
+ riskPolicyHealthSeen,
662
+ modelPresetHealthOpen,
663
+ modelPresetHealthSeen,
664
+ decisionContext,
665
+ documentConnect,
666
+ documentImport,
667
+ documentTemplates,
668
+ spawnPreview,
669
+ taskConnect,
670
+ taskImport,
671
+ addTaskContainerId,
672
+ addTaskPrefill,
673
+ addRecurringFrameId,
674
+ createInitiativeFrameId,
675
+ bootstrapOpen,
676
+ addServiceOpen,
677
+ githubOpen,
678
+ slackOpen,
679
+ fragmentLibraryOpen,
680
+ commandBarOpen,
681
+ shortcutsHelpOpen,
682
+ mobileNavOpen,
683
+ integrationsOpen,
684
+ cameFromIntegrations,
685
+ personalSetupOpen,
686
+ cameFromPersonal,
687
+ workspaceSettingsOpen,
688
+ workspaceSettingsTab,
689
+ accountSettingsOpen,
690
+ accountSettingsTab,
691
+ accountSettingsScrollTarget,
692
+ observabilityConnectionOpen,
693
+ packageRegistriesOpen,
694
+ apiTokensOpen,
695
+ infrastructureOpen,
696
+ infrastructureTab,
697
+ openInfrastructure,
698
+ modelConfigOpen,
699
+ vendorCredentialsOpen,
700
+ vendorCredentialsTab,
701
+ localModelsOpen,
702
+ sandboxOpen,
703
+ userSecretsOpen,
704
+ openRouterOpen,
705
+ aiProviderSetupOpen,
706
+ aiPresetMismatchOpen,
707
+ aiSetupDismissed,
708
+ aiPresetDismissed,
709
+ infraSetupSessionDismissed,
710
+ dismissInfraSetupForSession,
711
+ resetInfraSetupDismissals,
712
+ k3sSetupPrefill,
713
+ consumeK3sSetupDeepLink,
714
+ environmentWizardOpen,
715
+ environmentWizardFrameId,
716
+ openBuilder,
717
+ maybeOpenPipelineHealth,
718
+ openPipelineHealth,
719
+ closePipelineHealth,
720
+ maybeOpenRiskPolicyHealth,
721
+ openRiskPolicyHealth,
722
+ closeRiskPolicyHealth,
723
+ maybeOpenModelPresetHealth,
724
+ openModelPresetHealth,
725
+ closeModelPresetHealth,
726
+ openDecision,
727
+ closeDecision,
728
+ openDocumentConnect,
729
+ closeDocumentConnect,
730
+ openDocumentImport,
731
+ closeDocumentImport,
732
+ openDocumentTemplates,
733
+ closeDocumentTemplates,
734
+ openSpawnPreview,
735
+ closeSpawnPreview,
736
+ openTaskConnect,
737
+ closeTaskConnect,
738
+ openTaskImport,
739
+ closeTaskImport,
740
+ openAddTask,
741
+ closeAddTask,
742
+ openAddRecurring,
743
+ closeAddRecurring,
744
+ openCreateInitiative,
745
+ closeCreateInitiative,
746
+ openBootstrap,
747
+ closeBootstrap,
748
+ openAddService,
749
+ closeAddService,
750
+ openGitHub,
751
+ closeGitHub,
752
+ openSlack,
753
+ closeSlack,
754
+ openFragmentLibrary,
755
+ closeFragmentLibrary,
756
+ openCommandBar,
757
+ closeCommandBar,
758
+ toggleCommandBar,
759
+ openShortcutsHelp,
760
+ closeShortcutsHelp,
761
+ toggleShortcutsHelp,
762
+ openMobileNav,
763
+ closeMobileNav,
764
+ toggleMobileNav,
765
+ openIntegrations,
766
+ closeIntegrations,
767
+ openFromIntegrations,
768
+ openPersonalSetup,
769
+ closePersonalSetup,
770
+ openFromPersonal,
771
+ openWorkspaceSettings,
772
+ closeWorkspaceSettings,
773
+ setWorkspaceSettingsTab,
774
+ openAccountSettings,
775
+ openContentStorageSettings,
776
+ clearAccountSettingsScrollTarget,
777
+ closeAccountSettings,
778
+ setAccountSettingsTab,
779
+ openObservabilityConnection,
780
+ closeObservabilityConnection,
781
+ openPackageRegistries,
782
+ closePackageRegistries,
783
+ openApiTokens,
784
+ closeApiTokens,
785
+ openProviderConnection,
786
+ closeProviderConnection,
787
+ openEnvironmentSetup,
788
+ closeEnvironmentSetup,
789
+ openModelConfig,
790
+ closeModelConfig,
791
+ openVendorCredentials,
792
+ setVendorCredentialsTab,
793
+ closeVendorCredentials,
794
+ openLocalModels,
795
+ closeLocalModels,
796
+ openSandbox,
797
+ closeSandbox,
798
+ openUserSecrets,
799
+ closeUserSecrets,
800
+ openOpenRouter,
801
+ closeOpenRouter,
802
+ openAiProviderSetup,
803
+ closeAiProviderSetup,
804
+ openAiPresetMismatch,
805
+ closeAiPresetMismatch,
806
+ dismissAiSetup,
807
+ dismissAiPresetMismatch,
808
+ resetAiOnboarding,
809
+ }
810
+ }