@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,5 +1,5 @@
1
1
  import { defineStore } from 'pinia'
2
- import { computed, ref } from 'vue'
2
+ import { ref } from 'vue'
3
3
  import type {
4
4
  DocumentBoardPlan,
5
5
  DocumentConnection,
@@ -8,6 +8,8 @@ import type {
8
8
  DocumentSourceKind,
9
9
  SourceDocument,
10
10
  } from '~/types/domain'
11
+ import { useSourceIntegration } from '~/composables/useSourceIntegration'
12
+ import { useUpsertList } from '~/composables/useUpsertList'
11
13
  import { useWorkspaceStore } from '~/stores/workspace'
12
14
 
13
15
  /**
@@ -24,83 +26,46 @@ export const useDocumentsStore = defineStore('documents', () => {
24
26
  const api = useApi()
25
27
  const workspace = useWorkspaceStore()
26
28
 
27
- /** null = unknown (not probed yet), true/false = integration on/off. */
28
- const available = ref<boolean | null>(null)
29
- /** The configured sources and their connect/import descriptors. */
30
- const sources = ref<DocumentSourceDescriptor[]>([])
31
- /** Live connections, one per connected source. */
32
- const connections = ref<DocumentConnection[]>([])
33
- const documents = ref<SourceDocument[]>([])
29
+ // Shared opt-in / probe / connections lifecycle (see `useSourceIntegration`).
30
+ const integration = useSourceIntegration<
31
+ DocumentSourceKind,
32
+ DocumentConnection,
33
+ DocumentSourceDescriptor
34
+ >({
35
+ enabled: () => !!workspace.workspaceId,
36
+ fetch: async () => {
37
+ const [{ sources }, { connections }] = await Promise.all([
38
+ api.listDocumentSources(workspace.requireId()),
39
+ api.listDocumentConnections(workspace.requireId()),
40
+ ])
41
+ return { sources, connections }
42
+ },
43
+ })
44
+ const { available, sources, connections, connectedSources, anyConnected } = integration
45
+ const { descriptorFor, connectionFor, isConnected, probe } = integration
46
+
47
+ const { items: documents, upsert: upsertDoc } = useUpsertList<SourceDocument>({
48
+ key: (d) => `${d.source}:${d.externalId}`,
49
+ prepend: true,
50
+ })
34
51
  const loading = ref(false)
35
52
 
36
- /** Sources the workspace currently has a live connection to. */
37
- const connectedSources = computed(() =>
38
- sources.value.filter((s) => connections.value.some((c) => c.source === s.source)),
39
- )
40
- const anyConnected = computed(() => connections.value.length > 0)
41
-
42
- function descriptorFor(source: DocumentSourceKind): DocumentSourceDescriptor | undefined {
43
- return sources.value.find((s) => s.source === source)
44
- }
45
-
46
- function connectionFor(source: DocumentSourceKind): DocumentConnection | undefined {
47
- return connections.value.find((c) => c.source === source)
48
- }
49
-
50
- function isConnected(source: DocumentSourceKind): boolean {
51
- return connectionFor(source) !== undefined
52
- }
53
-
54
53
  /** Imported documents currently attached to a given block. */
55
54
  function docsForBlock(blockId: string): SourceDocument[] {
56
55
  return documents.value.filter((d) => d.linkedBlockId === blockId)
57
56
  }
58
57
 
59
- /** Merge a document returned by the backend into the local cache. */
60
- function upsertDoc(doc: SourceDocument) {
61
- const i = documents.value.findIndex(
62
- (d) => d.source === doc.source && d.externalId === doc.externalId,
63
- )
64
- if (i >= 0) documents.value[i] = doc
65
- else documents.value.unshift(doc)
66
- }
67
-
68
- function upsertConnection(conn: DocumentConnection) {
69
- const i = connections.value.findIndex((c) => c.source === conn.source)
70
- if (i >= 0) connections.value[i] = conn
71
- else connections.value.push(conn)
72
- }
73
-
74
- /** Probe the integration: resolves `available`, the sources and connections. */
75
- async function probe() {
76
- if (!workspace.workspaceId) return
77
- try {
78
- const [{ sources: srcs }, { connections: conns }] = await Promise.all([
79
- api.listDocumentSources(workspace.requireId()),
80
- api.listDocumentConnections(workspace.requireId()),
81
- ])
82
- available.value = true
83
- sources.value = srcs
84
- connections.value = conns
85
- } catch {
86
- // 503 (integration disabled) or any error → hide the UI entry points.
87
- available.value = false
88
- sources.value = []
89
- connections.value = []
90
- }
91
- }
92
-
93
58
  /** Connect the workspace to a source with its credential bag. */
94
59
  async function connect(source: DocumentSourceKind, credentials: Record<string, string>) {
95
60
  const conn = await api.connectDocumentSource(workspace.requireId(), source, credentials)
96
- upsertConnection(conn)
61
+ integration.upsertConnection(conn)
97
62
  available.value = true
98
63
  }
99
64
 
100
65
  /** Disconnect the workspace from a source. */
101
66
  async function disconnect(source: DocumentSourceKind) {
102
67
  await api.disconnectDocumentSource(workspace.requireId(), source)
103
- connections.value = connections.value.filter((c) => c.source !== source)
68
+ integration.removeConnection(source)
104
69
  }
105
70
 
106
71
  /** Load the imported documents for the workspace (across sources). */
@@ -7,7 +7,8 @@ import type {
7
7
  PipelineStep,
8
8
  StepApproval,
9
9
  } from '~/types/domain'
10
- import type { IterationCapChoice, ReviewComment } from '~/types/execution'
10
+ import type { RequestStepChangesInput } from '@cat-factory/contracts'
11
+ import type { IterationCapChoice } from '~/types/execution'
11
12
  import { useWorkspaceStore } from '~/stores/workspace'
12
13
 
13
14
  /**
@@ -163,7 +164,7 @@ export const useExecutionStore = defineStore('execution', () => {
163
164
  async function requestStepChanges(
164
165
  instanceId: string,
165
166
  approvalId: string,
166
- review: { feedback?: string; comments?: ReviewComment[] },
167
+ review: RequestStepChangesInput,
167
168
  ) {
168
169
  const ws = useWorkspaceStore()
169
170
  const personal = usePersonalSubscriptionsStore()
@@ -2,7 +2,6 @@ import { defineStore } from 'pinia'
2
2
  import { computed, ref } from 'vue'
3
3
  import type {
4
4
  CreateBranchInput,
5
- CreateRepoRequest,
6
5
  GitHubAvailableRepo,
7
6
  GitHubBranch,
8
7
  GitHubConnection,
@@ -224,7 +223,7 @@ export const useGitHubStore = defineStore('github', () => {
224
223
  * meaningful when `canCreateRepos`; the backend 409s otherwise. Returns the
225
224
  * created repo so the caller can confirm/link it.
226
225
  */
227
- function createRepo(input: CreateRepoRequest) {
226
+ function createRepo(input: Parameters<typeof api.createGitHubRepo>[1]) {
228
227
  return api.createGitHubRepo(workspace.requireId(), input)
229
228
  }
230
229
 
@@ -1,10 +1,6 @@
1
1
  import { defineStore } from 'pinia'
2
2
  import { computed, ref } from 'vue'
3
- import type {
4
- CreateMergePresetInput,
5
- MergeThresholdPreset,
6
- UpdateMergePresetInput,
7
- } from '~/types/domain'
3
+ import type { MergeThresholdPreset, UpdateMergePresetInput } from '~/types/domain'
8
4
  import { useWorkspaceStore } from '~/stores/workspace'
9
5
 
10
6
  /**
@@ -34,7 +30,7 @@ export const useMergePresetsStore = defineStore('mergePresets', () => {
34
30
  return defaultPreset.value
35
31
  }
36
32
 
37
- async function create(input: CreateMergePresetInput) {
33
+ async function create(input: Parameters<typeof api.createMergePreset>[1]) {
38
34
  const ws = useWorkspaceStore()
39
35
  const created = await api.createMergePreset(ws.requireId(), input)
40
36
  await ws.refresh()
@@ -1,6 +1,7 @@
1
1
  import { defineStore } from 'pinia'
2
- import { computed, ref } from 'vue'
2
+ import { computed } from 'vue'
3
3
  import type { Notification } from '~/types/domain'
4
+ import { useUpsertList } from '~/composables/useUpsertList'
4
5
  import { useWorkspaceStore } from '~/stores/workspace'
5
6
 
6
7
  /**
@@ -14,7 +15,11 @@ export const useNotificationsStore = defineStore('notifications', () => {
14
15
  const api = useApi()
15
16
 
16
17
  /** All open notifications, newest-first. */
17
- const open = ref<Notification[]>([])
18
+ const {
19
+ items: open,
20
+ upsert: upsertOpen,
21
+ remove,
22
+ } = useUpsertList<Notification>({ key: (n) => n.id, prepend: true })
18
23
 
19
24
  /** Replace the cache from a server snapshot. */
20
25
  function hydrate(notifications: Notification[]) {
@@ -28,13 +33,11 @@ export const useNotificationsStore = defineStore('notifications', () => {
28
33
  * replaced in place; a resolved one (acted/dismissed) is removed from the inbox.
29
34
  */
30
35
  function upsert(notification: Notification) {
31
- const i = open.value.findIndex((n) => n.id === notification.id)
32
36
  if (notification.status !== 'open') {
33
- if (i >= 0) open.value.splice(i, 1)
37
+ remove(notification.id)
34
38
  return
35
39
  }
36
- if (i >= 0) open.value[i] = notification
37
- else open.value.unshift(notification)
40
+ upsertOpen(notification)
38
41
  }
39
42
 
40
43
  /** Open notifications for a given block (for the board card badge). */
@@ -132,7 +132,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
132
132
  function toggleDraftGating(index: number) {
133
133
  draftGating.value[index] = draftGating.value[index]?.enabled
134
134
  ? null
135
- : { enabled: true, minRisk: 0.5, minImpact: 0.5 }
135
+ : { enabled: true, minRisk: 0.5, minImpact: 0.5, onMissingEstimate: 'run' }
136
136
  }
137
137
 
138
138
  /**
@@ -1,11 +1,6 @@
1
1
  import { defineStore } from 'pinia'
2
2
  import { computed, ref } from 'vue'
3
- import type {
4
- CreateScheduleInput,
5
- PipelineSchedule,
6
- ScheduleRun,
7
- UpdateScheduleInput,
8
- } from '~/types/recurring'
3
+ import type { PipelineSchedule, ScheduleRun, UpdateScheduleInput } from '~/types/recurring'
9
4
  import { useWorkspaceStore } from '~/stores/workspace'
10
5
  import { useBoardStore } from '~/stores/board'
11
6
 
@@ -39,7 +34,7 @@ export const useRecurringPipelinesStore = defineStore('recurringPipelines', () =
39
34
  return schedules.value.find((s) => s.blockId === blockId)
40
35
  }
41
36
 
42
- async function create(input: CreateScheduleInput) {
37
+ async function create(input: Parameters<typeof api.createRecurringPipeline>[1]) {
43
38
  const ws = useWorkspaceStore()
44
39
  const created = await api.createRecurringPipeline(ws.requireId(), input)
45
40
  await ws.refresh()
@@ -2,7 +2,6 @@ import { defineStore } from 'pinia'
2
2
  import { computed, ref } from 'vue'
3
3
  import type { ModelOption } from '~/types/domain'
4
4
  import type {
5
- CreateSandboxExperimentInput,
6
5
  SandboxAgentKindMeta,
7
6
  SandboxExperiment,
8
7
  SandboxExperimentDetail,
@@ -118,7 +117,7 @@ export const useSandboxStore = defineStore('sandbox', () => {
118
117
  await load()
119
118
  }
120
119
 
121
- async function createExperiment(input: CreateSandboxExperimentInput) {
120
+ async function createExperiment(input: Parameters<typeof api.createSandboxExperiment>[1]) {
122
121
  const ws = useWorkspaceStore()
123
122
  const created = await api.createSandboxExperiment(ws.requireId(), input)
124
123
  await load()
@@ -8,6 +8,8 @@ import type {
8
8
  TaskSourceKind,
9
9
  TaskSourceState,
10
10
  } from '~/types/domain'
11
+ import { useSourceIntegration } from '~/composables/useSourceIntegration'
12
+ import { useUpsertList } from '~/composables/useUpsertList'
11
13
  import { useWorkspaceStore } from '~/stores/workspace'
12
14
  import { useBoardStore } from '~/stores/board'
13
15
 
@@ -27,95 +29,42 @@ export const useTasksStore = defineStore('tasks', () => {
27
29
  const api = useApi()
28
30
  const workspace = useWorkspaceStore()
29
31
 
30
- /** null = unknown (not probed yet), true/false = integration on/off. */
31
- const available = ref<boolean | null>(null)
32
- /**
33
- * Why the last probe failed, when it did — captured (rather than swallowed) so
34
- * the settings panel can explain *why* nothing is surfaced (integration disabled
35
- * vs a server/backend error) instead of a blanket "install integration first".
36
- */
37
- const probeError = ref<{ status: number | null; message: string } | null>(null)
38
- /** The configured sources, each with its descriptor + per-workspace state (available + enabled). */
39
- const sources = ref<TaskSourceState[]>([])
40
- /** Live connections, one per connected (credentialed) source. */
41
- const connections = ref<TaskConnection[]>([])
42
- const tasks = ref<SourceTask[]>([])
32
+ // Shared opt-in / probe / connections lifecycle (see `useSourceIntegration`). Its
33
+ // `probeError` is what lets the settings panel explain *why* nothing is surfaced
34
+ // (integration disabled vs a server/backend error) instead of "install it first".
35
+ const integration = useSourceIntegration<TaskSourceKind, TaskConnection, TaskSourceState>({
36
+ enabled: () => !!workspace.workspaceId,
37
+ fetch: async () => {
38
+ const [{ sources }, { connections }] = await Promise.all([
39
+ api.listTaskSources(workspace.requireId()),
40
+ api.listTaskConnections(workspace.requireId()),
41
+ ])
42
+ return { sources, connections }
43
+ },
44
+ })
45
+ const { available, probeError, sources, connections, connectedSources, anyConnected } =
46
+ integration
47
+ const { descriptorFor, connectionFor, isConnected, probe } = integration
48
+
49
+ const { items: tasks, upsert: upsertTask } = useUpsertList<SourceTask>({
50
+ key: (t) => `${t.source}:${t.externalId}`,
51
+ prepend: true,
52
+ })
43
53
  /** The last live setup-check verdict per source (from `checkSetup`). */
44
54
  const diagnostics = ref<Partial<Record<TaskSourceKind, TaskSourceDiagnostic>>>({})
45
55
  /** The source currently running a setup check, if any. */
46
56
  const checking = ref<TaskSourceKind | null>(null)
47
57
  const loading = ref(false)
48
58
 
49
- /** Sources the workspace currently has a live connection to. */
50
- const connectedSources = computed(() =>
51
- sources.value.filter((s) => connections.value.some((c) => c.source === s.source)),
52
- )
53
- const anyConnected = computed(() => connections.value.length > 0)
54
-
55
59
  /** Sources offered for import: available (connected / App installed) AND enabled. */
56
60
  const offeredSources = computed(() => sources.value.filter((s) => s.available && s.enabled))
57
61
  const anyOffered = computed(() => offeredSources.value.length > 0)
58
62
 
59
- function descriptorFor(source: TaskSourceKind): TaskSourceState | undefined {
60
- return sources.value.find((s) => s.source === source)
61
- }
62
-
63
- function connectionFor(source: TaskSourceKind): TaskConnection | undefined {
64
- return connections.value.find((c) => c.source === source)
65
- }
66
-
67
- function isConnected(source: TaskSourceKind): boolean {
68
- return connectionFor(source) !== undefined
69
- }
70
-
71
63
  /** Imported issues currently attached to a given block. */
72
64
  function tasksForBlock(blockId: string): SourceTask[] {
73
65
  return tasks.value.filter((t) => t.linkedBlockId === blockId)
74
66
  }
75
67
 
76
- /** Merge an issue returned by the backend into the local cache. */
77
- function upsertTask(task: SourceTask) {
78
- const i = tasks.value.findIndex(
79
- (t) => t.source === task.source && t.externalId === task.externalId,
80
- )
81
- if (i >= 0) tasks.value[i] = task
82
- else tasks.value.unshift(task)
83
- }
84
-
85
- function upsertConnection(conn: TaskConnection) {
86
- const i = connections.value.findIndex((c) => c.source === conn.source)
87
- if (i >= 0) connections.value[i] = conn
88
- else connections.value.push(conn)
89
- }
90
-
91
- /** Probe the integration: resolves `available`, the sources and connections. */
92
- async function probe() {
93
- if (!workspace.workspaceId) return
94
- try {
95
- const [{ sources: srcs }, { connections: conns }] = await Promise.all([
96
- api.listTaskSources(workspace.requireId()),
97
- api.listTaskConnections(workspace.requireId()),
98
- ])
99
- available.value = true
100
- probeError.value = null
101
- sources.value = srcs
102
- connections.value = conns
103
- } catch (e) {
104
- // 503 (integration disabled) or any error → hide the UI entry points, but keep
105
- // the reason so the settings panel can explain it (a 503 is "turned off on this
106
- // deployment"; a 500 is "the backend errored — e.g. a migration isn't applied").
107
- available.value = false
108
- const err = e as { statusCode?: number; data?: { error?: { message?: string } } }
109
- const serverMessage = err?.data?.error?.message
110
- probeError.value = {
111
- status: err?.statusCode ?? null,
112
- message: serverMessage || (e instanceof Error ? e.message : String(e)),
113
- }
114
- sources.value = []
115
- connections.value = []
116
- }
117
- }
118
-
119
68
  /**
120
69
  * Run a live setup check for a source (authenticate + read), caching the verdict
121
70
  * so the panel can show exactly what's wrong (missing App / wrong token / lacking
@@ -137,14 +86,14 @@ export const useTasksStore = defineStore('tasks', () => {
137
86
  /** Connect the workspace to a source with its credential bag. */
138
87
  async function connect(source: TaskSourceKind, credentials: Record<string, string>) {
139
88
  const conn = await api.connectTaskSource(workspace.requireId(), source, credentials)
140
- upsertConnection(conn)
89
+ integration.upsertConnection(conn)
141
90
  available.value = true
142
91
  }
143
92
 
144
93
  /** Disconnect the workspace from a source. */
145
94
  async function disconnect(source: TaskSourceKind) {
146
95
  await api.disconnectTaskSource(workspace.requireId(), source)
147
- connections.value = connections.value.filter((c) => c.source !== source)
96
+ integration.removeConnection(source)
148
97
  }
149
98
 
150
99
  /** Enable or disable a source for the workspace (the per-workspace toggle). */
package/app/stores/ui.ts CHANGED
@@ -91,6 +91,14 @@ export const useUiStore = defineStore('ui', () => {
91
91
  // is one. Every direct `open*` below resets it; `openFromIntegrations` sets it.
92
92
  const cameFromIntegrations = ref(false)
93
93
 
94
+ // Personal "My setup" hub: a user-scoped sibling of the Integrations hub, listing the
95
+ // signed-in user's own connections (GitHub PAT, local runners, personal subscriptions)
96
+ // separated out of the workspace-scoped Integrations hub. `cameFromPersonal` is the
97
+ // symmetric came-from marker, so a panel reached from here renders a "Back to My setup"
98
+ // control instead of "Back to Integrations".
99
+ const personalSetupOpen = ref(false)
100
+ const cameFromPersonal = ref(false)
101
+
94
102
  // Workspace-settings modal: a single tabbed window gathering the workspace-wide
95
103
  // config (workspace / merge thresholds / issue writeback / service best practices).
96
104
  // `workspaceSettingsTab` lets other surfaces deep-link straight to a tab.
@@ -112,8 +120,10 @@ export const useUiStore = defineStore('ui', () => {
112
120
  const providerConnectionKind = ref<'environment' | 'runner-pool' | null>(null)
113
121
  const modelConfigOpen = ref(false)
114
122
  // LLM-vendor subscription credentials (the token pool powering the Claude Code
115
- // / Codex harnesses).
123
+ // / Codex harnesses). `vendorCredentialsTab` lets a caller deep-link to one tab —
124
+ // the user-scoped "My subscriptions" entry opens straight onto the `personal` tab.
116
125
  const vendorCredentialsOpen = ref(false)
126
+ const vendorCredentialsTab = ref('pool')
117
127
  // Per-user settings panel: the signed-in user's own-machine local model runners.
118
128
  const localModelsOpen = ref(false)
119
129
  // Local-mode-only settings panel: the warm-container pool sizing + per-repo checkout reuse
@@ -267,7 +277,7 @@ export const useUiStore = defineStore('ui', () => {
267
277
  }
268
278
 
269
279
  function openDocumentConnect(source: DocumentSourceKind) {
270
- cameFromIntegrations.value = false
280
+ resetHubReturn()
271
281
  documentConnect.value = { source }
272
282
  }
273
283
  function closeDocumentConnect() {
@@ -277,7 +287,7 @@ export const useUiStore = defineStore('ui', () => {
277
287
  targetFrameId: string | null = null,
278
288
  source: DocumentSourceKind | null = null,
279
289
  ) {
280
- cameFromIntegrations.value = false
290
+ resetHubReturn()
281
291
  documentImport.value = { source, targetFrameId }
282
292
  }
283
293
  function closeDocumentImport() {
@@ -294,14 +304,14 @@ export const useUiStore = defineStore('ui', () => {
294
304
  spawnPreview.value = null
295
305
  }
296
306
  function openTaskConnect(source: TaskSourceKind) {
297
- cameFromIntegrations.value = false
307
+ resetHubReturn()
298
308
  taskConnect.value = { source }
299
309
  }
300
310
  function closeTaskConnect() {
301
311
  taskConnect.value = null
302
312
  }
303
313
  function openTaskImport(source: TaskSourceKind | null = null, containerId: string | null = null) {
304
- cameFromIntegrations.value = false
314
+ resetHubReturn()
305
315
  taskImport.value = { source, containerId }
306
316
  }
307
317
  function closeTaskImport() {
@@ -334,14 +344,14 @@ export const useUiStore = defineStore('ui', () => {
334
344
  addServiceOpen.value = false
335
345
  }
336
346
  function openGitHub() {
337
- cameFromIntegrations.value = false
347
+ resetHubReturn()
338
348
  githubOpen.value = true
339
349
  }
340
350
  function closeGitHub() {
341
351
  githubOpen.value = false
342
352
  }
343
353
  function openSlack() {
344
- cameFromIntegrations.value = false
354
+ resetHubReturn()
345
355
  slackOpen.value = true
346
356
  }
347
357
  function closeSlack() {
@@ -362,15 +372,37 @@ export const useUiStore = defineStore('ui', () => {
362
372
  function toggleCommandBar() {
363
373
  commandBarOpen.value = !commandBarOpen.value
364
374
  }
375
+ // Clear BOTH hub came-from markers. Every direct `open*` below calls this so that a
376
+ // panel opened outside the hubs never grows a dead Back control, and so switching from
377
+ // one hub's panel to the other's clears the stale marker.
378
+ function resetHubReturn() {
379
+ resetHubReturn()
380
+ cameFromPersonal.value = false
381
+ }
365
382
  function openIntegrations() {
366
383
  // Reaching the hub itself (fresh, or via a panel's Back control) clears the
367
- // came-from marker — we're at the hub, not inside a hub-spawned panel.
368
- cameFromIntegrations.value = false
384
+ // came-from markers — we're at the hub, not inside a hub-spawned panel.
385
+ resetHubReturn()
369
386
  integrationsOpen.value = true
370
387
  }
371
388
  function closeIntegrations() {
372
389
  integrationsOpen.value = false
373
390
  }
391
+ function openPersonalSetup() {
392
+ resetHubReturn()
393
+ personalSetupOpen.value = true
394
+ }
395
+ function closePersonalSetup() {
396
+ personalSetupOpen.value = false
397
+ }
398
+ // Open a user-scoped panel FROM the My-setup hub: run its open handler (which resets the
399
+ // markers), then mark that we came from My setup and dismiss it, so the panel's
400
+ // IntegrationBackTitle returns here rather than to the workspace Integrations hub.
401
+ function openFromPersonal(open: () => void) {
402
+ open()
403
+ cameFromPersonal.value = true
404
+ personalSetupOpen.value = false
405
+ }
374
406
  // Open an integration's own panel FROM the hub: run its open handler (which resets
375
407
  // `cameFromIntegrations`), then mark that we came from the hub and dismiss it. The
376
408
  // panel reads `cameFromIntegrations` to show its Back control.
@@ -380,7 +412,7 @@ export const useUiStore = defineStore('ui', () => {
380
412
  integrationsOpen.value = false
381
413
  }
382
414
  function openWorkspaceSettings(tab = 'workspace') {
383
- cameFromIntegrations.value = false
415
+ resetHubReturn()
384
416
  workspaceSettingsTab.value = tab
385
417
  workspaceSettingsOpen.value = true
386
418
  }
@@ -391,7 +423,7 @@ export const useUiStore = defineStore('ui', () => {
391
423
  workspaceSettingsTab.value = tab
392
424
  }
393
425
  function openAccountSettings(tab = 'team') {
394
- cameFromIntegrations.value = false
426
+ resetHubReturn()
395
427
  accountSettingsTab.value = tab
396
428
  accountSettingsOpen.value = true
397
429
  }
@@ -402,14 +434,14 @@ export const useUiStore = defineStore('ui', () => {
402
434
  accountSettingsTab.value = tab
403
435
  }
404
436
  function openObservabilityConnection() {
405
- cameFromIntegrations.value = false
437
+ resetHubReturn()
406
438
  observabilityConnectionOpen.value = true
407
439
  }
408
440
  function closeObservabilityConnection() {
409
441
  observabilityConnectionOpen.value = false
410
442
  }
411
443
  function openProviderConnection(kind: 'environment' | 'runner-pool') {
412
- cameFromIntegrations.value = false
444
+ resetHubReturn()
413
445
  providerConnectionKind.value = kind
414
446
  }
415
447
  function closeProviderConnection() {
@@ -421,22 +453,26 @@ export const useUiStore = defineStore('ui', () => {
421
453
  function closeModelConfig() {
422
454
  modelConfigOpen.value = false
423
455
  }
424
- function openVendorCredentials() {
425
- cameFromIntegrations.value = false
456
+ function openVendorCredentials(tab = 'pool') {
457
+ resetHubReturn()
458
+ vendorCredentialsTab.value = tab
426
459
  vendorCredentialsOpen.value = true
427
460
  }
461
+ function setVendorCredentialsTab(tab: string) {
462
+ vendorCredentialsTab.value = tab
463
+ }
428
464
  function closeVendorCredentials() {
429
465
  vendorCredentialsOpen.value = false
430
466
  }
431
467
  function openLocalModels() {
432
- cameFromIntegrations.value = false
468
+ resetHubReturn()
433
469
  localModelsOpen.value = true
434
470
  }
435
471
  function closeLocalModels() {
436
472
  localModelsOpen.value = false
437
473
  }
438
474
  function openLocalModeSettings() {
439
- cameFromIntegrations.value = false
475
+ resetHubReturn()
440
476
  localModeSettingsOpen.value = true
441
477
  }
442
478
  function closeLocalModeSettings() {
@@ -449,14 +485,14 @@ export const useUiStore = defineStore('ui', () => {
449
485
  sandboxOpen.value = false
450
486
  }
451
487
  function openUserSecrets() {
452
- cameFromIntegrations.value = false
488
+ resetHubReturn()
453
489
  userSecretsOpen.value = true
454
490
  }
455
491
  function closeUserSecrets() {
456
492
  userSecretsOpen.value = false
457
493
  }
458
494
  function openOpenRouter() {
459
- cameFromIntegrations.value = false
495
+ resetHubReturn()
460
496
  openRouterOpen.value = true
461
497
  }
462
498
  function closeOpenRouter() {
@@ -581,6 +617,8 @@ export const useUiStore = defineStore('ui', () => {
581
617
  commandBarOpen,
582
618
  integrationsOpen,
583
619
  cameFromIntegrations,
620
+ personalSetupOpen,
621
+ cameFromPersonal,
584
622
  workspaceSettingsOpen,
585
623
  workspaceSettingsTab,
586
624
  accountSettingsOpen,
@@ -589,6 +627,7 @@ export const useUiStore = defineStore('ui', () => {
589
627
  providerConnectionKind,
590
628
  modelConfigOpen,
591
629
  vendorCredentialsOpen,
630
+ vendorCredentialsTab,
592
631
  localModelsOpen,
593
632
  localModeSettingsOpen,
594
633
  sandboxOpen,
@@ -645,6 +684,9 @@ export const useUiStore = defineStore('ui', () => {
645
684
  openIntegrations,
646
685
  closeIntegrations,
647
686
  openFromIntegrations,
687
+ openPersonalSetup,
688
+ closePersonalSetup,
689
+ openFromPersonal,
648
690
  openWorkspaceSettings,
649
691
  closeWorkspaceSettings,
650
692
  setWorkspaceSettingsTab,
@@ -658,6 +700,7 @@ export const useUiStore = defineStore('ui', () => {
658
700
  openModelConfig,
659
701
  closeModelConfig,
660
702
  openVendorCredentials,
703
+ setVendorCredentialsTab,
661
704
  closeVendorCredentials,
662
705
  openLocalModels,
663
706
  closeLocalModels,