@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.
package/app/stores/ui.ts CHANGED
@@ -1,1082 +1,29 @@
1
1
  import { defineStore } from 'pinia'
2
- import { ref, computed } from 'vue'
3
- import type { DocumentSourceKind, TaskSourceKind, LodLevel, InfraSetupArea } from '~/types/domain'
4
- import type { PendingContext } from '~/composables/useContextLinking'
5
- import { zoomToLod } from '~/composables/useSemanticZoom'
6
- import { useExecutionStore } from '~/stores/execution'
7
- import { agentKindMeta } from '~/utils/catalog'
2
+ import { createUiNavigation } from '~/stores/ui/navigation'
3
+ import { createUiResultViews } from '~/stores/ui/resultViews'
4
+ import { createUiModals } from '~/stores/ui/modals'
8
5
 
9
- /** Values used to seed the add-task form when it is opened from another surface. */
10
- export interface AddTaskPrefill {
11
- title?: string
12
- description?: string
13
- /** Context items staged on the new task (e.g. the source issue), linked once created. */
14
- context?: PendingContext[]
15
- }
6
+ export type { AddTaskPrefill, K3sSetupPrefill } from '~/stores/ui/modals'
16
7
 
17
8
  /**
18
- * Non-secret `local-k3s` connection values captured from the `cat-factory k3s` CLI deep-link
19
- * (`?infraSetup=local-k3s&…`). Mirrors the params `buildK3sSetupUrl` emits (the CLI-side
20
- * `k3s-handler.ts`); the ServiceAccount token is intentionally absent the user pastes it.
9
+ * Transient UI state: selection, panels, zoom level.
10
+ *
11
+ * The concerns are split into cohesive, independently-testable slices under `stores/ui/`
12
+ * (refactoring candidate #4 — the store had grown to 40+ unrelated concerns in one 800-line
13
+ * file): board `navigation` (selection / focus / zoom / LOD), the step `resultViews` overlay
14
+ * seam (`dispatchStepView` / `ui.resultView` + the observability + Kaizen panels), and the
15
+ * `modals` slice (every modal / panel open-close flag, hub markers, deep-link params, and the
16
+ * startup + AI-onboarding advisories). This store composes them behind ONE unchanged public
17
+ * surface, so every existing `useUiStore()` consumer is untouched — the split is internal.
21
18
  */
22
- export interface K3sSetupPrefill {
23
- label: string
24
- apiServerUrl: string
25
- namespaceTemplate: string
26
- hostTemplate: string
27
- // Absent when the link omitted the param, so the form keeps its engine default rather than
28
- // forcing verification back on (which would break a self-signed local cluster).
29
- insecureSkipTlsVerify?: boolean
30
- }
31
-
32
- /** Transient UI state: selection, panels, zoom level. */
33
19
  export const useUiStore = defineStore('ui', () => {
34
- const selectedBlockId = ref<string | null>(null)
35
- const focusBlockId = ref<string | null>(null)
36
- const builderOpen = ref(false)
37
- // Pipeline-health startup advisory: lists invalid pipelines (delete / reseed) + built-ins
38
- // with a newer catalog version (reseed). `pipelineHealthSeen` gates auto-open to once per
39
- // session so it does not re-pop on every snapshot re-hydration.
40
- const pipelineHealthOpen = ref(false)
41
- const pipelineHealthSeen = ref(false)
42
- // Merge-preset health startup advisory: lists built-ins with a newer catalog version (reseed)
43
- // and new built-in presets the workspace can add. `riskPolicyHealthSeen` gates auto-open to
44
- // once per session so it does not re-pop on every snapshot re-hydration (mirrors pipelines).
45
- const riskPolicyHealthOpen = ref(false)
46
- const riskPolicyHealthSeen = ref(false)
47
- // Model-preset health startup advisory: lists built-ins with a newer catalog version (reseed)
48
- // and new built-in presets the workspace can add. `modelPresetHealthSeen` gates auto-open to
49
- // once per session so it does not re-pop on every snapshot re-hydration (mirrors pipelines).
50
- const modelPresetHealthOpen = ref(false)
51
- const modelPresetHealthSeen = ref(false)
52
- const decisionContext = ref<{ instanceId: string; decisionId: string } | null>(null)
53
-
54
- // Document-source integration modals, keyed by source. `documentImport` and
55
- // `spawnPreview` carry an optional target frame, so structure spawned from a
56
- // frame's inspector lands inside that frame rather than creating new top-level
57
- // frames. `documentConnect` carries the source whose connect form to show;
58
- // `documentImport`'s source may be null to let the modal pick a connected one.
59
- const documentConnect = ref<{ source: DocumentSourceKind } | null>(null)
60
- const documentImport = ref<{
61
- source: DocumentSourceKind | null
62
- targetFrameId: string | null
63
- } | null>(null)
64
- // The workspace+DocKind template / exemplar management modal (WS1). A single boolean —
65
- // it manages every kind's links in one place.
66
- const documentTemplates = ref(false)
67
- const spawnPreview = ref<{
68
- source: DocumentSourceKind
69
- externalId: string
70
- targetFrameId: string | null
71
- } | null>(null)
72
-
73
- // Task-source integration modals, keyed by source. `taskConnect` carries the
74
- // source whose connect form to show; `taskImport`'s source may be null to let
75
- // the modal pick a connected one (there is no spawn target — issues are linked
76
- // to a block for context, not expanded into structure).
77
- const taskConnect = ref<{ source: TaskSourceKind } | null>(null)
78
- // `containerId` (a service frame) scopes the modal: it preselects that frame as
79
- // the create-in target AND scopes the issue search to the frame's linked repo.
80
- // Null → the unscoped "import an issue" surface (workspace-wide search).
81
- const taskImport = ref<{ source: TaskSourceKind | null; containerId: string | null } | null>(null)
82
-
83
- // Add-task modal: the container (service frame or module) a new task is being
84
- // added to, or null when closed. The user types the title + description; nothing
85
- // is launched until they explicitly start the created task.
86
- const addTaskContainerId = ref<string | null>(null)
87
- // Optional values to seed the add-task form with when it is opened from another
88
- // surface (e.g. "create task from issue" prefills the title + stages the issue as
89
- // linked context). The user still confirms pipeline / preset before adding.
90
- const addTaskPrefill = ref<AddTaskPrefill | null>(null)
91
-
92
- // Add-recurring-pipeline modal: the service frame a new recurring pipeline is
93
- // being added to, or null when closed (mirrors the add-task flow — a button on
94
- // the frame opens it, scoped to that frame).
95
- const addRecurringFrameId = ref<string | null>(null)
96
-
97
- // Create-initiative modal: the service frame a new initiative is being created
98
- // under, or null when closed (mirrors the add-task flow).
99
- const createInitiativeFrameId = ref<string | null>(null)
100
-
101
- // Repo-bootstrap modal (manage reference architectures + launch a bootstrap).
102
- const bootstrapOpen = ref(false)
103
-
104
- // "Add a service from an existing GitHub repo" modal (no bootstrap run).
105
- const addServiceOpen = ref(false)
106
-
107
- // GitHub integration panel (connection management + repo/PR/issue browsing).
108
- const githubOpen = ref(false)
109
-
110
- // Slack integration panel (connect the account's Slack + per-workspace routing).
111
- const slackOpen = ref(false)
112
-
113
- // Prompt-fragment library panel (manage the board's best-practice catalog +
114
- // linked guideline repos; ADR 0006).
115
- const fragmentLibraryOpen = ref(false)
116
-
117
- // Command bar (⌘K) — searchable launcher for every navbar action.
118
- const commandBarOpen = ref(false)
119
-
120
- // Keyboard-shortcuts cheatsheet (?) — a modal listing every global shortcut.
121
- const shortcutsHelpOpen = ref(false)
122
-
123
- // Mobile navigation drawer: on compact (< lg) viewports the SideBar is an
124
- // off-canvas drawer toggled by a hamburger; on lg+ it is a static aside and this
125
- // flag is ignored. Closed on any nav action so the board is revealed immediately.
126
- const mobileNavOpen = ref(false)
127
-
128
- // Integrations hub: a single modal listing every external system the workspace
129
- // can enable/link (GitHub, Slack, document + task sources, Datadog, LLM vendors,
130
- // local runners, OpenRouter). Replaces the per-integration navbar buttons; each
131
- // row inside it opens that integration's own panel via the handlers below.
132
- const integrationsOpen = ref(false)
133
- // True while an integration's own panel is showing AND it was reached from the hub
134
- // (not the command bar, sidebar, a banner or an inspector link). Drives the "Back to
135
- // Integrations" control those panels render: it only offers a return path when there
136
- // is one. Every direct `open*` below resets it; `openFromIntegrations` sets it.
137
- const cameFromIntegrations = ref(false)
138
-
139
- // Personal "My setup" hub: a user-scoped sibling of the Integrations hub, listing the
140
- // signed-in user's own connections (GitHub PAT, local runners, personal subscriptions)
141
- // separated out of the workspace-scoped Integrations hub. `cameFromPersonal` is the
142
- // symmetric came-from marker, so a panel reached from here renders a "Back to My setup"
143
- // control instead of "Back to Integrations".
144
- const personalSetupOpen = ref(false)
145
- const cameFromPersonal = ref(false)
146
-
147
- // Workspace-settings modal: a single tabbed window gathering the workspace-wide
148
- // config (workspace / merge thresholds / issue writeback / service best practices).
149
- // `workspaceSettingsTab` lets other surfaces deep-link straight to a tab.
150
- const workspaceSettingsOpen = ref(false)
151
- const workspaceSettingsTab = ref('workspace')
152
- // Account-settings modal: a single tabbed window for the per-account configuration —
153
- // the team panel (members + roles + invitations + email sender + account API keys,
154
- // `AccountTeamSettings`) and the account-tier prompt-fragment library. Account-scoped
155
- // (distinct from workspace settings). `accountSettingsTab` lets other surfaces deep-link
156
- // straight to a tab.
157
- const accountSettingsOpen = ref(false)
158
- const accountSettingsTab = ref('team')
159
- // A one-shot deep-link anchor: when a surface opens account settings AND wants to land on a
160
- // specific section within the (long) tab body, it sets this to that section's id. The owning
161
- // panel scrolls the matching element into view once, then calls `clearAccountSettingsScrollTarget`
162
- // so a later plain open doesn't re-scroll. Null when no section was requested.
163
- const accountSettingsScrollTarget = ref<string | null>(null)
164
- // Observability integration: the post-release-health connection panel (Datadog
165
- // today, pluggable). NB: distinct from `observabilityInstanceId` below, which is the
166
- // LLM per-call observability panel.
167
- const observabilityConnectionOpen = ref(false)
168
- // Private package registries: the workspace's npm/GitHub-Packages entries agent
169
- // containers install with. Opened from the Integrations hub.
170
- const packageRegistriesOpen = ref(false)
171
- // API access tokens: the workspace's inbound public-API keys external systems present to
172
- // the `/api/v1` surface. Opened from the Integrations hub.
173
- const apiTokensOpen = ref(false)
174
- // The single tabbed Infrastructure window — a TOP-LEVEL navbar destination (no longer
175
- // reached via the Integrations hub). Two topical tabs: "Agent containers" (the execution
176
- // backend + self-hosted runner pool, plus the local-mode warm pool/checkout) and "Test
177
- // environments" (the ephemeral-environment provider). `infrastructureOpen` is the modal
178
- // flag; `infrastructureTab` selects the tab. `openInfrastructure()` is the navbar entry;
179
- // `openProviderConnection(kind)` remains for deep-links (a banner's "Configure…" button).
180
- const infrastructureOpen = ref(false)
181
- const infrastructureTab = ref<'environment' | 'runner-pool'>('runner-pool')
182
- // Non-secret prefill captured from the `cat-factory k3s` CLI deep-link (see
183
- // `consumeK3sSetupDeepLink`). When set, the Test-environments tab's kube engine form seeds the
184
- // `local-k3s` connection from it; the ServiceAccount token is deliberately NOT in the link (a
185
- // secret in a URL leaks into history/logs), so the user still pastes it before Test → Save.
186
- const k3sSetupPrefill = ref<K3sSetupPrefill | null>(null)
187
- // Environment setup wizard (shared-stacks slice 7): the guided detect → review → preflight →
188
- // trial → save flow for a service frame's `docker-compose` provisioning. `environmentWizardOpen`
189
- // is the modal flag; `environmentWizardFrameId` preselects the service frame the flow targets
190
- // (set when launched from a frame's inspector nudge; null ⇒ the wizard's pick step chooses one).
191
- const environmentWizardOpen = ref(false)
192
- const environmentWizardFrameId = ref<string | null>(null)
193
- const modelConfigOpen = ref(false)
194
- // LLM-vendor subscription credentials (the token pool powering the Claude Code
195
- // / Codex harnesses). `vendorCredentialsTab` lets a caller deep-link to one tab —
196
- // the user-scoped "My subscriptions" entry opens straight onto the `personal` tab.
197
- const vendorCredentialsOpen = ref(false)
198
- const vendorCredentialsTab = ref('pool')
199
- // Per-user settings panel: the signed-in user's own-machine local model runners.
200
- const localModelsOpen = ref(false)
201
- // The Sandbox (parallel prompt/model testing) surface — an opt-in, on-demand window.
202
- const sandboxOpen = ref(false)
203
- const userSecretsOpen = ref(false)
204
- // Per-workspace settings panel: the OpenRouter dynamic catalog (browse/enable gateway models).
205
- const openRouterOpen = ref(false)
206
-
207
- // AI-onboarding surfaces (driven by `useAiReadiness`). `aiProviderSetupOpen` is the
208
- // "no usable AI source" dialog; `aiPresetMismatchOpen` is the "default preset points at
209
- // unavailable models" dialog. The `*Dismissed` flags are per-session: they suppress the
210
- // auto-open (and let the banner be dismissed) without permanently hiding the prompt — it
211
- // re-evaluates on the next load. Both clear themselves once the underlying gap is closed.
212
- const aiProviderSetupOpen = ref(false)
213
- const aiPresetMismatchOpen = ref(false)
214
- const aiSetupDismissed = ref(false)
215
- const aiPresetDismissed = ref(false)
216
-
217
- // Infra-setup banner: per-SESSION dismissals, one flag per area, cleared on workspace switch
218
- // exactly like the AI-onboarding flags (a dismissal in one workspace must not suppress the
219
- // independent prompt for another). The PERMANENT "don't notify me again" dismissal is per-USER
220
- // and persists in localStorage from the banner component; this only covers "hide for now".
221
- const infraSetupSessionDismissed = ref<InfraSetupArea[]>([])
222
- function dismissInfraSetupForSession(area: InfraSetupArea) {
223
- if (!infraSetupSessionDismissed.value.includes(area))
224
- infraSetupSessionDismissed.value = [...infraSetupSessionDismissed.value, area]
225
- }
226
- function resetInfraSetupDismissals() {
227
- infraSetupSessionDismissed.value = []
228
- }
229
-
230
- // Dedicated result-view overlay: a step whose agent kind declares a bespoke
231
- // visualization (via the archetype's `resultView`) opens here instead of the generic
232
- // prose step-detail panel. `view` is the registry id (e.g. 'requirements-review');
233
- // `blockId` is always set; `instanceId`/`stepIndex` are present on the pipeline path and
234
- // null for an off-path open (e.g. the inspector's pre-start requirements review).
235
- const resultView = ref<{
236
- view: string
237
- blockId: string
238
- instanceId: string | null
239
- stepIndex: number | null
240
- // The brainstorm dialogue stage, set only when `view === 'brainstorm'` (its two agent
241
- // kinds share one window). Derived from the step's agent kind on the pipeline path, or
242
- // passed explicitly on an off-path open.
243
- stage?: 'requirements' | 'architecture'
244
- } | null>(null)
245
-
246
- // Agent step-detail overlay: which pipeline step (a run instance + step index)
247
- // a human is inspecting, or null when closed. The overlay resolves the step
248
- // from the execution store so it stays live; it shows the step's metadata
249
- // (model, state, progress, subtasks, …) and — when the agent produced prose —
250
- // a reader for it (ToC + collapsible sections).
251
- const stepDetail = ref<{ instanceId: string; stepIndex: number } | null>(null)
252
-
253
- // LLM observability panel: which run (execution instance) a human is inspecting
254
- // the per-call model activity for, or null when closed. The panel loads the full
255
- // per-call detail from the observability store on open.
256
- const observabilityInstanceId = ref<string | null>(null)
257
-
258
- // The Kaizen screen (grading history + verified-combo library), a full-panel overlay
259
- // opened from the sidebar. Distinct from the per-run grading status shown in run details.
260
- const kaizenScreenOpen = ref(false)
261
-
262
- /** Current canvas zoom (driven by Vue Flow viewport). */
263
- const zoom = ref(1)
264
-
265
- const lod = computed<LodLevel>(() => zoomToLod(zoom.value))
266
-
267
- /** Frames the user has manually expanded to reveal their tasks. */
268
- const expandedFrames = ref<Set<string>>(new Set())
269
-
270
- function toggleFrame(id: string) {
271
- const next = new Set(expandedFrames.value)
272
- if (next.has(id)) next.delete(id)
273
- else next.add(id)
274
- expandedFrames.value = next
275
- }
276
-
277
- function expandFrame(id: string) {
278
- if (expandedFrames.value.has(id)) return
279
- expandedFrames.value = new Set(expandedFrames.value).add(id)
280
- }
281
-
282
- /** Services are always expanded to their task canvas, at every zoom level, so the
283
- * board layout is fixed: panning never changes it and zooming has no expand/collapse
284
- * transition to snap on. (`expandedFrames`/`toggleFrame` are retained for callers but
285
- * no longer gate rendering.) */
286
- function isFrameExpanded(_id: string) {
287
- return true
288
- }
289
-
290
- function select(id: string | null) {
291
- selectedBlockId.value = id
292
- }
293
-
294
- function focus(id: string | null) {
295
- focusBlockId.value = id
296
- }
297
-
298
- function openBuilder() {
299
- builderOpen.value = true
300
- }
301
-
302
- /** Auto-open the pipeline-health advisory once per session (no-op after it's been shown). */
303
- function maybeOpenPipelineHealth() {
304
- if (pipelineHealthSeen.value) return
305
- pipelineHealthSeen.value = true
306
- pipelineHealthOpen.value = true
307
- }
308
-
309
- function openPipelineHealth() {
310
- pipelineHealthSeen.value = true
311
- pipelineHealthOpen.value = true
312
- }
313
-
314
- function closePipelineHealth() {
315
- pipelineHealthOpen.value = false
316
- }
317
-
318
- /** Auto-open the merge-preset health advisory once per session (no-op after it's been shown). */
319
- function maybeOpenRiskPolicyHealth() {
320
- if (riskPolicyHealthSeen.value) return
321
- riskPolicyHealthSeen.value = true
322
- riskPolicyHealthOpen.value = true
323
- }
324
-
325
- function openRiskPolicyHealth() {
326
- riskPolicyHealthSeen.value = true
327
- riskPolicyHealthOpen.value = true
328
- }
329
-
330
- function closeRiskPolicyHealth() {
331
- riskPolicyHealthOpen.value = false
332
- }
333
-
334
- /** Auto-open the model-preset health advisory once per session (no-op after it's been shown). */
335
- function maybeOpenModelPresetHealth() {
336
- if (modelPresetHealthSeen.value) return
337
- modelPresetHealthSeen.value = true
338
- modelPresetHealthOpen.value = true
339
- }
340
-
341
- function openModelPresetHealth() {
342
- modelPresetHealthSeen.value = true
343
- modelPresetHealthOpen.value = true
344
- }
345
-
346
- function closeModelPresetHealth() {
347
- modelPresetHealthOpen.value = false
348
- }
349
-
350
- function openDecision(instanceId: string, decisionId: string) {
351
- decisionContext.value = { instanceId, decisionId }
352
- }
353
-
354
- function closeDecision() {
355
- decisionContext.value = null
356
- }
357
-
358
- /**
359
- * Open a pending approval gate in the conclusions reader (approval mode). Resolves
360
- * the step index from the gate id so every board/inspector entry point can keep
361
- * passing the approval id it already has.
362
- */
363
- function openApprovalDetail(instanceId: string, approvalId: string) {
364
- const execution = useExecutionStore()
365
- const instance = execution.getInstance(instanceId)
366
- const idx = instance?.steps.findIndex((s) => s.approval?.id === approvalId) ?? -1
367
- if (idx >= 0) dispatchStepView(instanceId, idx)
368
- }
369
-
370
- /**
371
- * Open a pipeline step: route it to its agent kind's DEDICATED result window when the
372
- * archetype declares one (the universal `resultView` seam), else the generic prose
373
- * step-detail panel. This is the single dispatch every board/inspector entry point uses,
374
- * so adding a bespoke window for a new agent is just declaring `resultView` + registering
375
- * a component — no caller changes.
376
- */
377
- function dispatchStepView(instanceId: string, stepIndex: number) {
378
- const execution = useExecutionStore()
379
- const instance = execution.getInstance(instanceId)
380
- const step = instance?.steps[stepIndex]
381
- // A step that actually ran the consensus mechanism opens the dedicated Consensus
382
- // Session window, regardless of its kind's normal result view — consensus is an
383
- // execution MODE on a kind, not a kind, so it can't be a static archetype `resultView`.
384
- const view = step?.consensus?.enabled
385
- ? 'consensus-session'
386
- : step
387
- ? agentKindMeta(step.agentKind).resultView
388
- : undefined
389
- if (view && instance) {
390
- // The brainstorm window is shared by both stages; carry which one from the step's kind.
391
- const stage =
392
- view === 'brainstorm'
393
- ? step?.agentKind === 'architecture-brainstorm'
394
- ? 'architecture'
395
- : 'requirements'
396
- : undefined
397
- resultView.value = {
398
- view,
399
- blockId: instance.blockId,
400
- instanceId,
401
- stepIndex,
402
- ...(stage ? { stage } : {}),
403
- }
404
- return
405
- }
406
- stepDetail.value = { instanceId, stepIndex }
407
- }
408
-
409
- function openDocumentConnect(source: DocumentSourceKind) {
410
- resetHubReturn()
411
- documentConnect.value = { source }
412
- }
413
- function closeDocumentConnect() {
414
- documentConnect.value = null
415
- }
416
- function openDocumentImport(
417
- targetFrameId: string | null = null,
418
- source: DocumentSourceKind | null = null,
419
- ) {
420
- resetHubReturn()
421
- documentImport.value = { source, targetFrameId }
422
- }
423
- function closeDocumentImport() {
424
- documentImport.value = null
425
- }
426
- function openDocumentTemplates() {
427
- resetHubReturn()
428
- documentTemplates.value = true
429
- }
430
- function closeDocumentTemplates() {
431
- documentTemplates.value = false
432
- }
433
- function openSpawnPreview(
434
- source: DocumentSourceKind,
435
- externalId: string,
436
- targetFrameId: string | null = null,
437
- ) {
438
- spawnPreview.value = { source, externalId, targetFrameId }
439
- }
440
- function closeSpawnPreview() {
441
- spawnPreview.value = null
442
- }
443
- function openTaskConnect(source: TaskSourceKind) {
444
- resetHubReturn()
445
- taskConnect.value = { source }
446
- }
447
- function closeTaskConnect() {
448
- taskConnect.value = null
449
- }
450
- function openTaskImport(source: TaskSourceKind | null = null, containerId: string | null = null) {
451
- resetHubReturn()
452
- taskImport.value = { source, containerId }
453
- }
454
- function closeTaskImport() {
455
- taskImport.value = null
456
- }
457
- function openAddTask(containerId: string, prefill: AddTaskPrefill | null = null) {
458
- addTaskPrefill.value = prefill
459
- addTaskContainerId.value = containerId
460
- }
461
- function closeAddTask() {
462
- addTaskContainerId.value = null
463
- addTaskPrefill.value = null
464
- }
465
- function openAddRecurring(frameId: string) {
466
- addRecurringFrameId.value = frameId
467
- }
468
- function closeAddRecurring() {
469
- addRecurringFrameId.value = null
470
- }
471
- function openCreateInitiative(frameId: string) {
472
- createInitiativeFrameId.value = frameId
473
- }
474
- function closeCreateInitiative() {
475
- createInitiativeFrameId.value = null
476
- }
477
- function openBootstrap() {
478
- bootstrapOpen.value = true
479
- }
480
- function closeBootstrap() {
481
- bootstrapOpen.value = false
482
- }
483
- function openAddService() {
484
- addServiceOpen.value = true
485
- }
486
- function closeAddService() {
487
- addServiceOpen.value = false
488
- }
489
- function openGitHub() {
490
- resetHubReturn()
491
- githubOpen.value = true
492
- }
493
- function closeGitHub() {
494
- githubOpen.value = false
495
- }
496
- function openSlack() {
497
- resetHubReturn()
498
- slackOpen.value = true
499
- }
500
- function closeSlack() {
501
- slackOpen.value = false
502
- }
503
- function openFragmentLibrary() {
504
- fragmentLibraryOpen.value = true
505
- }
506
- function closeFragmentLibrary() {
507
- fragmentLibraryOpen.value = false
508
- }
509
- function openCommandBar() {
510
- commandBarOpen.value = true
511
- }
512
- function closeCommandBar() {
513
- commandBarOpen.value = false
514
- }
515
- function toggleCommandBar() {
516
- commandBarOpen.value = !commandBarOpen.value
517
- }
518
- function openShortcutsHelp() {
519
- shortcutsHelpOpen.value = true
520
- }
521
- function closeShortcutsHelp() {
522
- shortcutsHelpOpen.value = false
523
- }
524
- function toggleShortcutsHelp() {
525
- shortcutsHelpOpen.value = !shortcutsHelpOpen.value
526
- }
527
- function openMobileNav() {
528
- mobileNavOpen.value = true
529
- }
530
- function closeMobileNav() {
531
- mobileNavOpen.value = false
532
- }
533
- function toggleMobileNav() {
534
- mobileNavOpen.value = !mobileNavOpen.value
535
- }
536
- // Clear BOTH hub came-from markers. Every direct `open*` below calls this so that a
537
- // panel opened outside the hubs never grows a dead Back control, and so switching from
538
- // one hub's panel to the other's clears the stale marker.
539
- function resetHubReturn() {
540
- cameFromIntegrations.value = false
541
- cameFromPersonal.value = false
542
- }
543
- function openIntegrations() {
544
- // Reaching the hub itself (fresh, or via a panel's Back control) clears the
545
- // came-from markers — we're at the hub, not inside a hub-spawned panel.
546
- resetHubReturn()
547
- integrationsOpen.value = true
548
- }
549
- function closeIntegrations() {
550
- integrationsOpen.value = false
551
- }
552
- function openPersonalSetup() {
553
- resetHubReturn()
554
- personalSetupOpen.value = true
555
- }
556
- function closePersonalSetup() {
557
- personalSetupOpen.value = false
558
- }
559
- // Open a user-scoped panel FROM the My-setup hub: run its open handler (which resets the
560
- // markers), then mark that we came from My setup and dismiss it, so the panel's
561
- // IntegrationBackTitle returns here rather than to the workspace Integrations hub.
562
- function openFromPersonal(open: () => void) {
563
- open()
564
- cameFromPersonal.value = true
565
- personalSetupOpen.value = false
566
- }
567
- // Open an integration's own panel FROM the hub: run its open handler (which resets
568
- // `cameFromIntegrations`), then mark that we came from the hub and dismiss it. The
569
- // panel reads `cameFromIntegrations` to show its Back control.
570
- function openFromIntegrations(open: () => void) {
571
- open()
572
- cameFromIntegrations.value = true
573
- integrationsOpen.value = false
574
- }
575
- function openWorkspaceSettings(tab = 'workspace') {
576
- resetHubReturn()
577
- workspaceSettingsTab.value = tab
578
- workspaceSettingsOpen.value = true
579
- }
580
- function closeWorkspaceSettings() {
581
- workspaceSettingsOpen.value = false
582
- }
583
- function setWorkspaceSettingsTab(tab: string) {
584
- workspaceSettingsTab.value = tab
585
- }
586
- function openAccountSettings(tab = 'team') {
587
- resetHubReturn()
588
- accountSettingsTab.value = tab
589
- accountSettingsOpen.value = true
590
- }
591
- // Deep-link to the content (binary-artifact) storage configuration, which lives near the
592
- // bottom of the account settings' team tab (`AccountDeploymentSettings`). Used by the
593
- // pipeline-start error prompt when a storage-reliant agent (the UI Tester) has no storage
594
- // configured. Sets a scroll anchor so the panel brings the storage section into view rather
595
- // than dropping the user at the top of the long team tab to hunt for it.
596
- function openContentStorageSettings() {
597
- accountSettingsScrollTarget.value = 'content-storage'
598
- openAccountSettings('team')
599
- }
600
- function clearAccountSettingsScrollTarget() {
601
- accountSettingsScrollTarget.value = null
602
- }
603
- function closeAccountSettings() {
604
- accountSettingsOpen.value = false
605
- accountSettingsScrollTarget.value = null
606
- }
607
- function setAccountSettingsTab(tab: string) {
608
- accountSettingsTab.value = tab
609
- }
610
- function openObservabilityConnection() {
611
- resetHubReturn()
612
- observabilityConnectionOpen.value = true
613
- }
614
- function closeObservabilityConnection() {
615
- observabilityConnectionOpen.value = false
616
- }
617
- function openPackageRegistries() {
618
- resetHubReturn()
619
- packageRegistriesOpen.value = true
620
- }
621
- function closePackageRegistries() {
622
- packageRegistriesOpen.value = false
623
- }
624
- function openApiTokens() {
625
- resetHubReturn()
626
- apiTokensOpen.value = true
627
- }
628
- function closeApiTokens() {
629
- apiTokensOpen.value = false
630
- }
631
- // Top-level navbar entry into the Infrastructure window. No hub-return marker (it isn't
632
- // reached from the Integrations hub), so the window shows no "Back to Integrations" control.
633
- function openInfrastructure(tab: 'environment' | 'runner-pool' = 'runner-pool') {
634
- resetHubReturn()
635
- infrastructureTab.value = tab
636
- infrastructureOpen.value = true
637
- }
638
- function openProviderConnection(kind: 'environment' | 'runner-pool') {
639
- resetHubReturn()
640
- infrastructureTab.value = kind
641
- infrastructureOpen.value = true
642
- }
643
- function closeProviderConnection() {
644
- infrastructureOpen.value = false
645
- // Drop any consumed CLI prefill so re-opening the window normally doesn't re-seed the form.
646
- k3sSetupPrefill.value = null
647
- }
648
- // Capture a `cat-factory k3s` deep-link (`?infraSetup=local-k3s&…`) on app load: stash the
649
- // non-secret connection values, open the Infrastructure window on the Test-environments tab so
650
- // the kube engine form seeds from them, then strip the params from the URL (mirrors the
651
- // `?invite=` handling in the auth store) so a reload doesn't re-trigger and the link isn't left
652
- // in history. No-op when the query param is absent.
653
- function consumeK3sSetupDeepLink() {
654
- if (typeof window === 'undefined') return
655
- const params = new URLSearchParams(window.location.search)
656
- if (params.get('infraSetup') !== 'local-k3s') return
657
- k3sSetupPrefill.value = {
658
- label: params.get('label') ?? 'Local k3s',
659
- apiServerUrl: params.get('apiServerUrl') ?? '',
660
- namespaceTemplate: params.get('namespaceTemplate') ?? '',
661
- hostTemplate: params.get('hostTemplate') ?? '',
662
- // Only carry the flag the link actually set — a missing param leaves the form's engine
663
- // default (skip-TLS on for a local self-signed cluster) untouched.
664
- insecureSkipTlsVerify: params.has('insecureSkipTlsVerify')
665
- ? params.get('insecureSkipTlsVerify') === '1'
666
- : undefined,
667
- }
668
- resetHubReturn()
669
- infrastructureTab.value = 'environment'
670
- infrastructureOpen.value = true
671
- for (const key of [
672
- 'infraSetup',
673
- 'label',
674
- 'apiServerUrl',
675
- 'namespaceTemplate',
676
- 'hostTemplate',
677
- 'insecureSkipTlsVerify',
678
- ]) {
679
- params.delete(key)
680
- }
681
- const qs = params.toString()
682
- history.replaceState(null, '', window.location.pathname + (qs ? `?${qs}` : ''))
683
- }
684
- // Launch the environment setup wizard, optionally preselecting the service frame it targets
685
- // (the inspector nudge passes the frame; the navbar entry opens it with the pick step active).
686
- function openEnvironmentSetup(frameId: string | null = null) {
687
- resetHubReturn()
688
- environmentWizardFrameId.value = frameId
689
- environmentWizardOpen.value = true
690
- }
691
- function closeEnvironmentSetup() {
692
- environmentWizardOpen.value = false
693
- environmentWizardFrameId.value = null
694
- }
695
- function openModelConfig() {
696
- modelConfigOpen.value = true
697
- }
698
- function closeModelConfig() {
699
- modelConfigOpen.value = false
700
- }
701
- function openVendorCredentials(tab = 'pool') {
702
- resetHubReturn()
703
- vendorCredentialsTab.value = tab
704
- vendorCredentialsOpen.value = true
705
- }
706
- function setVendorCredentialsTab(tab: string) {
707
- vendorCredentialsTab.value = tab
708
- }
709
- function closeVendorCredentials() {
710
- vendorCredentialsOpen.value = false
711
- }
712
- function openLocalModels() {
713
- resetHubReturn()
714
- localModelsOpen.value = true
715
- }
716
- function closeLocalModels() {
717
- localModelsOpen.value = false
718
- }
719
- function openSandbox() {
720
- sandboxOpen.value = true
721
- }
722
- function closeSandbox() {
723
- sandboxOpen.value = false
724
- }
725
- function openUserSecrets() {
726
- resetHubReturn()
727
- userSecretsOpen.value = true
728
- }
729
- function closeUserSecrets() {
730
- userSecretsOpen.value = false
731
- }
732
- function openOpenRouter() {
733
- resetHubReturn()
734
- openRouterOpen.value = true
735
- }
736
- function closeOpenRouter() {
737
- openRouterOpen.value = false
738
- }
739
- function openAiProviderSetup() {
740
- aiProviderSetupOpen.value = true
741
- }
742
- function closeAiProviderSetup() {
743
- aiProviderSetupOpen.value = false
744
- }
745
- function openAiPresetMismatch() {
746
- aiPresetMismatchOpen.value = true
747
- }
748
- function closeAiPresetMismatch() {
749
- aiPresetMismatchOpen.value = false
750
- }
751
- // Banner dismissal is distinct from closing the dialog: closing the dialog leaves the
752
- // banner so the user can reopen it; dismissing the banner hides the whole prompt for
753
- // the session (it re-evaluates on the next load).
754
- function dismissAiSetup() {
755
- aiProviderSetupOpen.value = false
756
- aiSetupDismissed.value = true
757
- }
758
- function dismissAiPresetMismatch() {
759
- aiPresetMismatchOpen.value = false
760
- aiPresetDismissed.value = true
761
- }
762
- // Clear the per-session AI-onboarding state (open dialogs + dismissed flags). Called on
763
- // workspace switch: dismissals are per-session-per-workspace, so a prompt dismissed in one
764
- // workspace must not suppress the (independent) prompt for another workspace that also
765
- // lacks a usable AI source / has a broken default preset.
766
- function resetAiOnboarding() {
767
- aiProviderSetupOpen.value = false
768
- aiPresetMismatchOpen.value = false
769
- aiSetupDismissed.value = false
770
- aiPresetDismissed.value = false
771
- }
772
- function openRequirementReview(blockId: string) {
773
- resultView.value = { view: 'requirements-review', blockId, instanceId: null, stepIndex: null }
774
- }
775
- function openClarityReview(blockId: string) {
776
- resultView.value = { view: 'clarity-review', blockId, instanceId: null, stepIndex: null }
777
- }
778
- function openBrainstorm(blockId: string, stage: 'requirements' | 'architecture') {
779
- resultView.value = { view: 'brainstorm', blockId, instanceId: null, stepIndex: null, stage }
780
- }
781
- // Open the service-spec window for a service frame (the inspector's "View Requirements").
782
- function openServiceSpec(blockId: string) {
783
- resultView.value = { view: 'service-spec', blockId, instanceId: null, stepIndex: null }
784
- }
785
- // Open the initiative tracker window for an initiative block (board card / inspector).
786
- function openInitiativeTracker(blockId: string) {
787
- resultView.value = { view: 'initiative-tracker', blockId, instanceId: null, stepIndex: null }
788
- }
789
- // Open the interactive-planning Q&A window for an initiative block (inspector / card,
790
- // when the interviewer has parked the planning run with pending questions).
791
- function openInitiativePlanning(blockId: string) {
792
- resultView.value = { view: 'initiative-planning', blockId, instanceId: null, stepIndex: null }
793
- }
794
- // Open the Follow-up companion window for a run's Coder step (the blinking chip + the
795
- // `followup_pending` notification). Resolves the Coder step index from the run when not
796
- // given, so callers that only know the run can still open it.
797
- function openFollowUps(instanceId: string, stepIndex: number | null = null) {
798
- const execution = useExecutionStore()
799
- const instance = execution.getInstance(instanceId)
800
- if (!instance) return
801
- // A pipeline may carry more than one follow-up-enabled Coder step, so don't blindly pick
802
- // the first when no index is given: prefer the step that still has undecided items (the
803
- // one the run is parked on), else the current step, else the first enabled one.
804
- const resolveIdx = () => {
805
- const pending = instance.steps.findIndex(
806
- (s) => s.followUps?.enabled && s.followUps.items.some((i) => i.status === 'pending'),
807
- )
808
- if (pending >= 0) return pending
809
- const current = instance.steps[instance.currentStep]
810
- if (current?.followUps?.enabled) return instance.currentStep
811
- return instance.steps.findIndex((s) => s.followUps?.enabled)
812
- }
813
- const idx = stepIndex ?? resolveIdx()
814
- if (idx < 0) return
815
- resultView.value = {
816
- view: 'follow-ups',
817
- blockId: instance.blockId,
818
- instanceId,
819
- stepIndex: idx,
820
- }
821
- }
822
- // Open the implementation-fork decision window for a run's coder step (from the inspector /
823
- // pipeline chip / `fork_decision_pending` notification). Resolves the coder step index from
824
- // the run when not given, preferring the step parked awaiting a choice.
825
- function openForkDecision(instanceId: string, stepIndex: number | null = null) {
826
- const execution = useExecutionStore()
827
- const instance = execution.getInstance(instanceId)
828
- if (!instance) return
829
- const resolveIdx = () => {
830
- const awaiting = instance.steps.findIndex(
831
- (s) => s.agentKind === 'coder' && s.forkDecision?.status === 'awaiting_choice',
832
- )
833
- if (awaiting >= 0) return awaiting
834
- const current = instance.steps[instance.currentStep]
835
- if (current?.agentKind === 'coder' && current.forkDecision) return instance.currentStep
836
- return instance.steps.findIndex((s) => s.agentKind === 'coder' && s.forkDecision)
837
- }
838
- const idx = stepIndex ?? resolveIdx()
839
- if (idx < 0) return
840
- resultView.value = {
841
- view: 'fork-decision',
842
- blockId: instance.blockId,
843
- instanceId,
844
- stepIndex: idx,
845
- }
846
- }
847
- // Open the PR deep-review window for a run's `pr-reviewer` step (from the `pr_review_ready`
848
- // notification / the step). Resolves the step index from the run when not given, preferring
849
- // the step parked awaiting a finding selection.
850
- function openPrReview(instanceId: string, stepIndex: number | null = null) {
851
- const execution = useExecutionStore()
852
- const instance = execution.getInstance(instanceId)
853
- if (!instance) return
854
- const resolveIdx = () => {
855
- const awaiting = instance.steps.findIndex(
856
- (s) => s.agentKind === 'pr-reviewer' && s.prReview?.status === 'awaiting_selection',
857
- )
858
- if (awaiting >= 0) return awaiting
859
- const current = instance.steps[instance.currentStep]
860
- if (current?.agentKind === 'pr-reviewer' && current.prReview) return instance.currentStep
861
- return instance.steps.findIndex((s) => s.agentKind === 'pr-reviewer' && s.prReview)
862
- }
863
- const idx = stepIndex ?? resolveIdx()
864
- if (idx < 0) return
865
- resultView.value = {
866
- view: 'pr-review',
867
- blockId: instance.blockId,
868
- instanceId,
869
- stepIndex: idx,
870
- }
871
- }
872
- function closeResultView() {
873
- resultView.value = null
874
- }
875
- // Kept name for the requirements window's close handler.
876
- const closeRequirementReview = closeResultView
877
- function openStepDetail(instanceId: string, stepIndex: number) {
878
- dispatchStepView(instanceId, stepIndex)
879
- }
880
- function closeStepDetail() {
881
- stepDetail.value = null
882
- }
883
- function openObservability(instanceId: string) {
884
- observabilityInstanceId.value = instanceId
885
- }
886
- function closeObservability() {
887
- observabilityInstanceId.value = null
888
- }
889
- function openKaizen() {
890
- kaizenScreenOpen.value = true
891
- }
892
- function closeKaizen() {
893
- kaizenScreenOpen.value = false
894
- }
20
+ const navigation = createUiNavigation()
21
+ const resultViews = createUiResultViews()
22
+ const modals = createUiModals()
895
23
 
896
24
  return {
897
- selectedBlockId,
898
- focusBlockId,
899
- builderOpen,
900
- pipelineHealthOpen,
901
- pipelineHealthSeen,
902
- riskPolicyHealthOpen,
903
- riskPolicyHealthSeen,
904
- modelPresetHealthOpen,
905
- modelPresetHealthSeen,
906
- decisionContext,
907
- documentConnect,
908
- documentImport,
909
- documentTemplates,
910
- spawnPreview,
911
- taskConnect,
912
- taskImport,
913
- addTaskContainerId,
914
- addTaskPrefill,
915
- addRecurringFrameId,
916
- createInitiativeFrameId,
917
- bootstrapOpen,
918
- addServiceOpen,
919
- githubOpen,
920
- slackOpen,
921
- fragmentLibraryOpen,
922
- commandBarOpen,
923
- shortcutsHelpOpen,
924
- mobileNavOpen,
925
- integrationsOpen,
926
- cameFromIntegrations,
927
- personalSetupOpen,
928
- cameFromPersonal,
929
- workspaceSettingsOpen,
930
- workspaceSettingsTab,
931
- accountSettingsOpen,
932
- accountSettingsTab,
933
- accountSettingsScrollTarget,
934
- observabilityConnectionOpen,
935
- packageRegistriesOpen,
936
- apiTokensOpen,
937
- infrastructureOpen,
938
- infrastructureTab,
939
- openInfrastructure,
940
- modelConfigOpen,
941
- vendorCredentialsOpen,
942
- vendorCredentialsTab,
943
- localModelsOpen,
944
- sandboxOpen,
945
- userSecretsOpen,
946
- openRouterOpen,
947
- aiProviderSetupOpen,
948
- aiPresetMismatchOpen,
949
- aiSetupDismissed,
950
- aiPresetDismissed,
951
- infraSetupSessionDismissed,
952
- dismissInfraSetupForSession,
953
- resetInfraSetupDismissals,
954
- resultView,
955
- closeResultView,
956
- stepDetail,
957
- observabilityInstanceId,
958
- kaizenScreenOpen,
959
- zoom,
960
- lod,
961
- expandedFrames,
962
- toggleFrame,
963
- expandFrame,
964
- isFrameExpanded,
965
- select,
966
- focus,
967
- openBuilder,
968
- maybeOpenPipelineHealth,
969
- openPipelineHealth,
970
- closePipelineHealth,
971
- maybeOpenRiskPolicyHealth,
972
- openRiskPolicyHealth,
973
- closeRiskPolicyHealth,
974
- maybeOpenModelPresetHealth,
975
- openModelPresetHealth,
976
- closeModelPresetHealth,
977
- openDecision,
978
- closeDecision,
979
- openApprovalDetail,
980
- openDocumentConnect,
981
- closeDocumentConnect,
982
- openDocumentImport,
983
- closeDocumentImport,
984
- openDocumentTemplates,
985
- closeDocumentTemplates,
986
- openSpawnPreview,
987
- closeSpawnPreview,
988
- openTaskConnect,
989
- closeTaskConnect,
990
- openTaskImport,
991
- closeTaskImport,
992
- openAddTask,
993
- closeAddTask,
994
- openAddRecurring,
995
- closeAddRecurring,
996
- openCreateInitiative,
997
- closeCreateInitiative,
998
- openBootstrap,
999
- closeBootstrap,
1000
- openAddService,
1001
- closeAddService,
1002
- openGitHub,
1003
- closeGitHub,
1004
- openSlack,
1005
- closeSlack,
1006
- openFragmentLibrary,
1007
- closeFragmentLibrary,
1008
- openCommandBar,
1009
- closeCommandBar,
1010
- toggleCommandBar,
1011
- openShortcutsHelp,
1012
- closeShortcutsHelp,
1013
- toggleShortcutsHelp,
1014
- openMobileNav,
1015
- closeMobileNav,
1016
- toggleMobileNav,
1017
- openIntegrations,
1018
- closeIntegrations,
1019
- openFromIntegrations,
1020
- openPersonalSetup,
1021
- closePersonalSetup,
1022
- openFromPersonal,
1023
- openWorkspaceSettings,
1024
- closeWorkspaceSettings,
1025
- setWorkspaceSettingsTab,
1026
- openAccountSettings,
1027
- openContentStorageSettings,
1028
- clearAccountSettingsScrollTarget,
1029
- closeAccountSettings,
1030
- setAccountSettingsTab,
1031
- openObservabilityConnection,
1032
- closeObservabilityConnection,
1033
- openPackageRegistries,
1034
- closePackageRegistries,
1035
- openApiTokens,
1036
- closeApiTokens,
1037
- openProviderConnection,
1038
- closeProviderConnection,
1039
- k3sSetupPrefill,
1040
- consumeK3sSetupDeepLink,
1041
- environmentWizardOpen,
1042
- environmentWizardFrameId,
1043
- openEnvironmentSetup,
1044
- closeEnvironmentSetup,
1045
- openModelConfig,
1046
- closeModelConfig,
1047
- openVendorCredentials,
1048
- setVendorCredentialsTab,
1049
- closeVendorCredentials,
1050
- openLocalModels,
1051
- closeLocalModels,
1052
- openSandbox,
1053
- closeSandbox,
1054
- openUserSecrets,
1055
- closeUserSecrets,
1056
- openOpenRouter,
1057
- closeOpenRouter,
1058
- openAiProviderSetup,
1059
- closeAiProviderSetup,
1060
- openAiPresetMismatch,
1061
- closeAiPresetMismatch,
1062
- dismissAiSetup,
1063
- dismissAiPresetMismatch,
1064
- resetAiOnboarding,
1065
- openRequirementReview,
1066
- openClarityReview,
1067
- openBrainstorm,
1068
- openServiceSpec,
1069
- openInitiativeTracker,
1070
- openInitiativePlanning,
1071
- openFollowUps,
1072
- openForkDecision,
1073
- openPrReview,
1074
- closeRequirementReview,
1075
- openStepDetail,
1076
- closeStepDetail,
1077
- openObservability,
1078
- closeObservability,
1079
- openKaizen,
1080
- closeKaizen,
25
+ ...navigation,
26
+ ...resultViews,
27
+ ...modals,
1081
28
  }
1082
29
  })