@cat-factory/app 0.36.0 → 0.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/app/components/auth/UserMenu.vue +11 -1
  2. package/app/components/brainstorm/BrainstormWindow.vue +2 -1
  3. package/app/components/clarity/ClarityReviewWindow.vue +2 -1
  4. package/app/components/layout/IntegrationBackTitle.vue +12 -7
  5. package/app/components/layout/IntegrationsHub.vue +191 -43
  6. package/app/components/layout/PersonalSetupModal.vue +141 -0
  7. package/app/components/pipeline/PipelineBuilder.vue +1 -1
  8. package/app/components/providers/VendorCredentialsModal.vue +7 -2
  9. package/app/composables/api/accounts.ts +36 -51
  10. package/app/composables/api/auth.ts +20 -19
  11. package/app/composables/api/board.ts +60 -40
  12. package/app/composables/api/bootstrap.ts +25 -22
  13. package/app/composables/api/client.ts +102 -0
  14. package/app/composables/api/context.ts +25 -6
  15. package/app/composables/api/documents.ts +36 -34
  16. package/app/composables/api/execution.ts +65 -48
  17. package/app/composables/api/followUps.ts +26 -26
  18. package/app/composables/api/fragments.ts +47 -34
  19. package/app/composables/api/github.ts +65 -45
  20. package/app/composables/api/humanReview.ts +7 -6
  21. package/app/composables/api/humanTest.ts +15 -11
  22. package/app/composables/api/kaizen.ts +8 -6
  23. package/app/composables/api/localSettings.ts +5 -4
  24. package/app/composables/api/models.ts +58 -51
  25. package/app/composables/api/notifications.ts +13 -7
  26. package/app/composables/api/presets.ts +34 -28
  27. package/app/composables/api/providerConnections.ts +68 -26
  28. package/app/composables/api/recurring.ts +40 -30
  29. package/app/composables/api/releaseHealth.ts +28 -26
  30. package/app/composables/api/reviews.ts +136 -114
  31. package/app/composables/api/sandbox.ts +52 -34
  32. package/app/composables/api/slack.ts +22 -25
  33. package/app/composables/api/spec.ts +3 -3
  34. package/app/composables/api/tasks.ts +42 -41
  35. package/app/composables/api/userSecrets.ts +12 -17
  36. package/app/composables/api/workspaces.ts +21 -15
  37. package/app/composables/useApi.ts +9 -1
  38. package/app/composables/useIntegrationBack.ts +9 -3
  39. package/app/composables/useSourceIntegration.ts +107 -0
  40. package/app/composables/useUpsertList.spec.ts +60 -0
  41. package/app/composables/useUpsertList.ts +57 -0
  42. package/app/pages/index.vue +2 -0
  43. package/app/stores/auth.ts +2 -1
  44. package/app/stores/board.ts +2 -1
  45. package/app/stores/brainstorm.ts +2 -2
  46. package/app/stores/clarity.ts +6 -2
  47. package/app/stores/documents.ts +27 -62
  48. package/app/stores/execution.ts +3 -2
  49. package/app/stores/github.ts +1 -2
  50. package/app/stores/mergePresets.ts +2 -6
  51. package/app/stores/notifications.ts +9 -6
  52. package/app/stores/pipelines.ts +1 -1
  53. package/app/stores/recurringPipelines.ts +2 -7
  54. package/app/stores/sandbox.ts +1 -2
  55. package/app/stores/tasks.ts +25 -76
  56. package/app/stores/ui.ts +62 -19
  57. package/app/types/accountSettings.ts +11 -36
  58. package/app/types/accounts.ts +16 -71
  59. package/app/types/bootstrap.ts +13 -75
  60. package/app/types/brainstorm.ts +13 -38
  61. package/app/types/clarity.ts +12 -43
  62. package/app/types/consensus.ts +16 -89
  63. package/app/types/documents.ts +19 -94
  64. package/app/types/domain.ts +54 -586
  65. package/app/types/execution.ts +48 -515
  66. package/app/types/fragments.ts +15 -83
  67. package/app/types/github.ts +25 -161
  68. package/app/types/incidentEnrichment.ts +10 -25
  69. package/app/types/localModels.ts +11 -61
  70. package/app/types/localSettings.ts +9 -26
  71. package/app/types/merge.ts +10 -68
  72. package/app/types/model-presets.ts +7 -28
  73. package/app/types/models.ts +16 -164
  74. package/app/types/notifications.ts +18 -77
  75. package/app/types/openrouter.ts +8 -34
  76. package/app/types/providerConnections.ts +21 -41
  77. package/app/types/provisioningLogs.ts +9 -29
  78. package/app/types/recurring.ts +10 -63
  79. package/app/types/releaseHealth.ts +15 -39
  80. package/app/types/requirements.ts +14 -84
  81. package/app/types/sandbox.ts +45 -161
  82. package/app/types/services.ts +3 -22
  83. package/app/types/slack.ts +10 -47
  84. package/app/types/spec.ts +15 -68
  85. package/app/types/tasks.ts +15 -111
  86. package/app/types/tracker.ts +9 -24
  87. package/app/types/userSecrets.ts +12 -47
  88. package/package.json +9 -2
@@ -1,10 +1,20 @@
1
1
  <script setup lang="ts">
2
2
  import type { DropdownMenuItem } from '@nuxt/ui'
3
3
 
4
- // Signed-in identity + sign-out, shown in the sidebar when auth is enabled.
4
+ // Signed-in identity + per-user actions, shown in the sidebar when auth is enabled.
5
5
  const auth = useAuthStore()
6
+ const ui = useUiStore()
6
7
 
7
8
  const items = computed<DropdownMenuItem[][]>(() => [
9
+ [
10
+ {
11
+ // The user-scoped "My setup" hub (personal GitHub token, local runners, personal
12
+ // subscriptions) — kept out of the workspace Integrations hub, reachable here.
13
+ label: 'My setup',
14
+ icon: 'i-lucide-user-cog',
15
+ onSelect: () => ui.openPersonalSetup(),
16
+ },
17
+ ],
8
18
  [
9
19
  {
10
20
  label: 'Sign out',
@@ -10,6 +10,7 @@
10
10
  import { parseOutputOutline } from '~/utils/agentOutput'
11
11
  import type {
12
12
  BrainstormItem,
13
+ BrainstormItemStatus,
13
14
  BrainstormSession,
14
15
  BrainstormStage,
15
16
  ReviewItemCategory,
@@ -136,7 +137,7 @@ async function submitReply(item: BrainstormItem) {
136
137
  }
137
138
  }
138
139
 
139
- async function setStatus(item: BrainstormItem, itemStatus: ReviewItemStatus) {
140
+ async function setStatus(item: BrainstormItem, itemStatus: BrainstormItemStatus) {
140
141
  if (!session.value) return
141
142
  try {
142
143
  await brainstorm.setItemStatus(session.value, item.id, itemStatus)
@@ -10,6 +10,7 @@
10
10
  // consumes.
11
11
  import { parseOutputOutline } from '~/utils/agentOutput'
12
12
  import type {
13
+ ClarityItemStatus,
13
14
  ClarityReview,
14
15
  ClarityReviewItem,
15
16
  ReviewItemCategory,
@@ -131,7 +132,7 @@ async function submitReply(item: ClarityReviewItem) {
131
132
  }
132
133
  }
133
134
 
134
- async function setStatus(item: ClarityReviewItem, itemStatus: ReviewItemStatus) {
135
+ async function setStatus(item: ClarityReviewItem, itemStatus: ClarityItemStatus) {
135
136
  if (!review.value) return
136
137
  try {
137
138
  await clarity.setItemStatus(review.value, item.id, itemStatus)
@@ -1,25 +1,30 @@
1
1
  <script setup lang="ts">
2
2
  // Title content for an integration sub-panel's modal header. Renders the panel
3
- // title with a leading "back" control that returns to the Integrations hub, shown
4
- // only when the panel was actually reached from there (`ui.cameFromIntegrations`).
5
- // Panels opened from the command bar, sidebar, a banner or an inspector link don't
6
- // grow a dead Back. Dropped into a UModal's #title slot, so it inherits the modal's
7
- // title styling; it emits `back` and the host panel closes itself + reopens the hub.
3
+ // title with a leading "back" control that returns to the hub the panel was reached
4
+ // from the workspace Integrations hub (`ui.cameFromIntegrations`) or the user-scoped
5
+ // "My setup" hub (`ui.cameFromPersonal`) shown only when there is one. Panels opened
6
+ // from the command bar, sidebar, a banner or an inspector link don't grow a dead Back.
7
+ // Dropped into a UModal's #title slot, so it inherits the modal's title styling; it
8
+ // emits `back` and the host panel closes itself + reopens the right hub.
8
9
  defineProps<{ title?: string }>()
9
10
  const emit = defineEmits<{ back: [] }>()
10
11
  const ui = useUiStore()
12
+ const cameFromHub = computed(() => ui.cameFromIntegrations || ui.cameFromPersonal)
13
+ const backLabel = computed(() =>
14
+ ui.cameFromPersonal ? 'Back to My setup' : 'Back to Integrations',
15
+ )
11
16
  </script>
12
17
 
13
18
  <template>
14
19
  <span class="flex items-center gap-1.5">
15
20
  <UButton
16
- v-if="ui.cameFromIntegrations"
21
+ v-if="cameFromHub"
17
22
  icon="i-lucide-arrow-left"
18
23
  color="neutral"
19
24
  variant="ghost"
20
25
  size="xs"
21
26
  class="-ml-1.5 shrink-0"
22
- aria-label="Back to Integrations"
27
+ :aria-label="backLabel"
23
28
  @click.stop="emit('back')"
24
29
  />
25
30
  <span class="min-w-0 truncate">{{ title }}</span>
@@ -1,12 +1,16 @@
1
1
  <script setup lang="ts">
2
- // The Integrations hub: a single modal that lists every external system the
3
- // workspace can enable or link in replacing the per-integration buttons that
4
- // used to clutter the left navbar. Each row reuses the existing per-integration
5
- // panel handlers on the `ui` store (so the integrations themselves are unchanged);
6
- // opening one closes the hub and reveals that integration's own panel/modal.
2
+ // The Integrations hub: a single modal that lists every external system the WORKSPACE can
3
+ // enable or link in. Each row reuses the existing per-integration panel handlers on the `ui`
4
+ // store (so the integrations themselves are unchanged); opening one closes the hub and
5
+ // reveals that integration's own panel/modal.
7
6
  //
8
- // Sections gate on the same `available` probes the navbar used, so a system that
9
- // the backend has turned off simply doesn't appear here.
7
+ // Sections gate on the same `available` probes the navbar used, so a system that the backend
8
+ // has turned off simply doesn't appear here.
9
+ //
10
+ // Scope split: per-USER connections (a personal GitHub token, own-machine runners, personal
11
+ // subscriptions) now live in the "My setup" hub (UserMenu → My setup), NOT here — keeping
12
+ // this hub purely workspace-scoped. When auth is disabled there is no UserMenu to host them,
13
+ // so a "Personal (only you)" group falls back into this hub so they stay reachable.
10
14
  const ui = useUiStore()
11
15
  const auth = useAuthStore()
12
16
  const github = useGitHubStore()
@@ -20,6 +24,11 @@ const userSecrets = useUserSecretsStore()
20
24
  const apiKeys = useApiKeysStore()
21
25
  const workspace = useWorkspaceStore()
22
26
 
27
+ // True when the per-user "My setup" hub is reachable (UserMenu renders only when signed in).
28
+ // When false (auth disabled / local mode) we fold the personal rows back into this hub so
29
+ // nothing becomes unreachable.
30
+ const personalHubReachable = computed(() => !!auth.user)
31
+
23
32
  // The selected filing tracker, as a badge label ("GitHub Issues" / "Jira").
24
33
  const trackerLabel = computed(() => {
25
34
  if (tracker.settings.tracker === 'github') return 'GitHub Issues'
@@ -27,12 +36,17 @@ const trackerLabel = computed(() => {
27
36
  return undefined
28
37
  })
29
38
 
39
+ // Free-text filter over the rows (label + description), so a workspace with many enabled
40
+ // systems stays scannable. Reset when the hub re-opens.
41
+ const query = ref('')
42
+
30
43
  // The observability connection status drives the hub's connected badge. Load it
31
44
  // lazily when the hub opens (the secret-less connection view is cheap).
32
45
  watch(
33
46
  () => ui.integrationsOpen,
34
47
  (isOpen) => {
35
48
  if (isOpen) {
49
+ query.value = ''
36
50
  void releaseHealth.ensureLoaded().catch(() => {})
37
51
  void providerConnections.ensureLoaded().catch(() => {})
38
52
  void userSecrets.load().catch(() => {})
@@ -47,8 +61,10 @@ const open = computed({
47
61
  set: (v: boolean) => (v ? ui.openIntegrations() : ui.closeIntegrations()),
48
62
  })
49
63
 
50
- // One integration row. `status` is the connected-state line shown under the label
51
- // (an account/team name, "Connected", or a hint); `connected` drives the badge.
64
+ // One integration row. `connected` drives the green badge (`status` is its line — an
65
+ // account/team name or "Connected"); `attention` drives an amber badge (e.g. a source that
66
+ // is available but turned off) with `attentionLabel`. `recommended` tags an essential row
67
+ // with a "Recommended" chip while the workspace has nothing connected yet.
52
68
  interface IntegrationItem {
53
69
  key: string
54
70
  icon: string
@@ -56,12 +72,27 @@ interface IntegrationItem {
56
72
  description: string
57
73
  status?: string
58
74
  connected?: boolean
75
+ attention?: boolean
76
+ attentionLabel?: string
77
+ recommended?: boolean
78
+ onClick: () => void
79
+ }
80
+
81
+ // A group may carry a small de-emphasised footer LINK (workspace config that isn't itself an
82
+ // integration, e.g. the issue-tracker settings) rendered under its rows rather than as a
83
+ // full row competing with the connections.
84
+ interface IntegrationFooterLink {
85
+ key: string
86
+ icon: string
87
+ label: string
88
+ status?: string
59
89
  onClick: () => void
60
90
  }
61
91
 
62
92
  interface IntegrationGroup {
63
93
  title: string
64
94
  items: IntegrationItem[]
95
+ footerLink?: IntegrationFooterLink
65
96
  }
66
97
 
67
98
  // Run an integration's open handler, then dismiss the hub so its panel takes over.
@@ -87,22 +118,16 @@ const groups = computed<IntegrationGroup[]>(() => {
87
118
  description: 'One gateway to 300+ models — add your key and enable models in one place.',
88
119
  status: openRouterKeyConnected ? 'Key connected' : undefined,
89
120
  connected: openRouterKeyConnected,
121
+ recommended: true,
90
122
  onClick: () => go(ui.openOpenRouter),
91
123
  },
92
124
  {
93
125
  key: 'vendors',
94
126
  icon: 'i-lucide-key-round',
95
127
  label: 'Vendors & keys',
96
- description: 'LLM vendor subscriptions and provider API keys.',
128
+ description: 'Workspace LLM subscriptions and provider API keys.',
97
129
  onClick: () => go(ui.openVendorCredentials),
98
130
  },
99
- {
100
- key: 'local-runners',
101
- icon: 'i-lucide-server',
102
- label: 'My local runners',
103
- description: 'Your own-machine model runners (Ollama, LM Studio, vLLM…).',
104
- onClick: () => go(ui.openLocalModels),
105
- },
106
131
  ],
107
132
  })
108
133
 
@@ -116,23 +141,10 @@ const groups = computed<IntegrationGroup[]>(() => {
116
141
  description: 'Connect the workspace’s GitHub App, browse repos, PRs and issues.',
117
142
  status: github.connected ? github.connection?.accountLogin : undefined,
118
143
  connected: github.connected,
144
+ recommended: true,
119
145
  onClick: () => go(ui.openGitHub),
120
146
  })
121
147
  }
122
- // Per-user GitHub PAT — works on every runtime (used for runs you initiate). Always
123
- // offered; the badge reflects whether the signed-in user has stored one.
124
- {
125
- const pat = userSecrets.statusFor('github_pat')
126
- code.push({
127
- key: 'github-pat',
128
- icon: 'i-lucide-key-round',
129
- label: 'My GitHub token',
130
- description: 'A personal access token used for runs you start (pushes, PRs, CI, merge).',
131
- status: pat ? 'Connected' : undefined,
132
- connected: !!pat,
133
- onClick: () => go(ui.openUserSecrets),
134
- })
135
- }
136
148
  if (code.length) out.push({ title: 'Source control', items: code })
137
149
 
138
150
  // --- Communication ---------------------------------------------------------
@@ -180,10 +192,11 @@ const groups = computed<IntegrationGroup[]>(() => {
180
192
  icon: src.icon,
181
193
  label: src.label,
182
194
  description: `Link ${src.label} to import and reference tracker issues.`,
183
- // Available + enabled ⇒ offered (green); available + off ⇒ "Disabled";
195
+ // Available + enabled ⇒ offered (green); available + off ⇒ "Disabled" (amber);
184
196
  // not available ⇒ no badge (Jira needs connecting; GitHub needs its App).
185
- status: src.available ? (src.enabled ? undefined : 'Disabled') : undefined,
186
197
  connected: src.available && src.enabled,
198
+ attention: src.available && !src.enabled,
199
+ attentionLabel: 'Disabled',
187
200
  onClick: () => go(() => ui.openTaskConnect(src.source)),
188
201
  }))
189
202
  if (tasks.anyOffered) {
@@ -195,16 +208,19 @@ const groups = computed<IntegrationGroup[]>(() => {
195
208
  onClick: () => go(() => ui.openTaskImport(null)),
196
209
  })
197
210
  }
198
- trackers.push({
199
- key: 'task:tracker',
200
- icon: 'i-lucide-list-checks',
201
- label: 'Issue tracker settings',
202
- description: 'Choose the filing tracker, enable linking sources, and configure writeback.',
203
- status: trackerLabel.value,
204
- connected: tracker.settings.tracker !== null,
205
- onClick: () => go(() => ui.openWorkspaceSettings('tracker')),
211
+ // Choosing the filing tracker / writeback is workspace CONFIG, not an integration, so it
212
+ // sits as a quiet footer link under the sources rather than a competing row.
213
+ out.push({
214
+ title: 'Task trackers',
215
+ items: trackers,
216
+ footerLink: {
217
+ key: 'task:tracker',
218
+ icon: 'i-lucide-list-checks',
219
+ label: 'Issue tracker settings',
220
+ status: trackerLabel.value,
221
+ onClick: () => go(() => ui.openWorkspaceSettings('tracker')),
222
+ },
206
223
  })
207
- out.push({ title: 'Task trackers', items: trackers })
208
224
  }
209
225
 
210
226
  // --- Observability ---------------------------------------------------------
@@ -271,8 +287,73 @@ const groups = computed<IntegrationGroup[]>(() => {
271
287
  }
272
288
  if (infra.length) out.push({ title: 'Infrastructure', items: infra })
273
289
 
290
+ // --- Personal (only you) — fallback when there is no UserMenu to host "My setup" -------
291
+ // Per-user connections normally live in the My-setup hub; with auth disabled they fold in
292
+ // here so they stay reachable. (The badge reflects the signed-in user's stored secret.)
293
+ if (!personalHubReachable.value) {
294
+ const pat = !!userSecrets.statusFor('github_pat')
295
+ out.push({
296
+ title: 'Personal (only you)',
297
+ items: [
298
+ {
299
+ key: 'github-pat',
300
+ icon: 'i-lucide-key-round',
301
+ label: 'My GitHub token',
302
+ description: 'A personal access token used for runs you start (pushes, PRs, CI, merge).',
303
+ status: pat ? 'Connected' : undefined,
304
+ connected: pat,
305
+ onClick: () => go(ui.openUserSecrets),
306
+ },
307
+ {
308
+ key: 'local-runners',
309
+ icon: 'i-lucide-server',
310
+ label: 'My local runners',
311
+ description: 'Your own-machine model runners (Ollama, LM Studio, vLLM…).',
312
+ onClick: () => go(ui.openLocalModels),
313
+ },
314
+ ],
315
+ })
316
+ }
317
+
274
318
  return out
275
319
  })
320
+
321
+ // Sort connected rows first, then amber "attention", then idle — a stable rank so each
322
+ // group reads "what's live" top-down without reshuffling unrelated rows.
323
+ function stateRank(item: IntegrationItem): number {
324
+ if (item.connected) return 0
325
+ if (item.attention) return 1
326
+ return 2
327
+ }
328
+
329
+ const allItems = computed(() => groups.value.flatMap((g) => g.items))
330
+ const anyConnected = computed(() => allItems.value.some((i) => i.connected))
331
+ // Essential rows still unconnected — surfaced as the empty-workspace get-started shortcuts.
332
+ const recommendedActions = computed(() =>
333
+ allItems.value.filter((i) => i.recommended && !i.connected),
334
+ )
335
+
336
+ function matches(text: string, q: string): boolean {
337
+ return text.toLowerCase().includes(q)
338
+ }
339
+
340
+ // Groups after the search filter + connected-first sort. A footer link is kept only when it
341
+ // also matches the query (or the query is empty); a group with no surviving rows/link drops.
342
+ const filteredGroups = computed<IntegrationGroup[]>(() => {
343
+ const q = query.value.trim().toLowerCase()
344
+ return groups.value
345
+ .map((g) => {
346
+ const items = (
347
+ q ? g.items.filter((i) => matches(i.label, q) || matches(i.description, q)) : g.items
348
+ )
349
+ .slice()
350
+ .sort((a, b) => stateRank(a) - stateRank(b))
351
+ const footerLink =
352
+ g.footerLink && (!q || matches(g.footerLink.label, q)) ? g.footerLink : undefined
353
+ return { ...g, items, footerLink }
354
+ })
355
+ .filter((g) => g.items.length || g.footerLink)
356
+ })
276
357
  </script>
277
358
 
278
359
  <template>
@@ -283,7 +364,47 @@ const groups = computed<IntegrationGroup[]>(() => {
283
364
  Connect and manage the external systems this workspace can link in.
284
365
  </p>
285
366
 
286
- <section v-for="group in groups" :key="group.title">
367
+ <!-- Get-started cue: an empty workspace gets the two essentials up front so the first
368
+ run isn't blocked on hunting for them. Hidden once anything is connected. -->
369
+ <div
370
+ v-if="!anyConnected && recommendedActions.length"
371
+ class="rounded-lg border border-primary-500/40 bg-primary-500/10 p-3"
372
+ >
373
+ <div class="mb-2 flex items-center gap-2 text-sm font-medium text-primary-200">
374
+ <UIcon name="i-lucide-rocket" class="h-4 w-4 shrink-0" />
375
+ <span>Get started</span>
376
+ </div>
377
+ <p class="mb-3 text-xs text-slate-300">
378
+ Connect a code source and a model provider to run your first pipeline.
379
+ </p>
380
+ <div class="flex flex-wrap gap-2">
381
+ <UButton
382
+ v-for="item in recommendedActions"
383
+ :key="`rec:${item.key}`"
384
+ size="xs"
385
+ color="primary"
386
+ variant="soft"
387
+ :icon="item.icon"
388
+ @click="item.onClick()"
389
+ >
390
+ {{ item.label }}
391
+ </UButton>
392
+ </div>
393
+ </div>
394
+
395
+ <UInput
396
+ v-model="query"
397
+ icon="i-lucide-search"
398
+ size="sm"
399
+ placeholder="Search integrations…"
400
+ class="w-full"
401
+ />
402
+
403
+ <p v-if="!filteredGroups.length" class="px-1 py-6 text-center text-sm text-slate-500">
404
+ No integrations match “{{ query }}”.
405
+ </p>
406
+
407
+ <section v-for="group in filteredGroups" :key="group.title">
287
408
  <h3 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
288
409
  {{ group.title }}
289
410
  </h3>
@@ -302,12 +423,39 @@ const groups = computed<IntegrationGroup[]>(() => {
302
423
  <UBadge v-if="item.connected" color="success" variant="subtle" size="sm">
303
424
  {{ item.status || 'Connected' }}
304
425
  </UBadge>
426
+ <UBadge v-else-if="item.attention" color="warning" variant="subtle" size="sm">
427
+ {{ item.attentionLabel || 'Needs attention' }}
428
+ </UBadge>
429
+ <span v-else class="text-[11px] text-slate-500">Not connected</span>
430
+ <UBadge
431
+ v-if="!anyConnected && item.recommended && !item.connected"
432
+ color="primary"
433
+ variant="subtle"
434
+ size="sm"
435
+ >
436
+ Recommended
437
+ </UBadge>
305
438
  </div>
306
439
  <p class="truncate text-xs text-slate-400">{{ item.description }}</p>
307
440
  </div>
308
441
  <UIcon name="i-lucide-chevron-right" class="h-4 w-4 shrink-0 text-slate-500" />
309
442
  </button>
310
443
  </div>
444
+
445
+ <!-- De-emphasised workspace-config link (e.g. issue tracker settings). -->
446
+ <button
447
+ v-if="group.footerLink"
448
+ type="button"
449
+ class="mt-1.5 flex w-full items-center gap-2 rounded-md px-3 py-1.5 text-left text-xs text-slate-400 transition hover:bg-slate-900/60 hover:text-slate-200"
450
+ @click="group.footerLink.onClick()"
451
+ >
452
+ <UIcon :name="group.footerLink.icon" class="h-3.5 w-3.5 shrink-0" />
453
+ <span class="flex-1 truncate">{{ group.footerLink.label }}</span>
454
+ <span v-if="group.footerLink.status" class="shrink-0 text-slate-500">{{
455
+ group.footerLink.status
456
+ }}</span>
457
+ <UIcon name="i-lucide-chevron-right" class="h-3.5 w-3.5 shrink-0 text-slate-600" />
458
+ </button>
311
459
  </section>
312
460
  </div>
313
461
  </template>
@@ -0,0 +1,141 @@
1
+ <script setup lang="ts">
2
+ // "My setup": the user-scoped sibling of the Integrations hub. It lists the connections
3
+ // that belong to the SIGNED-IN USER rather than the workspace — a personal GitHub token,
4
+ // own-machine local model runners, and individual-usage (personal) subscriptions — which
5
+ // previously sat, confusingly, among the workspace-wide integrations. Each row reuses the
6
+ // existing per-panel handlers via `ui.openFromPersonal(...)`, so opening one closes this
7
+ // hub, reveals that panel, and gives it a "Back to My setup" control (IntegrationBackTitle).
8
+ const ui = useUiStore()
9
+ const userSecrets = useUserSecretsStore()
10
+ const localModels = useLocalModelsStore()
11
+ const personalSubs = usePersonalSubscriptionsStore()
12
+
13
+ // Load the cheap user-scoped status whenever the hub opens, so each row's badge is accurate.
14
+ watch(
15
+ () => ui.personalSetupOpen,
16
+ (isOpen) => {
17
+ if (!isOpen) return
18
+ void userSecrets.load().catch(() => {})
19
+ void localModels.load().catch(() => {})
20
+ void personalSubs.load().catch(() => {})
21
+ },
22
+ )
23
+
24
+ const open = computed({
25
+ get: () => ui.personalSetupOpen,
26
+ set: (v: boolean) => (v ? ui.openPersonalSetup() : ui.closePersonalSetup()),
27
+ })
28
+
29
+ // One row. `connected` drives the badge; `status` is the connected-state line (a count or
30
+ // "Connected"). Mirrors the Integrations hub's row shape so the two hubs look identical.
31
+ interface PersonalItem {
32
+ key: string
33
+ icon: string
34
+ label: string
35
+ description: string
36
+ status?: string
37
+ connected: boolean
38
+ onClick: () => void
39
+ }
40
+
41
+ interface PersonalGroup {
42
+ title: string
43
+ items: PersonalItem[]
44
+ }
45
+
46
+ // Open a user-scoped panel from this hub (sets the "came from My setup" marker).
47
+ function go(fn: () => void) {
48
+ ui.openFromPersonal(fn)
49
+ }
50
+
51
+ const groups = computed<PersonalGroup[]>(() => {
52
+ const out: PersonalGroup[] = []
53
+
54
+ // --- Source control --------------------------------------------------------
55
+ const pat = !!userSecrets.statusFor('github_pat')
56
+ out.push({
57
+ title: 'Source control',
58
+ items: [
59
+ {
60
+ key: 'github-pat',
61
+ icon: 'i-lucide-key-round',
62
+ label: 'My GitHub token',
63
+ description: 'A personal access token used for runs you start (pushes, PRs, CI, merge).',
64
+ status: pat ? 'Connected' : undefined,
65
+ connected: pat,
66
+ onClick: () => go(ui.openUserSecrets),
67
+ },
68
+ ],
69
+ })
70
+
71
+ // --- Models ----------------------------------------------------------------
72
+ const runnerCount = localModels.endpoints.length
73
+ const subCount = personalSubs.subscriptions.length
74
+ out.push({
75
+ title: 'Models',
76
+ items: [
77
+ {
78
+ key: 'local-runners',
79
+ icon: 'i-lucide-server',
80
+ label: 'My local runners',
81
+ description: 'Your own-machine model runners (Ollama, LM Studio, vLLM…).',
82
+ status: runnerCount ? `${runnerCount} connected` : undefined,
83
+ connected: runnerCount > 0,
84
+ onClick: () => go(ui.openLocalModels),
85
+ },
86
+ {
87
+ key: 'personal-subs',
88
+ icon: 'i-lucide-user',
89
+ label: 'My subscriptions',
90
+ description:
91
+ 'Individual-usage plans you unlock with a personal password (Claude, GLM, Codex).',
92
+ status: subCount ? `${subCount} connected` : undefined,
93
+ connected: subCount > 0,
94
+ onClick: () => go(() => ui.openVendorCredentials('personal')),
95
+ },
96
+ ],
97
+ })
98
+
99
+ return out
100
+ })
101
+ </script>
102
+
103
+ <template>
104
+ <UModal v-model:open="open" title="My setup" :ui="{ content: 'max-w-xl' }">
105
+ <template #body>
106
+ <div class="space-y-5">
107
+ <p class="text-xs text-slate-400">
108
+ Your personal connections — used for runs you start, and visible only to you.
109
+ </p>
110
+
111
+ <section v-for="group in groups" :key="group.title">
112
+ <h3 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
113
+ {{ group.title }}
114
+ </h3>
115
+ <div class="space-y-1.5">
116
+ <button
117
+ v-for="item in group.items"
118
+ :key="item.key"
119
+ type="button"
120
+ class="flex w-full items-center gap-3 rounded-lg border border-slate-800 bg-slate-900/50 px-3 py-2.5 text-left transition hover:border-slate-700 hover:bg-slate-900"
121
+ @click="item.onClick()"
122
+ >
123
+ <UIcon :name="item.icon" class="h-5 w-5 shrink-0 text-slate-300" />
124
+ <div class="min-w-0 flex-1">
125
+ <div class="flex items-center gap-2">
126
+ <span class="truncate text-sm font-medium text-slate-100">{{ item.label }}</span>
127
+ <UBadge v-if="item.connected" color="success" variant="subtle" size="sm">
128
+ {{ item.status || 'Connected' }}
129
+ </UBadge>
130
+ <span v-else class="text-[11px] text-slate-500">Not connected</span>
131
+ </div>
132
+ <p class="truncate text-xs text-slate-400">{{ item.description }}</p>
133
+ </div>
134
+ <UIcon name="i-lucide-chevron-right" class="h-4 w-4 shrink-0 text-slate-500" />
135
+ </button>
136
+ </div>
137
+ </section>
138
+ </div>
139
+ </template>
140
+ </UModal>
141
+ </template>
@@ -31,7 +31,7 @@ function toggleGating(i: number) {
31
31
  if (!cfg) return
32
32
  cfg.gating = cfg.gating?.enabled
33
33
  ? { ...cfg.gating, enabled: false }
34
- : { enabled: true, minRisk: 0.6, minImpact: 0.6 }
34
+ : { enabled: true, minRisk: 0.6, minImpact: 0.6, onMissingEstimate: 'consensus' }
35
35
  }
36
36
  const agents = useAgentsStore()
37
37
  const ui = useUiStore()
@@ -22,7 +22,9 @@ const back = useIntegrationBack(open)
22
22
 
23
23
  // Horizontal tabs replace the old long vertical scroll: each credential kind is its own
24
24
  // section (pooled subscriptions, direct vendor keys, proxy/gateway keys, personal subs).
25
- const activeTab = ref('pool')
25
+ // Initialised from the ui store so a caller can deep-link to a tab — the user-scoped
26
+ // "My subscriptions" entry opens straight onto the `personal` tab.
27
+ const activeTab = ref(ui.vendorCredentialsTab)
26
28
  const tabs = [
27
29
  { value: 'pool', label: 'Workspace pool', icon: 'i-lucide-users', slot: 'pool' },
28
30
  { value: 'direct', label: 'Direct providers', icon: 'i-lucide-key-round', slot: 'direct' },
@@ -51,7 +53,10 @@ const token = ref('')
51
53
  const busy = ref(false)
52
54
 
53
55
  watch(open, (isOpen) => {
54
- if (isOpen && workspace.workspaceId) void creds.load(workspace.workspaceId)
56
+ if (!isOpen) return
57
+ // Honour a deep-linked tab each time the modal opens (e.g. "My subscriptions" → personal).
58
+ activeTab.value = ui.vendorCredentialsTab
59
+ if (workspace.workspaceId) void creds.load(workspace.workspaceId)
55
60
  })
56
61
 
57
62
  /** Step-by-step instructions for the selected vendor. */