@cat-factory/app 0.145.0 → 0.145.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.
@@ -25,34 +25,110 @@ export interface K3sSetupPrefill {
25
25
  insecureSkipTlsVerify?: boolean
26
26
  }
27
27
 
28
+ /** Clears both hub came-from markers; injected into the slices whose `open*` handlers reset them. */
29
+ type ResetHubReturn = () => void
30
+
28
31
  /**
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.
32
+ * Startup health advisories (pipeline / merge-preset / model-preset). Each lists built-ins with a
33
+ * newer catalog version (reseed) + new built-ins the workspace can add; the `*Seen` flag gates
34
+ * auto-open to once per session so it does not re-pop on every snapshot re-hydration.
36
35
  */
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.
36
+ function createHealthAdvisoryModals() {
42
37
  const pipelineHealthOpen = ref(false)
43
38
  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
39
  const riskPolicyHealthOpen = ref(false)
48
40
  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
41
  const modelPresetHealthOpen = ref(false)
53
42
  const modelPresetHealthSeen = ref(false)
43
+
44
+ /** Auto-open the pipeline-health advisory once per session (no-op after it's been shown). */
45
+ function maybeOpenPipelineHealth() {
46
+ if (pipelineHealthSeen.value) return
47
+ pipelineHealthSeen.value = true
48
+ pipelineHealthOpen.value = true
49
+ }
50
+ function openPipelineHealth() {
51
+ pipelineHealthSeen.value = true
52
+ pipelineHealthOpen.value = true
53
+ }
54
+ function closePipelineHealth() {
55
+ pipelineHealthOpen.value = false
56
+ }
57
+
58
+ /** Auto-open the merge-preset health advisory once per session (no-op after it's been shown). */
59
+ function maybeOpenRiskPolicyHealth() {
60
+ if (riskPolicyHealthSeen.value) return
61
+ riskPolicyHealthSeen.value = true
62
+ riskPolicyHealthOpen.value = true
63
+ }
64
+ function openRiskPolicyHealth() {
65
+ riskPolicyHealthSeen.value = true
66
+ riskPolicyHealthOpen.value = true
67
+ }
68
+ function closeRiskPolicyHealth() {
69
+ riskPolicyHealthOpen.value = false
70
+ }
71
+
72
+ /** Auto-open the model-preset health advisory once per session (no-op after it's been shown). */
73
+ function maybeOpenModelPresetHealth() {
74
+ if (modelPresetHealthSeen.value) return
75
+ modelPresetHealthSeen.value = true
76
+ modelPresetHealthOpen.value = true
77
+ }
78
+ function openModelPresetHealth() {
79
+ modelPresetHealthSeen.value = true
80
+ modelPresetHealthOpen.value = true
81
+ }
82
+ function closeModelPresetHealth() {
83
+ modelPresetHealthOpen.value = false
84
+ }
85
+
86
+ return {
87
+ pipelineHealthOpen,
88
+ pipelineHealthSeen,
89
+ riskPolicyHealthOpen,
90
+ riskPolicyHealthSeen,
91
+ modelPresetHealthOpen,
92
+ modelPresetHealthSeen,
93
+ maybeOpenPipelineHealth,
94
+ openPipelineHealth,
95
+ closePipelineHealth,
96
+ maybeOpenRiskPolicyHealth,
97
+ openRiskPolicyHealth,
98
+ closeRiskPolicyHealth,
99
+ maybeOpenModelPresetHealth,
100
+ openModelPresetHealth,
101
+ closeModelPresetHealth,
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Small standalone surfaces with no hub relationship: the pipeline builder and the
107
+ * decision-wait window.
108
+ */
109
+ function createMiscModals() {
110
+ const builderOpen = ref(false)
54
111
  const decisionContext = ref<{ instanceId: string; decisionId: string } | null>(null)
55
112
 
113
+ function openBuilder() {
114
+ builderOpen.value = true
115
+ }
116
+ function openDecision(instanceId: string, decisionId: string) {
117
+ decisionContext.value = { instanceId, decisionId }
118
+ }
119
+ function closeDecision() {
120
+ decisionContext.value = null
121
+ }
122
+
123
+ return { builderOpen, decisionContext, openBuilder, openDecision, closeDecision }
124
+ }
125
+
126
+ /**
127
+ * Document- and task-source integration modals (keyed by source), plus the add-task /
128
+ * add-recurring / create-initiative surfaces. The `open*` connect/import handlers reset the hub
129
+ * came-from markers (they can be reached from the Integrations hub).
130
+ */
131
+ function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
56
132
  // Document-source integration modals, keyed by source. `documentImport` and
57
133
  // `spawnPreview` carry an optional target frame, so structure spawned from a
58
134
  // frame's inspector lands inside that frame rather than creating new top-level
@@ -100,280 +176,248 @@ export function createUiModals() {
100
176
  // under, or null when closed (mirrors the add-task flow).
101
177
  const createInitiativeFrameId = ref<string | null>(null)
102
178
 
179
+ function openDocumentConnect(source: DocumentSourceKind) {
180
+ resetHubReturn()
181
+ documentConnect.value = { source }
182
+ }
183
+ function closeDocumentConnect() {
184
+ documentConnect.value = null
185
+ }
186
+ function openDocumentImport(
187
+ targetFrameId: string | null = null,
188
+ source: DocumentSourceKind | null = null,
189
+ ) {
190
+ resetHubReturn()
191
+ documentImport.value = { source, targetFrameId }
192
+ }
193
+ function closeDocumentImport() {
194
+ documentImport.value = null
195
+ }
196
+ function openDocumentTemplates() {
197
+ resetHubReturn()
198
+ documentTemplates.value = true
199
+ }
200
+ function closeDocumentTemplates() {
201
+ documentTemplates.value = false
202
+ }
203
+ function openSpawnPreview(
204
+ source: DocumentSourceKind,
205
+ externalId: string,
206
+ targetFrameId: string | null = null,
207
+ ) {
208
+ spawnPreview.value = { source, externalId, targetFrameId }
209
+ }
210
+ function closeSpawnPreview() {
211
+ spawnPreview.value = null
212
+ }
213
+ function openTaskConnect(source: TaskSourceKind) {
214
+ resetHubReturn()
215
+ taskConnect.value = { source }
216
+ }
217
+ function closeTaskConnect() {
218
+ taskConnect.value = null
219
+ }
220
+ function openTaskImport(source: TaskSourceKind | null = null, containerId: string | null = null) {
221
+ resetHubReturn()
222
+ taskImport.value = { source, containerId }
223
+ }
224
+ function closeTaskImport() {
225
+ taskImport.value = null
226
+ }
227
+ function openAddTask(containerId: string, prefill: AddTaskPrefill | null = null) {
228
+ addTaskPrefill.value = prefill
229
+ addTaskContainerId.value = containerId
230
+ }
231
+ function closeAddTask() {
232
+ addTaskContainerId.value = null
233
+ addTaskPrefill.value = null
234
+ }
235
+ function openAddRecurring(frameId: string) {
236
+ addRecurringFrameId.value = frameId
237
+ }
238
+ function closeAddRecurring() {
239
+ addRecurringFrameId.value = null
240
+ }
241
+ function openCreateInitiative(frameId: string) {
242
+ createInitiativeFrameId.value = frameId
243
+ }
244
+ function closeCreateInitiative() {
245
+ createInitiativeFrameId.value = null
246
+ }
247
+
248
+ return {
249
+ documentConnect,
250
+ documentImport,
251
+ documentTemplates,
252
+ spawnPreview,
253
+ taskConnect,
254
+ taskImport,
255
+ addTaskContainerId,
256
+ addTaskPrefill,
257
+ addRecurringFrameId,
258
+ createInitiativeFrameId,
259
+ openDocumentConnect,
260
+ closeDocumentConnect,
261
+ openDocumentImport,
262
+ closeDocumentImport,
263
+ openDocumentTemplates,
264
+ closeDocumentTemplates,
265
+ openSpawnPreview,
266
+ closeSpawnPreview,
267
+ openTaskConnect,
268
+ closeTaskConnect,
269
+ openTaskImport,
270
+ closeTaskImport,
271
+ openAddTask,
272
+ closeAddTask,
273
+ openAddRecurring,
274
+ closeAddRecurring,
275
+ openCreateInitiative,
276
+ closeCreateInitiative,
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Global overlays with no hub-return relationship: repo bootstrap, add-service, the fragment
282
+ * library, the command bar (⌘K), the shortcuts cheatsheet, the mobile nav drawer, and the
283
+ * Sandbox. None reset the hub came-from markers (they aren't reached from the Integrations hub).
284
+ */
285
+ function createOverlayModals() {
103
286
  // Repo-bootstrap modal (manage reference architectures + launch a bootstrap).
104
287
  const bootstrapOpen = ref(false)
105
-
106
288
  // "Add a service from an existing GitHub repo" modal (no bootstrap run).
107
289
  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
290
  // Prompt-fragment library panel (manage the board's best-practice catalog +
116
291
  // linked guideline repos; ADR 0006).
117
292
  const fragmentLibraryOpen = ref(false)
118
-
119
293
  // Command bar (⌘K) — searchable launcher for every navbar action.
120
294
  const commandBarOpen = ref(false)
121
-
122
295
  // Keyboard-shortcuts cheatsheet (?) — a modal listing every global shortcut.
123
296
  const shortcutsHelpOpen = ref(false)
124
-
125
297
  // Mobile navigation drawer: on compact (< lg) viewports the SideBar is an
126
298
  // off-canvas drawer toggled by a hamburger; on lg+ it is a static aside and this
127
299
  // flag is ignored. Closed on any nav action so the board is revealed immediately.
128
300
  const mobileNavOpen = ref(false)
301
+ // The Sandbox (parallel prompt/model testing) surface — an opt-in, on-demand window.
302
+ const sandboxOpen = ref(false)
129
303
 
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
- // Platform-operator observability: the deployment-level dashboard (aggregate run health of
171
- // the account — outcomes, failure taxonomy, live depth, durations). Admin-gated. Distinct
172
- // from `observabilityConnectionOpen` (the Datadog connection) AND `observabilityInstanceId`
173
- // (the per-run LLM call panel).
174
- const operatorDashboardOpen = ref(false)
175
- // Private package registries: the workspace's npm/GitHub-Packages entries agent
176
- // containers install with. Opened from the Integrations hub.
177
- const packageRegistriesOpen = ref(false)
178
- // API access tokens: the workspace's inbound public-API keys external systems present to
179
- // the `/api/v1` surface. Opened from the Integrations hub.
180
- const apiTokensOpen = ref(false)
181
- // The single tabbed Infrastructure window — a TOP-LEVEL navbar destination (no longer
182
- // reached via the Integrations hub). Two topical tabs: "Agent containers" (the execution
183
- // backend + self-hosted runner pool, plus the local-mode warm pool/checkout) and "Test
184
- // environments" (the ephemeral-environment provider). `infrastructureOpen` is the modal
185
- // flag; `infrastructureTab` selects the tab. `openInfrastructure()` is the navbar entry;
186
- // `openProviderConnection(kind)` remains for deep-links (a banner's "Configure…" button).
187
- const infrastructureOpen = ref(false)
188
- const infrastructureTab = ref<'environment' | 'runner-pool'>('runner-pool')
189
- // Non-secret prefill captured from the `cat-factory k3s` CLI deep-link (see
190
- // `consumeK3sSetupDeepLink`). When set, the Test-environments tab's kube engine form seeds the
191
- // `local-k3s` connection from it; the ServiceAccount token is deliberately NOT in the link (a
192
- // secret in a URL leaks into history/logs), so the user still pastes it before Test → Save.
193
- const k3sSetupPrefill = ref<K3sSetupPrefill | null>(null)
194
- // Environment setup wizard (shared-stacks slice 7): the guided detect → review → preflight →
195
- // trial → save flow for a service frame's `docker-compose` provisioning. `environmentWizardOpen`
196
- // is the modal flag; `environmentWizardFrameId` preselects the service frame the flow targets
197
- // (set when launched from a frame's inspector nudge; null ⇒ the wizard's pick step chooses one).
198
- const environmentWizardOpen = ref(false)
199
- const environmentWizardFrameId = ref<string | null>(null)
200
- const modelConfigOpen = ref(false)
201
- // LLM-vendor subscription credentials (the token pool powering the Claude Code
202
- // / Codex harnesses). `vendorCredentialsTab` lets a caller deep-link to one tab —
203
- // the user-scoped "My subscriptions" entry opens straight onto the `personal` tab.
204
- const vendorCredentialsOpen = ref(false)
205
- const vendorCredentialsTab = ref('pool')
206
- // Per-user settings panel: the signed-in user's own-machine local model runners.
207
- const localModelsOpen = ref(false)
208
- // The Sandbox (parallel prompt/model testing) surface — an opt-in, on-demand window.
209
- const sandboxOpen = ref(false)
210
- const userSecretsOpen = ref(false)
211
- // Per-workspace settings panel: the OpenRouter dynamic catalog (browse/enable gateway models).
212
- const openRouterOpen = ref(false)
213
-
214
- // AI-onboarding surfaces (driven by `useAiReadiness`). `aiProviderSetupOpen` is the
215
- // "no usable AI source" dialog; `aiPresetMismatchOpen` is the "default preset points at
216
- // unavailable models" dialog. The `*Dismissed` flags are per-session: they suppress the
217
- // auto-open (and let the banner be dismissed) without permanently hiding the prompt — it
218
- // re-evaluates on the next load. Both clear themselves once the underlying gap is closed.
219
- const aiProviderSetupOpen = ref(false)
220
- const aiPresetMismatchOpen = ref(false)
221
- const aiSetupDismissed = ref(false)
222
- const aiPresetDismissed = ref(false)
223
-
224
- // Infra-setup banner: per-SESSION dismissals, one flag per area, cleared on workspace switch
225
- // exactly like the AI-onboarding flags (a dismissal in one workspace must not suppress the
226
- // independent prompt for another). The PERMANENT "don't notify me again" dismissal is per-USER
227
- // and persists in localStorage from the banner component; this only covers "hide for now".
228
- const infraSetupSessionDismissed = ref<InfraSetupArea[]>([])
229
- function dismissInfraSetupForSession(area: InfraSetupArea) {
230
- if (!infraSetupSessionDismissed.value.includes(area))
231
- infraSetupSessionDismissed.value = [...infraSetupSessionDismissed.value, area]
232
- }
233
- function resetInfraSetupDismissals() {
234
- infraSetupSessionDismissed.value = []
235
- }
236
-
237
- function openBuilder() {
238
- builderOpen.value = true
239
- }
240
-
241
- /** Auto-open the pipeline-health advisory once per session (no-op after it's been shown). */
242
- function maybeOpenPipelineHealth() {
243
- if (pipelineHealthSeen.value) return
244
- pipelineHealthSeen.value = true
245
- pipelineHealthOpen.value = true
246
- }
247
-
248
- function openPipelineHealth() {
249
- pipelineHealthSeen.value = true
250
- pipelineHealthOpen.value = true
251
- }
252
-
253
- function closePipelineHealth() {
254
- pipelineHealthOpen.value = false
255
- }
256
-
257
- /** Auto-open the merge-preset health advisory once per session (no-op after it's been shown). */
258
- function maybeOpenRiskPolicyHealth() {
259
- if (riskPolicyHealthSeen.value) return
260
- riskPolicyHealthSeen.value = true
261
- riskPolicyHealthOpen.value = true
262
- }
263
-
264
- function openRiskPolicyHealth() {
265
- riskPolicyHealthSeen.value = true
266
- riskPolicyHealthOpen.value = true
267
- }
268
-
269
- function closeRiskPolicyHealth() {
270
- riskPolicyHealthOpen.value = false
271
- }
272
-
273
- /** Auto-open the model-preset health advisory once per session (no-op after it's been shown). */
274
- function maybeOpenModelPresetHealth() {
275
- if (modelPresetHealthSeen.value) return
276
- modelPresetHealthSeen.value = true
277
- modelPresetHealthOpen.value = true
278
- }
279
-
280
- function openModelPresetHealth() {
281
- modelPresetHealthSeen.value = true
282
- modelPresetHealthOpen.value = true
283
- }
284
-
285
- function closeModelPresetHealth() {
286
- modelPresetHealthOpen.value = false
287
- }
288
-
289
- function openDecision(instanceId: string, decisionId: string) {
290
- decisionContext.value = { instanceId, decisionId }
291
- }
292
-
293
- function closeDecision() {
294
- decisionContext.value = null
295
- }
296
-
297
- function openDocumentConnect(source: DocumentSourceKind) {
298
- resetHubReturn()
299
- documentConnect.value = { source }
300
- }
301
- function closeDocumentConnect() {
302
- documentConnect.value = null
303
- }
304
- function openDocumentImport(
305
- targetFrameId: string | null = null,
306
- source: DocumentSourceKind | null = null,
307
- ) {
308
- resetHubReturn()
309
- documentImport.value = { source, targetFrameId }
310
- }
311
- function closeDocumentImport() {
312
- documentImport.value = null
313
- }
314
- function openDocumentTemplates() {
315
- resetHubReturn()
316
- documentTemplates.value = true
304
+ function openBootstrap() {
305
+ bootstrapOpen.value = true
317
306
  }
318
- function closeDocumentTemplates() {
319
- documentTemplates.value = false
307
+ function closeBootstrap() {
308
+ bootstrapOpen.value = false
320
309
  }
321
- function openSpawnPreview(
322
- source: DocumentSourceKind,
323
- externalId: string,
324
- targetFrameId: string | null = null,
325
- ) {
326
- spawnPreview.value = { source, externalId, targetFrameId }
310
+ function openAddService() {
311
+ addServiceOpen.value = true
327
312
  }
328
- function closeSpawnPreview() {
329
- spawnPreview.value = null
313
+ function closeAddService() {
314
+ addServiceOpen.value = false
330
315
  }
331
- function openTaskConnect(source: TaskSourceKind) {
332
- resetHubReturn()
333
- taskConnect.value = { source }
316
+ function openFragmentLibrary() {
317
+ fragmentLibraryOpen.value = true
334
318
  }
335
- function closeTaskConnect() {
336
- taskConnect.value = null
319
+ function closeFragmentLibrary() {
320
+ fragmentLibraryOpen.value = false
337
321
  }
338
- function openTaskImport(source: TaskSourceKind | null = null, containerId: string | null = null) {
339
- resetHubReturn()
340
- taskImport.value = { source, containerId }
322
+ function openCommandBar() {
323
+ commandBarOpen.value = true
341
324
  }
342
- function closeTaskImport() {
343
- taskImport.value = null
325
+ function closeCommandBar() {
326
+ commandBarOpen.value = false
344
327
  }
345
- function openAddTask(containerId: string, prefill: AddTaskPrefill | null = null) {
346
- addTaskPrefill.value = prefill
347
- addTaskContainerId.value = containerId
328
+ function toggleCommandBar() {
329
+ commandBarOpen.value = !commandBarOpen.value
348
330
  }
349
- function closeAddTask() {
350
- addTaskContainerId.value = null
351
- addTaskPrefill.value = null
331
+ function openShortcutsHelp() {
332
+ shortcutsHelpOpen.value = true
352
333
  }
353
- function openAddRecurring(frameId: string) {
354
- addRecurringFrameId.value = frameId
334
+ function closeShortcutsHelp() {
335
+ shortcutsHelpOpen.value = false
355
336
  }
356
- function closeAddRecurring() {
357
- addRecurringFrameId.value = null
337
+ function toggleShortcutsHelp() {
338
+ shortcutsHelpOpen.value = !shortcutsHelpOpen.value
358
339
  }
359
- function openCreateInitiative(frameId: string) {
360
- createInitiativeFrameId.value = frameId
340
+ function openMobileNav() {
341
+ mobileNavOpen.value = true
361
342
  }
362
- function closeCreateInitiative() {
363
- createInitiativeFrameId.value = null
343
+ function closeMobileNav() {
344
+ mobileNavOpen.value = false
364
345
  }
365
- function openBootstrap() {
366
- bootstrapOpen.value = true
346
+ function toggleMobileNav() {
347
+ mobileNavOpen.value = !mobileNavOpen.value
367
348
  }
368
- function closeBootstrap() {
369
- bootstrapOpen.value = false
349
+ function openSandbox() {
350
+ sandboxOpen.value = true
370
351
  }
371
- function openAddService() {
372
- addServiceOpen.value = true
352
+ function closeSandbox() {
353
+ sandboxOpen.value = false
373
354
  }
374
- function closeAddService() {
375
- addServiceOpen.value = false
355
+
356
+ return {
357
+ bootstrapOpen,
358
+ addServiceOpen,
359
+ fragmentLibraryOpen,
360
+ commandBarOpen,
361
+ shortcutsHelpOpen,
362
+ mobileNavOpen,
363
+ sandboxOpen,
364
+ openBootstrap,
365
+ closeBootstrap,
366
+ openAddService,
367
+ closeAddService,
368
+ openFragmentLibrary,
369
+ closeFragmentLibrary,
370
+ openCommandBar,
371
+ closeCommandBar,
372
+ toggleCommandBar,
373
+ openShortcutsHelp,
374
+ closeShortcutsHelp,
375
+ toggleShortcutsHelp,
376
+ openMobileNav,
377
+ closeMobileNav,
378
+ toggleMobileNav,
379
+ openSandbox,
380
+ closeSandbox,
376
381
  }
382
+ }
383
+
384
+ /**
385
+ * The workspace-scoped integration panels: GitHub, Slack, observability, the operator dashboard,
386
+ * package registries, API tokens, model config, vendor credentials, local models, user secrets and
387
+ * OpenRouter. Every `open*` that a hub can route to resets the hub came-from markers.
388
+ */
389
+ function createIntegrationPanelModals(resetHubReturn: ResetHubReturn) {
390
+ // GitHub integration panel (connection management + repo/PR/issue browsing).
391
+ const githubOpen = ref(false)
392
+ // Slack integration panel (connect the account's Slack + per-workspace routing).
393
+ const slackOpen = ref(false)
394
+ // Observability integration: the post-release-health connection panel (Datadog
395
+ // today, pluggable). NB: distinct from `observabilityInstanceId`, which is the
396
+ // LLM per-call observability panel (see the result-views slice).
397
+ const observabilityConnectionOpen = ref(false)
398
+ // Platform-operator observability: the deployment-level dashboard (aggregate run health of
399
+ // the account — outcomes, failure taxonomy, live depth, durations). Admin-gated. Distinct
400
+ // from `observabilityConnectionOpen` (the Datadog connection) AND `observabilityInstanceId`
401
+ // (the per-run LLM call panel).
402
+ const operatorDashboardOpen = ref(false)
403
+ // Private package registries: the workspace's npm/GitHub-Packages entries agent
404
+ // containers install with. Opened from the Integrations hub.
405
+ const packageRegistriesOpen = ref(false)
406
+ // API access tokens: the workspace's inbound public-API keys external systems present to
407
+ // the `/api/v1` surface. Opened from the Integrations hub.
408
+ const apiTokensOpen = ref(false)
409
+ const modelConfigOpen = ref(false)
410
+ // LLM-vendor subscription credentials (the token pool powering the Claude Code
411
+ // / Codex harnesses). `vendorCredentialsTab` lets a caller deep-link to one tab —
412
+ // the user-scoped "My subscriptions" entry opens straight onto the `personal` tab.
413
+ const vendorCredentialsOpen = ref(false)
414
+ const vendorCredentialsTab = ref('pool')
415
+ // Per-user settings panel: the signed-in user's own-machine local model runners.
416
+ const localModelsOpen = ref(false)
417
+ const userSecretsOpen = ref(false)
418
+ // Per-workspace settings panel: the OpenRouter dynamic catalog (browse/enable gateway models).
419
+ const openRouterOpen = ref(false)
420
+
377
421
  function openGitHub() {
378
422
  resetHubReturn()
379
423
  githubOpen.value = true
@@ -388,78 +432,136 @@ export function createUiModals() {
388
432
  function closeSlack() {
389
433
  slackOpen.value = false
390
434
  }
391
- function openFragmentLibrary() {
392
- fragmentLibraryOpen.value = true
435
+ function openObservabilityConnection() {
436
+ resetHubReturn()
437
+ observabilityConnectionOpen.value = true
438
+ }
439
+ function closeObservabilityConnection() {
440
+ observabilityConnectionOpen.value = false
441
+ }
442
+ function openOperatorDashboard() {
443
+ resetHubReturn()
444
+ operatorDashboardOpen.value = true
445
+ }
446
+ function closeOperatorDashboard() {
447
+ operatorDashboardOpen.value = false
393
448
  }
394
- function closeFragmentLibrary() {
395
- fragmentLibraryOpen.value = false
449
+ function openPackageRegistries() {
450
+ resetHubReturn()
451
+ packageRegistriesOpen.value = true
396
452
  }
397
- function openCommandBar() {
398
- commandBarOpen.value = true
453
+ function closePackageRegistries() {
454
+ packageRegistriesOpen.value = false
399
455
  }
400
- function closeCommandBar() {
401
- commandBarOpen.value = false
456
+ function openApiTokens() {
457
+ resetHubReturn()
458
+ apiTokensOpen.value = true
402
459
  }
403
- function toggleCommandBar() {
404
- commandBarOpen.value = !commandBarOpen.value
460
+ function closeApiTokens() {
461
+ apiTokensOpen.value = false
405
462
  }
406
- function openShortcutsHelp() {
407
- shortcutsHelpOpen.value = true
463
+ function openModelConfig() {
464
+ modelConfigOpen.value = true
408
465
  }
409
- function closeShortcutsHelp() {
410
- shortcutsHelpOpen.value = false
466
+ function closeModelConfig() {
467
+ modelConfigOpen.value = false
411
468
  }
412
- function toggleShortcutsHelp() {
413
- shortcutsHelpOpen.value = !shortcutsHelpOpen.value
469
+ function openVendorCredentials(tab = 'pool') {
470
+ resetHubReturn()
471
+ vendorCredentialsTab.value = tab
472
+ vendorCredentialsOpen.value = true
414
473
  }
415
- function openMobileNav() {
416
- mobileNavOpen.value = true
474
+ function setVendorCredentialsTab(tab: string) {
475
+ vendorCredentialsTab.value = tab
417
476
  }
418
- function closeMobileNav() {
419
- mobileNavOpen.value = false
477
+ function closeVendorCredentials() {
478
+ vendorCredentialsOpen.value = false
420
479
  }
421
- function toggleMobileNav() {
422
- mobileNavOpen.value = !mobileNavOpen.value
480
+ function openLocalModels() {
481
+ resetHubReturn()
482
+ localModelsOpen.value = true
423
483
  }
424
- // Clear BOTH hub came-from markers. Every direct `open*` below calls this so that a
425
- // panel opened outside the hubs never grows a dead Back control, and so switching from
426
- // one hub's panel to the other's clears the stale marker.
427
- function resetHubReturn() {
428
- cameFromIntegrations.value = false
429
- cameFromPersonal.value = false
484
+ function closeLocalModels() {
485
+ localModelsOpen.value = false
430
486
  }
431
- function openIntegrations() {
432
- // Reaching the hub itself (fresh, or via a panel's Back control) clears the
433
- // came-from markers — we're at the hub, not inside a hub-spawned panel.
487
+ function openUserSecrets() {
434
488
  resetHubReturn()
435
- integrationsOpen.value = true
489
+ userSecretsOpen.value = true
436
490
  }
437
- function closeIntegrations() {
438
- integrationsOpen.value = false
491
+ function closeUserSecrets() {
492
+ userSecretsOpen.value = false
439
493
  }
440
- function openPersonalSetup() {
494
+ function openOpenRouter() {
441
495
  resetHubReturn()
442
- personalSetupOpen.value = true
443
- }
444
- function closePersonalSetup() {
445
- personalSetupOpen.value = false
496
+ openRouterOpen.value = true
446
497
  }
447
- // Open a user-scoped panel FROM the My-setup hub: run its open handler (which resets the
448
- // markers), then mark that we came from My setup and dismiss it, so the panel's
449
- // IntegrationBackTitle returns here rather than to the workspace Integrations hub.
450
- function openFromPersonal(open: () => void) {
451
- open()
452
- cameFromPersonal.value = true
453
- personalSetupOpen.value = false
498
+ function closeOpenRouter() {
499
+ openRouterOpen.value = false
454
500
  }
455
- // Open an integration's own panel FROM the hub: run its open handler (which resets
456
- // `cameFromIntegrations`), then mark that we came from the hub and dismiss it. The
457
- // panel reads `cameFromIntegrations` to show its Back control.
458
- function openFromIntegrations(open: () => void) {
459
- open()
460
- cameFromIntegrations.value = true
461
- integrationsOpen.value = false
501
+
502
+ return {
503
+ githubOpen,
504
+ slackOpen,
505
+ observabilityConnectionOpen,
506
+ operatorDashboardOpen,
507
+ packageRegistriesOpen,
508
+ apiTokensOpen,
509
+ modelConfigOpen,
510
+ vendorCredentialsOpen,
511
+ vendorCredentialsTab,
512
+ localModelsOpen,
513
+ userSecretsOpen,
514
+ openRouterOpen,
515
+ openGitHub,
516
+ closeGitHub,
517
+ openSlack,
518
+ closeSlack,
519
+ openObservabilityConnection,
520
+ closeObservabilityConnection,
521
+ openOperatorDashboard,
522
+ closeOperatorDashboard,
523
+ openPackageRegistries,
524
+ closePackageRegistries,
525
+ openApiTokens,
526
+ closeApiTokens,
527
+ openModelConfig,
528
+ closeModelConfig,
529
+ openVendorCredentials,
530
+ setVendorCredentialsTab,
531
+ closeVendorCredentials,
532
+ openLocalModels,
533
+ closeLocalModels,
534
+ openUserSecrets,
535
+ closeUserSecrets,
536
+ openOpenRouter,
537
+ closeOpenRouter,
462
538
  }
539
+ }
540
+
541
+ /**
542
+ * Workspace- and account-settings modals (single tabbed windows). `*Tab` lets a caller deep-link
543
+ * straight to a tab; `accountSettingsScrollTarget` is a one-shot deep-link anchor into a section
544
+ * within the (long) account-settings body.
545
+ */
546
+ function createSettingsModals(resetHubReturn: ResetHubReturn) {
547
+ // Workspace-settings modal: a single tabbed window gathering the workspace-wide
548
+ // config (workspace / merge thresholds / issue writeback / service best practices).
549
+ // `workspaceSettingsTab` lets other surfaces deep-link straight to a tab.
550
+ const workspaceSettingsOpen = ref(false)
551
+ const workspaceSettingsTab = ref('workspace')
552
+ // Account-settings modal: a single tabbed window for the per-account configuration —
553
+ // the team panel (members + roles + invitations + email sender + account API keys,
554
+ // `AccountTeamSettings`) and the account-tier prompt-fragment library. Account-scoped
555
+ // (distinct from workspace settings). `accountSettingsTab` lets other surfaces deep-link
556
+ // straight to a tab.
557
+ const accountSettingsOpen = ref(false)
558
+ const accountSettingsTab = ref('team')
559
+ // A one-shot deep-link anchor: when a surface opens account settings AND wants to land on a
560
+ // specific section within the (long) tab body, it sets this to that section's id. The owning
561
+ // panel scrolls the matching element into view once, then calls `clearAccountSettingsScrollTarget`
562
+ // so a later plain open doesn't re-scroll. Null when no section was requested.
563
+ const accountSettingsScrollTarget = ref<string | null>(null)
564
+
463
565
  function openWorkspaceSettings(tab = 'workspace') {
464
566
  resetHubReturn()
465
567
  workspaceSettingsTab.value = tab
@@ -495,34 +597,49 @@ export function createUiModals() {
495
597
  function setAccountSettingsTab(tab: string) {
496
598
  accountSettingsTab.value = tab
497
599
  }
498
- function openObservabilityConnection() {
499
- resetHubReturn()
500
- observabilityConnectionOpen.value = true
501
- }
502
- function closeObservabilityConnection() {
503
- observabilityConnectionOpen.value = false
504
- }
505
- function openOperatorDashboard() {
506
- resetHubReturn()
507
- operatorDashboardOpen.value = true
508
- }
509
- function closeOperatorDashboard() {
510
- operatorDashboardOpen.value = false
511
- }
512
- function openPackageRegistries() {
513
- resetHubReturn()
514
- packageRegistriesOpen.value = true
515
- }
516
- function closePackageRegistries() {
517
- packageRegistriesOpen.value = false
518
- }
519
- function openApiTokens() {
520
- resetHubReturn()
521
- apiTokensOpen.value = true
522
- }
523
- function closeApiTokens() {
524
- apiTokensOpen.value = false
600
+
601
+ return {
602
+ workspaceSettingsOpen,
603
+ workspaceSettingsTab,
604
+ accountSettingsOpen,
605
+ accountSettingsTab,
606
+ accountSettingsScrollTarget,
607
+ openWorkspaceSettings,
608
+ closeWorkspaceSettings,
609
+ setWorkspaceSettingsTab,
610
+ openAccountSettings,
611
+ openContentStorageSettings,
612
+ clearAccountSettingsScrollTarget,
613
+ closeAccountSettings,
614
+ setAccountSettingsTab,
525
615
  }
616
+ }
617
+
618
+ /**
619
+ * The Infrastructure window (agent containers + test environments) and the environment setup
620
+ * wizard, plus the `cat-factory k3s` CLI deep-link capture that seeds the kube engine form.
621
+ */
622
+ function createInfraModals(resetHubReturn: ResetHubReturn) {
623
+ // The single tabbed Infrastructure window — a TOP-LEVEL navbar destination (no longer
624
+ // reached via the Integrations hub). Two topical tabs: "Agent containers" (the execution
625
+ // backend + self-hosted runner pool, plus the local-mode warm pool/checkout) and "Test
626
+ // environments" (the ephemeral-environment provider). `infrastructureOpen` is the modal
627
+ // flag; `infrastructureTab` selects the tab. `openInfrastructure()` is the navbar entry;
628
+ // `openProviderConnection(kind)` remains for deep-links (a banner's "Configure…" button).
629
+ const infrastructureOpen = ref(false)
630
+ const infrastructureTab = ref<'environment' | 'runner-pool'>('runner-pool')
631
+ // Non-secret prefill captured from the `cat-factory k3s` CLI deep-link (see
632
+ // `consumeK3sSetupDeepLink`). When set, the Test-environments tab's kube engine form seeds the
633
+ // `local-k3s` connection from it; the ServiceAccount token is deliberately NOT in the link (a
634
+ // secret in a URL leaks into history/logs), so the user still pastes it before Test → Save.
635
+ const k3sSetupPrefill = ref<K3sSetupPrefill | null>(null)
636
+ // Environment setup wizard (shared-stacks slice 7): the guided detect → review → preflight →
637
+ // trial → save flow for a service frame's `docker-compose` provisioning. `environmentWizardOpen`
638
+ // is the modal flag; `environmentWizardFrameId` preselects the service frame the flow targets
639
+ // (set when launched from a frame's inspector nudge; null ⇒ the wizard's pick step chooses one).
640
+ const environmentWizardOpen = ref(false)
641
+ const environmentWizardFrameId = ref<string | null>(null)
642
+
526
643
  // Top-level navbar entry into the Infrastructure window. No hub-return marker (it isn't
527
644
  // reached from the Integrations hub), so the window shows no "Back to Integrations" control.
528
645
  function openInfrastructure(tab: 'environment' | 'runner-pool' = 'runner-pool') {
@@ -572,65 +689,64 @@ export function createUiModals() {
572
689
  'insecureSkipTlsVerify',
573
690
  ]) {
574
691
  params.delete(key)
575
- }
576
- const qs = params.toString()
577
- history.replaceState(null, '', window.location.pathname + (qs ? `?${qs}` : ''))
578
- }
579
- // Launch the environment setup wizard, optionally preselecting the service frame it targets
580
- // (the inspector nudge passes the frame; the navbar entry opens it with the pick step active).
581
- function openEnvironmentSetup(frameId: string | null = null) {
582
- resetHubReturn()
583
- environmentWizardFrameId.value = frameId
584
- environmentWizardOpen.value = true
585
- }
586
- function closeEnvironmentSetup() {
587
- environmentWizardOpen.value = false
588
- environmentWizardFrameId.value = null
589
- }
590
- function openModelConfig() {
591
- modelConfigOpen.value = true
592
- }
593
- function closeModelConfig() {
594
- modelConfigOpen.value = false
595
- }
596
- function openVendorCredentials(tab = 'pool') {
597
- resetHubReturn()
598
- vendorCredentialsTab.value = tab
599
- vendorCredentialsOpen.value = true
600
- }
601
- function setVendorCredentialsTab(tab: string) {
602
- vendorCredentialsTab.value = tab
603
- }
604
- function closeVendorCredentials() {
605
- vendorCredentialsOpen.value = false
606
- }
607
- function openLocalModels() {
608
- resetHubReturn()
609
- localModelsOpen.value = true
610
- }
611
- function closeLocalModels() {
612
- localModelsOpen.value = false
613
- }
614
- function openSandbox() {
615
- sandboxOpen.value = true
616
- }
617
- function closeSandbox() {
618
- sandboxOpen.value = false
692
+ }
693
+ const qs = params.toString()
694
+ history.replaceState(null, '', window.location.pathname + (qs ? `?${qs}` : ''))
619
695
  }
620
- function openUserSecrets() {
696
+ // Launch the environment setup wizard, optionally preselecting the service frame it targets
697
+ // (the inspector nudge passes the frame; the navbar entry opens it with the pick step active).
698
+ function openEnvironmentSetup(frameId: string | null = null) {
621
699
  resetHubReturn()
622
- userSecretsOpen.value = true
700
+ environmentWizardFrameId.value = frameId
701
+ environmentWizardOpen.value = true
623
702
  }
624
- function closeUserSecrets() {
625
- userSecretsOpen.value = false
703
+ function closeEnvironmentSetup() {
704
+ environmentWizardOpen.value = false
705
+ environmentWizardFrameId.value = null
626
706
  }
627
- function openOpenRouter() {
628
- resetHubReturn()
629
- openRouterOpen.value = true
707
+
708
+ return {
709
+ infrastructureOpen,
710
+ infrastructureTab,
711
+ openInfrastructure,
712
+ k3sSetupPrefill,
713
+ consumeK3sSetupDeepLink,
714
+ environmentWizardOpen,
715
+ environmentWizardFrameId,
716
+ openProviderConnection,
717
+ closeProviderConnection,
718
+ openEnvironmentSetup,
719
+ closeEnvironmentSetup,
630
720
  }
631
- function closeOpenRouter() {
632
- openRouterOpen.value = false
721
+ }
722
+
723
+ /**
724
+ * AI-onboarding surfaces (driven by `useAiReadiness`) + the infra-setup banner's per-session
725
+ * dismissals. The `*Dismissed` flags are per-session: they suppress the auto-open (and let the
726
+ * banner be dismissed) without permanently hiding the prompt — it re-evaluates on the next load.
727
+ */
728
+ function createAiOnboardingModals() {
729
+ // `aiProviderSetupOpen` is the "no usable AI source" dialog; `aiPresetMismatchOpen` is the
730
+ // "default preset points at unavailable models" dialog. Both clear themselves once the
731
+ // underlying gap is closed.
732
+ const aiProviderSetupOpen = ref(false)
733
+ const aiPresetMismatchOpen = ref(false)
734
+ const aiSetupDismissed = ref(false)
735
+ const aiPresetDismissed = ref(false)
736
+
737
+ // Infra-setup banner: per-SESSION dismissals, one flag per area, cleared on workspace switch
738
+ // exactly like the AI-onboarding flags (a dismissal in one workspace must not suppress the
739
+ // independent prompt for another). The PERMANENT "don't notify me again" dismissal is per-USER
740
+ // and persists in localStorage from the banner component; this only covers "hide for now".
741
+ const infraSetupSessionDismissed = ref<InfraSetupArea[]>([])
742
+ function dismissInfraSetupForSession(area: InfraSetupArea) {
743
+ if (!infraSetupSessionDismissed.value.includes(area))
744
+ infraSetupSessionDismissed.value = [...infraSetupSessionDismissed.value, area]
745
+ }
746
+ function resetInfraSetupDismissals() {
747
+ infraSetupSessionDismissed.value = []
633
748
  }
749
+
634
750
  function openAiProviderSetup() {
635
751
  aiProviderSetupOpen.value = true
636
752
  }
@@ -666,55 +782,6 @@ export function createUiModals() {
666
782
  }
667
783
 
668
784
  return {
669
- builderOpen,
670
- pipelineHealthOpen,
671
- pipelineHealthSeen,
672
- riskPolicyHealthOpen,
673
- riskPolicyHealthSeen,
674
- modelPresetHealthOpen,
675
- modelPresetHealthSeen,
676
- decisionContext,
677
- documentConnect,
678
- documentImport,
679
- documentTemplates,
680
- spawnPreview,
681
- taskConnect,
682
- taskImport,
683
- addTaskContainerId,
684
- addTaskPrefill,
685
- addRecurringFrameId,
686
- createInitiativeFrameId,
687
- bootstrapOpen,
688
- addServiceOpen,
689
- githubOpen,
690
- slackOpen,
691
- fragmentLibraryOpen,
692
- commandBarOpen,
693
- shortcutsHelpOpen,
694
- mobileNavOpen,
695
- integrationsOpen,
696
- cameFromIntegrations,
697
- personalSetupOpen,
698
- cameFromPersonal,
699
- workspaceSettingsOpen,
700
- workspaceSettingsTab,
701
- accountSettingsOpen,
702
- accountSettingsTab,
703
- accountSettingsScrollTarget,
704
- observabilityConnectionOpen,
705
- operatorDashboardOpen,
706
- packageRegistriesOpen,
707
- apiTokensOpen,
708
- infrastructureOpen,
709
- infrastructureTab,
710
- openInfrastructure,
711
- modelConfigOpen,
712
- vendorCredentialsOpen,
713
- vendorCredentialsTab,
714
- localModelsOpen,
715
- sandboxOpen,
716
- userSecretsOpen,
717
- openRouterOpen,
718
785
  aiProviderSetupOpen,
719
786
  aiPresetMismatchOpen,
720
787
  aiSetupDismissed,
@@ -722,98 +789,6 @@ export function createUiModals() {
722
789
  infraSetupSessionDismissed,
723
790
  dismissInfraSetupForSession,
724
791
  resetInfraSetupDismissals,
725
- k3sSetupPrefill,
726
- consumeK3sSetupDeepLink,
727
- environmentWizardOpen,
728
- environmentWizardFrameId,
729
- openBuilder,
730
- maybeOpenPipelineHealth,
731
- openPipelineHealth,
732
- closePipelineHealth,
733
- maybeOpenRiskPolicyHealth,
734
- openRiskPolicyHealth,
735
- closeRiskPolicyHealth,
736
- maybeOpenModelPresetHealth,
737
- openModelPresetHealth,
738
- closeModelPresetHealth,
739
- openDecision,
740
- closeDecision,
741
- openDocumentConnect,
742
- closeDocumentConnect,
743
- openDocumentImport,
744
- closeDocumentImport,
745
- openDocumentTemplates,
746
- closeDocumentTemplates,
747
- openSpawnPreview,
748
- closeSpawnPreview,
749
- openTaskConnect,
750
- closeTaskConnect,
751
- openTaskImport,
752
- closeTaskImport,
753
- openAddTask,
754
- closeAddTask,
755
- openAddRecurring,
756
- closeAddRecurring,
757
- openCreateInitiative,
758
- closeCreateInitiative,
759
- openBootstrap,
760
- closeBootstrap,
761
- openAddService,
762
- closeAddService,
763
- openGitHub,
764
- closeGitHub,
765
- openSlack,
766
- closeSlack,
767
- openFragmentLibrary,
768
- closeFragmentLibrary,
769
- openCommandBar,
770
- closeCommandBar,
771
- toggleCommandBar,
772
- openShortcutsHelp,
773
- closeShortcutsHelp,
774
- toggleShortcutsHelp,
775
- openMobileNav,
776
- closeMobileNav,
777
- toggleMobileNav,
778
- openIntegrations,
779
- closeIntegrations,
780
- openFromIntegrations,
781
- openPersonalSetup,
782
- closePersonalSetup,
783
- openFromPersonal,
784
- openWorkspaceSettings,
785
- closeWorkspaceSettings,
786
- setWorkspaceSettingsTab,
787
- openAccountSettings,
788
- openContentStorageSettings,
789
- clearAccountSettingsScrollTarget,
790
- closeAccountSettings,
791
- setAccountSettingsTab,
792
- openObservabilityConnection,
793
- closeObservabilityConnection,
794
- openOperatorDashboard,
795
- closeOperatorDashboard,
796
- openPackageRegistries,
797
- closePackageRegistries,
798
- openApiTokens,
799
- closeApiTokens,
800
- openProviderConnection,
801
- closeProviderConnection,
802
- openEnvironmentSetup,
803
- closeEnvironmentSetup,
804
- openModelConfig,
805
- closeModelConfig,
806
- openVendorCredentials,
807
- setVendorCredentialsTab,
808
- closeVendorCredentials,
809
- openLocalModels,
810
- closeLocalModels,
811
- openSandbox,
812
- closeSandbox,
813
- openUserSecrets,
814
- closeUserSecrets,
815
- openOpenRouter,
816
- closeOpenRouter,
817
792
  openAiProviderSetup,
818
793
  closeAiProviderSetup,
819
794
  openAiPresetMismatch,
@@ -823,3 +798,95 @@ export function createUiModals() {
823
798
  resetAiOnboarding,
824
799
  }
825
800
  }
801
+
802
+ /**
803
+ * The modal / panel slice of the UI store: every open-close flag for the dozens of modals,
804
+ * panels and hubs (document + task import, bootstrap, integrations, workspace/account settings,
805
+ * infrastructure, vendor credentials, the startup health advisories, the AI-onboarding surfaces,
806
+ * …), their deep-link params, and the hub came-from markers. Split out of the navigation +
807
+ * result-view state per refactoring candidate #4 so the god-object's modal churn is contained to
808
+ * one place. Composed into {@link useUiStore} with the same public names, so consumers are
809
+ * unchanged. The state itself is grouped into cohesive sub-slices (health advisories, document +
810
+ * task sources, integration panels, settings, infrastructure, AI onboarding), composed here behind
811
+ * the shared hub came-from markers.
812
+ */
813
+ export function createUiModals() {
814
+ // Integrations / My-setup hub came-from markers — the one piece of state SHARED across slices
815
+ // (many `open*` handlers reset it), so it lives here and `resetHubReturn` is threaded into the
816
+ // slices that need it. `cameFromIntegrations` is true while an integration's own panel is showing
817
+ // AND it was reached from the Integrations hub; `cameFromPersonal` is the My-setup analogue.
818
+ const cameFromIntegrations = ref(false)
819
+ const cameFromPersonal = ref(false)
820
+ const integrationsOpen = ref(false)
821
+ const personalSetupOpen = ref(false)
822
+
823
+ // Clear BOTH hub came-from markers. Every direct `open*` in the slices calls this so that a
824
+ // panel opened outside the hubs never grows a dead Back control, and so switching from one
825
+ // hub's panel to the other's clears the stale marker.
826
+ function resetHubReturn() {
827
+ cameFromIntegrations.value = false
828
+ cameFromPersonal.value = false
829
+ }
830
+ function openIntegrations() {
831
+ // Reaching the hub itself (fresh, or via a panel's Back control) clears the
832
+ // came-from markers — we're at the hub, not inside a hub-spawned panel.
833
+ resetHubReturn()
834
+ integrationsOpen.value = true
835
+ }
836
+ function closeIntegrations() {
837
+ integrationsOpen.value = false
838
+ }
839
+ function openPersonalSetup() {
840
+ resetHubReturn()
841
+ personalSetupOpen.value = true
842
+ }
843
+ function closePersonalSetup() {
844
+ personalSetupOpen.value = false
845
+ }
846
+ // Open a user-scoped panel FROM the My-setup hub: run its open handler (which resets the
847
+ // markers), then mark that we came from My setup and dismiss it, so the panel's
848
+ // IntegrationBackTitle returns here rather than to the workspace Integrations hub.
849
+ function openFromPersonal(open: () => void) {
850
+ open()
851
+ cameFromPersonal.value = true
852
+ personalSetupOpen.value = false
853
+ }
854
+ // Open an integration's own panel FROM the hub: run its open handler (which resets
855
+ // `cameFromIntegrations`), then mark that we came from the hub and dismiss it. The
856
+ // panel reads `cameFromIntegrations` to show its Back control.
857
+ function openFromIntegrations(open: () => void) {
858
+ open()
859
+ cameFromIntegrations.value = true
860
+ integrationsOpen.value = false
861
+ }
862
+
863
+ const health = createHealthAdvisoryModals()
864
+ const misc = createMiscModals()
865
+ const documentsTasks = createDocumentTaskModals(resetHubReturn)
866
+ const overlays = createOverlayModals()
867
+ const panels = createIntegrationPanelModals(resetHubReturn)
868
+ const settings = createSettingsModals(resetHubReturn)
869
+ const infra = createInfraModals(resetHubReturn)
870
+ const ai = createAiOnboardingModals()
871
+
872
+ return {
873
+ integrationsOpen,
874
+ cameFromIntegrations,
875
+ personalSetupOpen,
876
+ cameFromPersonal,
877
+ openIntegrations,
878
+ closeIntegrations,
879
+ openFromIntegrations,
880
+ openPersonalSetup,
881
+ closePersonalSetup,
882
+ openFromPersonal,
883
+ ...health,
884
+ ...misc,
885
+ ...documentsTasks,
886
+ ...overlays,
887
+ ...panels,
888
+ ...settings,
889
+ ...infra,
890
+ ...ai,
891
+ }
892
+ }