@cat-factory/app 0.6.0
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/LICENSE +21 -0
- package/README.md +88 -0
- package/app/app.config.ts +8 -0
- package/app/app.vue +11 -0
- package/app/assets/css/main.css +100 -0
- package/app/components/auth/AuthGate.vue +24 -0
- package/app/components/auth/LoginScreen.vue +143 -0
- package/app/components/auth/UserMenu.vue +39 -0
- package/app/components/board/AddTaskModal.vue +444 -0
- package/app/components/board/AgentFailureCard.vue +97 -0
- package/app/components/board/AgentStopButton.vue +61 -0
- package/app/components/board/BoardCanvas.vue +183 -0
- package/app/components/board/ContextPicker.vue +367 -0
- package/app/components/board/RecurringPipelineModal.vue +219 -0
- package/app/components/board/TaskDependencyEdges.vue +132 -0
- package/app/components/board/nodes/AgentChip.vue +59 -0
- package/app/components/board/nodes/BlockNode.vue +433 -0
- package/app/components/board/nodes/DecisionBadge.vue +27 -0
- package/app/components/board/nodes/DraggableTask.vue +48 -0
- package/app/components/board/nodes/ModuleFrame.vue +97 -0
- package/app/components/board/nodes/TaskCard.vue +359 -0
- package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
- package/app/components/bootstrap/BootstrapModal.vue +665 -0
- package/app/components/clarity/ClarityReviewWindow.vue +611 -0
- package/app/components/consensus/ConsensusSessionWindow.vue +210 -0
- package/app/components/documents/DocumentImportModal.vue +161 -0
- package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
- package/app/components/documents/SpawnPreviewModal.vue +161 -0
- package/app/components/documents/TaskContextDocs.vue +83 -0
- package/app/components/focus/BlockFocusView.vue +171 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
- package/app/components/gates/GateResultView.vue +282 -0
- package/app/components/github/AddServiceFromRepoModal.vue +354 -0
- package/app/components/github/GitHubConnect.vue +183 -0
- package/app/components/github/GitHubOnboarding.vue +45 -0
- package/app/components/github/GitHubPanel.vue +584 -0
- package/app/components/github/RepoTreeBrowser.vue +171 -0
- package/app/components/layout/AccountTeamSettings.vue +237 -0
- package/app/components/layout/BoardSwitcher.vue +280 -0
- package/app/components/layout/BoardToolbar.vue +156 -0
- package/app/components/layout/CommandBar.vue +336 -0
- package/app/components/layout/GitHubPatBanner.vue +73 -0
- package/app/components/layout/NotificationsInbox.vue +175 -0
- package/app/components/layout/SideBar.vue +314 -0
- package/app/components/layout/SpendWarningBanner.vue +107 -0
- package/app/components/observability/StepMetricsBar.vue +102 -0
- package/app/components/palettes/AgentPalette.vue +86 -0
- package/app/components/panels/AgentStepDetail.vue +737 -0
- package/app/components/panels/DecisionModal.vue +71 -0
- package/app/components/panels/InspectorPanel.vue +465 -0
- package/app/components/panels/ObservabilityPanel.vue +351 -0
- package/app/components/panels/StepMetadataCard.vue +253 -0
- package/app/components/panels/StepRestartControl.vue +90 -0
- package/app/components/panels/StepResultViewHost.vue +40 -0
- package/app/components/panels/StepTestReport.vue +84 -0
- package/app/components/panels/inspector/ContainerSummary.vue +74 -0
- package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
- package/app/components/panels/inspector/ServiceFragments.vue +82 -0
- package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
- package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
- package/app/components/panels/inspector/TaskDependencies.vue +70 -0
- package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
- package/app/components/panels/inspector/TaskExecution.vue +364 -0
- package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
- package/app/components/panels/inspector/TaskStructure.vue +96 -0
- package/app/components/pipeline/AgentKindIcon.vue +30 -0
- package/app/components/pipeline/IterationCapPrompt.vue +70 -0
- package/app/components/pipeline/PipelineBuilder.vue +817 -0
- package/app/components/pipeline/PipelineProgress.vue +484 -0
- package/app/components/providers/ApiKeysSection.vue +273 -0
- package/app/components/providers/PersonalCredentialModal.vue +128 -0
- package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
- package/app/components/providers/VendorCredentialsModal.vue +197 -0
- package/app/components/recurring/RecurrenceEditor.vue +124 -0
- package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
- package/app/components/settings/DatadogPanel.vue +213 -0
- package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
- package/app/components/settings/MergeThresholdsPanel.vue +378 -0
- package/app/components/settings/ModelDefaultsPanel.vue +250 -0
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
- package/app/components/slack/SlackPanel.vue +299 -0
- package/app/components/tasks/TaskContextIssues.vue +88 -0
- package/app/components/tasks/TaskImportModal.vue +207 -0
- package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
- package/app/components/testing/TestReportWindow.vue +404 -0
- package/app/composables/api/accounts.ts +81 -0
- package/app/composables/api/auth.ts +45 -0
- package/app/composables/api/board.ts +101 -0
- package/app/composables/api/bootstrap.ts +62 -0
- package/app/composables/api/context.ts +25 -0
- package/app/composables/api/documents.ts +74 -0
- package/app/composables/api/execution.ts +127 -0
- package/app/composables/api/fragments.ts +71 -0
- package/app/composables/api/github.ts +131 -0
- package/app/composables/api/models.ts +127 -0
- package/app/composables/api/notifications.ts +23 -0
- package/app/composables/api/presets.ts +29 -0
- package/app/composables/api/recurring.ts +68 -0
- package/app/composables/api/releaseHealth.ts +43 -0
- package/app/composables/api/reviews.ts +146 -0
- package/app/composables/api/slack.ts +54 -0
- package/app/composables/api/tasks.ts +72 -0
- package/app/composables/api/workspaces.ts +36 -0
- package/app/composables/useApi.ts +89 -0
- package/app/composables/useBlockDrag.ts +90 -0
- package/app/composables/useBlockQueries.ts +154 -0
- package/app/composables/useBoardFlow.ts +11 -0
- package/app/composables/useContextLinking.ts +65 -0
- package/app/composables/useDepLabels.ts +26 -0
- package/app/composables/useFrameResize.ts +54 -0
- package/app/composables/useResultView.ts +48 -0
- package/app/composables/useReviewStage.ts +40 -0
- package/app/composables/useSemanticZoom.ts +31 -0
- package/app/composables/useStepApproval.ts +233 -0
- package/app/composables/useStepProse.ts +78 -0
- package/app/composables/useStepTimer.ts +63 -0
- package/app/composables/useTaskExpansion.ts +92 -0
- package/app/composables/useWorkspaceStream.ts +155 -0
- package/app/docs/architecture.md +31 -0
- package/app/pages/index.vue +141 -0
- package/app/stores/accounts.ts +152 -0
- package/app/stores/agentConfig.ts +35 -0
- package/app/stores/agentRuns.ts +122 -0
- package/app/stores/agents.ts +40 -0
- package/app/stores/apiKeys.ts +108 -0
- package/app/stores/auth.ts +166 -0
- package/app/stores/board.spec.ts +205 -0
- package/app/stores/board.ts +286 -0
- package/app/stores/bootstrap.ts +97 -0
- package/app/stores/clarity.ts +196 -0
- package/app/stores/consensus.ts +60 -0
- package/app/stores/documents.ts +176 -0
- package/app/stores/execution.ts +273 -0
- package/app/stores/fragmentLibrary.ts +147 -0
- package/app/stores/fragments.ts +40 -0
- package/app/stores/github.ts +305 -0
- package/app/stores/localModels.ts +51 -0
- package/app/stores/mergePresets.ts +58 -0
- package/app/stores/modelDefaults.ts +76 -0
- package/app/stores/models.ts +134 -0
- package/app/stores/notifications.ts +70 -0
- package/app/stores/observability.ts +144 -0
- package/app/stores/personalSubscriptions.ts +215 -0
- package/app/stores/pipelines.ts +327 -0
- package/app/stores/recurringPipelines.ts +112 -0
- package/app/stores/releaseHealth.ts +75 -0
- package/app/stores/requirements.spec.ts +94 -0
- package/app/stores/requirements.ts +208 -0
- package/app/stores/serviceFragmentDefaults.ts +29 -0
- package/app/stores/services.ts +87 -0
- package/app/stores/slack.ts +142 -0
- package/app/stores/taskExpansion.ts +36 -0
- package/app/stores/tasks.spec.ts +71 -0
- package/app/stores/tasks.ts +176 -0
- package/app/stores/tracker.ts +27 -0
- package/app/stores/ui.ts +434 -0
- package/app/stores/vendorCredentials.ts +54 -0
- package/app/stores/workspace.ts +215 -0
- package/app/stores/workspaceSettings.ts +36 -0
- package/app/types/accounts.ts +77 -0
- package/app/types/bootstrap.ts +83 -0
- package/app/types/clarity.ts +59 -0
- package/app/types/consensus.ts +91 -0
- package/app/types/documents.ts +104 -0
- package/app/types/domain.ts +495 -0
- package/app/types/execution.ts +383 -0
- package/app/types/fragments.ts +72 -0
- package/app/types/github.ts +173 -0
- package/app/types/localModels.ts +73 -0
- package/app/types/merge.ts +71 -0
- package/app/types/models.ts +157 -0
- package/app/types/notifications.ts +74 -0
- package/app/types/recurring.ts +69 -0
- package/app/types/releaseHealth.ts +31 -0
- package/app/types/requirements.ts +61 -0
- package/app/types/services.ts +27 -0
- package/app/types/slack.ts +57 -0
- package/app/types/tasks.ts +82 -0
- package/app/types/tracker.ts +18 -0
- package/app/utils/agentOutput.spec.ts +128 -0
- package/app/utils/agentOutput.ts +173 -0
- package/app/utils/catalog.spec.ts +112 -0
- package/app/utils/catalog.ts +455 -0
- package/app/utils/dnd.ts +29 -0
- package/app/utils/observability.ts +52 -0
- package/app/utils/pipelineRender.ts +151 -0
- package/nuxt.config.ts +55 -0
- package/package.json +45 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import type {
|
|
4
|
+
CreateBranchInput,
|
|
5
|
+
CreateRepoRequest,
|
|
6
|
+
GitHubAvailableRepo,
|
|
7
|
+
GitHubBranch,
|
|
8
|
+
GitHubConnection,
|
|
9
|
+
GitHubInstallationOption,
|
|
10
|
+
GitHubIssue,
|
|
11
|
+
GitHubPullRequest,
|
|
12
|
+
GitHubRepo,
|
|
13
|
+
MergePullRequestInput,
|
|
14
|
+
OpenPullRequestInput,
|
|
15
|
+
ResyncRequest,
|
|
16
|
+
} from '~/types/domain'
|
|
17
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* GitHub integration state: the workspace's App installation, the projected
|
|
21
|
+
* repos/branches/pull-requests/issues the backend caches in D1, and the actions
|
|
22
|
+
* that connect/resync and write against the repo. `available` mirrors the
|
|
23
|
+
* backend's opt-in gate — a 503 from the connection probe means the integration
|
|
24
|
+
* is off, and the UI hides its entry points (exactly as the documents store
|
|
25
|
+
* gates on its source probe, and `auth.required` gates the login UI). Per
|
|
26
|
+
* workspace, like the board itself; nothing is persisted client-side.
|
|
27
|
+
*/
|
|
28
|
+
export const useGitHubStore = defineStore('github', () => {
|
|
29
|
+
const api = useApi()
|
|
30
|
+
const workspace = useWorkspaceStore()
|
|
31
|
+
|
|
32
|
+
/** null = unknown (not probed yet), true/false = integration on/off. */
|
|
33
|
+
const available = ref<boolean | null>(null)
|
|
34
|
+
/** The workspace's App installation, or null when not yet connected. */
|
|
35
|
+
const connection = ref<GitHubConnection | null>(null)
|
|
36
|
+
/** Discovered App installations for the connect picker; loaded on demand. */
|
|
37
|
+
const installations = ref<GitHubInstallationOption[]>([])
|
|
38
|
+
const loadingInstallations = ref(false)
|
|
39
|
+
const repos = ref<GitHubRepo[]>([])
|
|
40
|
+
/** Repos the installation can access, for the per-workspace link picker. */
|
|
41
|
+
const availableRepos = ref<GitHubAvailableRepo[]>([])
|
|
42
|
+
const loadingAvailable = ref(false)
|
|
43
|
+
const savingRepos = ref(false)
|
|
44
|
+
const pulls = ref<GitHubPullRequest[]>([])
|
|
45
|
+
const issues = ref<GitHubIssue[]>([])
|
|
46
|
+
/** Branches loaded lazily per repo (by GitHub numeric id). */
|
|
47
|
+
const branches = ref<Record<number, GitHubBranch[]>>({})
|
|
48
|
+
const loading = ref(false)
|
|
49
|
+
const syncing = ref(false)
|
|
50
|
+
|
|
51
|
+
const connected = computed(() => connection.value !== null)
|
|
52
|
+
/** Whether cat-factory can create repos under the connected account itself. */
|
|
53
|
+
const canCreateRepos = computed(() => connection.value?.canCreateRepos === true)
|
|
54
|
+
/**
|
|
55
|
+
* True when connected but the install is MISSING `workflows: write` — agent
|
|
56
|
+
* pushes that touch `.github/workflows/*` will be rejected until it's granted.
|
|
57
|
+
*/
|
|
58
|
+
const missingWorkflowsPermission = computed(
|
|
59
|
+
() => connection.value !== null && connection.value.canManageWorkflows !== true,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
function repoFor(repoGithubId: number): GitHubRepo | undefined {
|
|
63
|
+
return repos.value.find((r) => r.githubId === repoGithubId)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The repo linked to a board block (its backing service repo), if any. */
|
|
67
|
+
function repoForBlock(blockId: string): GitHubRepo | undefined {
|
|
68
|
+
return repos.value.find((r) => r.blockId === blockId)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function pullsForRepo(repoGithubId: number): GitHubPullRequest[] {
|
|
72
|
+
return pulls.value.filter((p) => p.repoGithubId === repoGithubId)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function issuesForRepo(repoGithubId: number): GitHubIssue[] {
|
|
76
|
+
return issues.value.filter((i) => i.repoGithubId === repoGithubId)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Build the github.com URL for a repo / PR / issue from the projection row. */
|
|
80
|
+
function repoUrl(repoGithubId: number): string | null {
|
|
81
|
+
const r = repoFor(repoGithubId)
|
|
82
|
+
return r ? `https://github.com/${r.owner}/${r.name}` : null
|
|
83
|
+
}
|
|
84
|
+
function pullUrl(pr: GitHubPullRequest): string | null {
|
|
85
|
+
const base = repoUrl(pr.repoGithubId)
|
|
86
|
+
return base ? `${base}/pull/${pr.number}` : null
|
|
87
|
+
}
|
|
88
|
+
function issueUrl(issue: GitHubIssue): string | null {
|
|
89
|
+
const base = repoUrl(issue.repoGithubId)
|
|
90
|
+
return base ? `${base}/issues/${issue.number}` : null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Probe the integration: resolves `available` and the current connection. */
|
|
94
|
+
async function probe() {
|
|
95
|
+
if (!workspace.workspaceId) return
|
|
96
|
+
try {
|
|
97
|
+
const { connection: conn } = await api.getGitHubConnection(workspace.requireId())
|
|
98
|
+
available.value = true
|
|
99
|
+
connection.value = conn
|
|
100
|
+
} catch {
|
|
101
|
+
// 503 (integration disabled) or any error → hide the UI entry points.
|
|
102
|
+
available.value = false
|
|
103
|
+
connection.value = null
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Load the cached repos, pull requests and issues for the workspace. */
|
|
108
|
+
async function load() {
|
|
109
|
+
if (!connected.value) return
|
|
110
|
+
loading.value = true
|
|
111
|
+
try {
|
|
112
|
+
const [r, p, i] = await Promise.all([
|
|
113
|
+
api.listGitHubRepos(workspace.requireId()),
|
|
114
|
+
api.listGitHubPullRequests(workspace.requireId()),
|
|
115
|
+
api.listGitHubIssues(workspace.requireId()),
|
|
116
|
+
])
|
|
117
|
+
repos.value = r
|
|
118
|
+
pulls.value = p
|
|
119
|
+
issues.value = i
|
|
120
|
+
} finally {
|
|
121
|
+
loading.value = false
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Ensure the projection (repos/PRs/issues) is loaded at least once — for views
|
|
127
|
+
* that need it without opening the GitHub panel (e.g. the inspector's repo link).
|
|
128
|
+
* Probes the integration first if it hasn't been yet.
|
|
129
|
+
*/
|
|
130
|
+
async function ensureLoaded() {
|
|
131
|
+
if (available.value === null) await probe()
|
|
132
|
+
if (connected.value && repos.value.length === 0) await load()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Load the repos the installation can access, with this workspace's link state. */
|
|
136
|
+
async function loadAvailableRepos() {
|
|
137
|
+
if (!connected.value) return
|
|
138
|
+
loadingAvailable.value = true
|
|
139
|
+
try {
|
|
140
|
+
availableRepos.value = await api.listGitHubAvailableRepos(workspace.requireId())
|
|
141
|
+
} finally {
|
|
142
|
+
loadingAvailable.value = false
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Set the exact set of repos this workspace links, then refresh projections. */
|
|
147
|
+
async function setLinkedRepos(repoGithubIds: number[]) {
|
|
148
|
+
savingRepos.value = true
|
|
149
|
+
try {
|
|
150
|
+
repos.value = await api.setGitHubLinkedRepos(workspace.requireId(), repoGithubIds)
|
|
151
|
+
// Reflect the new link state in the picker and refresh PRs/issues.
|
|
152
|
+
const linked = new Set(repoGithubIds)
|
|
153
|
+
availableRepos.value = availableRepos.value.map((r) => ({
|
|
154
|
+
...r,
|
|
155
|
+
linked: linked.has(r.githubId),
|
|
156
|
+
}))
|
|
157
|
+
await load()
|
|
158
|
+
} finally {
|
|
159
|
+
savingRepos.value = false
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Lazily load (and cache) the branches for a single repo. */
|
|
164
|
+
async function loadBranches(repoGithubId: number): Promise<GitHubBranch[]> {
|
|
165
|
+
const list = await api.listGitHubBranches(workspace.requireId(), repoGithubId)
|
|
166
|
+
branches.value = { ...branches.value, [repoGithubId]: list }
|
|
167
|
+
return list
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** List one level of a (monorepo) repo's tree, for the service-directory picker. */
|
|
171
|
+
function loadRepoTree(repoGithubId: number, path = '') {
|
|
172
|
+
return api.listGitHubRepoTree(workspace.requireId(), repoGithubId, path)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The URL a workspace owner visits to install the App against this workspace. */
|
|
176
|
+
function getInstallUrl(): Promise<string> {
|
|
177
|
+
return api.getGitHubInstallUrl(workspace.requireId()).then((r) => r.url)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Discover the App's installations so the user can connect one without typing an id. */
|
|
181
|
+
async function loadInstallations() {
|
|
182
|
+
loadingInstallations.value = true
|
|
183
|
+
try {
|
|
184
|
+
const { installations: list } = await api.listGitHubInstallations(workspace.requireId())
|
|
185
|
+
installations.value = list
|
|
186
|
+
} finally {
|
|
187
|
+
loadingInstallations.value = false
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Programmatic bind by installation id (the browser flow uses the redirect). */
|
|
192
|
+
async function connect(installationId: number) {
|
|
193
|
+
connection.value = await api.connectGitHub(workspace.requireId(), installationId)
|
|
194
|
+
available.value = true
|
|
195
|
+
await load()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function disconnect() {
|
|
199
|
+
await api.disconnectGitHub(workspace.requireId())
|
|
200
|
+
connection.value = null
|
|
201
|
+
repos.value = []
|
|
202
|
+
availableRepos.value = []
|
|
203
|
+
pulls.value = []
|
|
204
|
+
issues.value = []
|
|
205
|
+
branches.value = {}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Trigger a resync, then refresh projections (no-op for queued/backfill). */
|
|
209
|
+
async function resync(body: ResyncRequest = {}) {
|
|
210
|
+
syncing.value = true
|
|
211
|
+
try {
|
|
212
|
+
const res = await api.resyncGitHub(workspace.requireId(), body)
|
|
213
|
+
await load()
|
|
214
|
+
return res
|
|
215
|
+
} finally {
|
|
216
|
+
syncing.value = false
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---- repo writes ----------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Create a repository under the connected account (privileged App tier). Only
|
|
224
|
+
* meaningful when `canCreateRepos`; the backend 409s otherwise. Returns the
|
|
225
|
+
* created repo so the caller can confirm/link it.
|
|
226
|
+
*/
|
|
227
|
+
function createRepo(input: CreateRepoRequest) {
|
|
228
|
+
return api.createGitHubRepo(workspace.requireId(), input)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function createBranch(repoGithubId: number, input: CreateBranchInput) {
|
|
232
|
+
const branch = await api.createGitHubBranch(workspace.requireId(), repoGithubId, input)
|
|
233
|
+
const next = branches.value[repoGithubId] ?? []
|
|
234
|
+
branches.value = { ...branches.value, [repoGithubId]: [branch, ...next] }
|
|
235
|
+
return branch
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function openPullRequest(repoGithubId: number, input: OpenPullRequestInput) {
|
|
239
|
+
const pr = await api.openGitHubPullRequest(workspace.requireId(), repoGithubId, input)
|
|
240
|
+
const i = pulls.value.findIndex(
|
|
241
|
+
(p) => p.repoGithubId === pr.repoGithubId && p.number === pr.number,
|
|
242
|
+
)
|
|
243
|
+
if (i >= 0) pulls.value[i] = pr
|
|
244
|
+
else pulls.value.unshift(pr)
|
|
245
|
+
return pr
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function mergePullRequest(
|
|
249
|
+
repoGithubId: number,
|
|
250
|
+
number: number,
|
|
251
|
+
input: MergePullRequestInput = {},
|
|
252
|
+
) {
|
|
253
|
+
await api.mergeGitHubPullRequest(workspace.requireId(), repoGithubId, number, input)
|
|
254
|
+
// Optimistically reflect the merge until the next sync confirms it.
|
|
255
|
+
const i = pulls.value.findIndex((p) => p.repoGithubId === repoGithubId && p.number === number)
|
|
256
|
+
if (i >= 0) pulls.value[i] = { ...pulls.value[i]!, state: 'closed', merged: true }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function comment(repoGithubId: number, number: number, body: string) {
|
|
260
|
+
return api.commentGitHubIssue(workspace.requireId(), repoGithubId, number, body)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
available,
|
|
265
|
+
connection,
|
|
266
|
+
installations,
|
|
267
|
+
loadingInstallations,
|
|
268
|
+
repos,
|
|
269
|
+
availableRepos,
|
|
270
|
+
loadingAvailable,
|
|
271
|
+
savingRepos,
|
|
272
|
+
pulls,
|
|
273
|
+
issues,
|
|
274
|
+
branches,
|
|
275
|
+
loading,
|
|
276
|
+
syncing,
|
|
277
|
+
connected,
|
|
278
|
+
canCreateRepos,
|
|
279
|
+
missingWorkflowsPermission,
|
|
280
|
+
repoFor,
|
|
281
|
+
repoForBlock,
|
|
282
|
+
pullsForRepo,
|
|
283
|
+
issuesForRepo,
|
|
284
|
+
repoUrl,
|
|
285
|
+
pullUrl,
|
|
286
|
+
issueUrl,
|
|
287
|
+
probe,
|
|
288
|
+
load,
|
|
289
|
+
ensureLoaded,
|
|
290
|
+
loadAvailableRepos,
|
|
291
|
+
setLinkedRepos,
|
|
292
|
+
loadRepoTree,
|
|
293
|
+
loadBranches,
|
|
294
|
+
getInstallUrl,
|
|
295
|
+
loadInstallations,
|
|
296
|
+
connect,
|
|
297
|
+
disconnect,
|
|
298
|
+
resync,
|
|
299
|
+
createRepo,
|
|
300
|
+
createBranch,
|
|
301
|
+
openPullRequest,
|
|
302
|
+
mergePullRequest,
|
|
303
|
+
comment,
|
|
304
|
+
}
|
|
305
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import type {
|
|
4
|
+
LocalModelEndpoint,
|
|
5
|
+
LocalModelEndpointTestResult,
|
|
6
|
+
LocalRunner,
|
|
7
|
+
TestLocalModelEndpointInput,
|
|
8
|
+
UpsertLocalModelEndpointInput,
|
|
9
|
+
} from '~/types/localModels'
|
|
10
|
+
|
|
11
|
+
// The signed-in user's local model runner endpoints — Ollama / LM Studio / llama.cpp /
|
|
12
|
+
// vLLM / a custom OpenAI-compatible server running on their OWN machine. A runner lives
|
|
13
|
+
// on a person's box (`localhost:11434` means something different per member), so these are
|
|
14
|
+
// stored PER USER, not pooled on the workspace. The API key is write-only server-side and
|
|
15
|
+
// never returned; this store only carries the metadata (+ the enabled model ids). Loaded
|
|
16
|
+
// INDEPENDENTLY (not from the workspace snapshot) — like personal subscriptions.
|
|
17
|
+
export const useLocalModelsStore = defineStore('localModels', () => {
|
|
18
|
+
const api = useApi()
|
|
19
|
+
const endpoints = ref<LocalModelEndpoint[]>([])
|
|
20
|
+
const loading = ref(false)
|
|
21
|
+
|
|
22
|
+
async function load() {
|
|
23
|
+
loading.value = true
|
|
24
|
+
try {
|
|
25
|
+
const { endpoints: list } = await api.listLocalModelEndpoints()
|
|
26
|
+
endpoints.value = list
|
|
27
|
+
} catch {
|
|
28
|
+
// Auth disabled / not signed in / feature off → no local runners surface.
|
|
29
|
+
endpoints.value = []
|
|
30
|
+
} finally {
|
|
31
|
+
loading.value = false
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function upsert(input: UpsertLocalModelEndpointInput) {
|
|
36
|
+
const endpoint = await api.upsertLocalModelEndpoint(input.provider, input)
|
|
37
|
+
endpoints.value = [...endpoints.value.filter((e) => e.provider !== endpoint.provider), endpoint]
|
|
38
|
+
return endpoint
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function remove(provider: LocalRunner) {
|
|
42
|
+
await api.deleteLocalModelEndpoint(provider)
|
|
43
|
+
endpoints.value = endpoints.value.filter((e) => e.provider !== provider)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function test(input: TestLocalModelEndpointInput): Promise<LocalModelEndpointTestResult> {
|
|
47
|
+
return await api.testLocalModelEndpoint(input)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { endpoints, loading, load, upsert, remove, test }
|
|
51
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import type {
|
|
4
|
+
CreateMergePresetInput,
|
|
5
|
+
MergeThresholdPreset,
|
|
6
|
+
UpdateMergePresetInput,
|
|
7
|
+
} from '~/types/domain'
|
|
8
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The workspace's merge threshold presets — the library a task picks its
|
|
12
|
+
* auto-merge policy from (the `merger` step compares the PR assessment against the
|
|
13
|
+
* resolved preset). Hydrated from the workspace snapshot; managed via a small
|
|
14
|
+
* settings UI. The backend always keeps at least one default preset.
|
|
15
|
+
*/
|
|
16
|
+
export const useMergePresetsStore = defineStore('mergePresets', () => {
|
|
17
|
+
const api = useApi()
|
|
18
|
+
|
|
19
|
+
const presets = ref<MergeThresholdPreset[]>([])
|
|
20
|
+
|
|
21
|
+
function hydrate(list: MergeThresholdPreset[]) {
|
|
22
|
+
presets.value = [...list].sort((a, b) => a.createdAt - b.createdAt)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The workspace default (fallback for a task that picks none). */
|
|
26
|
+
const defaultPreset = computed(() => presets.value.find((p) => p.isDefault) ?? null)
|
|
27
|
+
|
|
28
|
+
/** Resolve a task's effective preset by id, falling back to the default. */
|
|
29
|
+
function resolve(presetId: string | undefined): MergeThresholdPreset | null {
|
|
30
|
+
if (presetId) {
|
|
31
|
+
const picked = presets.value.find((p) => p.id === presetId)
|
|
32
|
+
if (picked) return picked
|
|
33
|
+
}
|
|
34
|
+
return defaultPreset.value
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function create(input: CreateMergePresetInput) {
|
|
38
|
+
const ws = useWorkspaceStore()
|
|
39
|
+
const created = await api.createMergePreset(ws.requireId(), input)
|
|
40
|
+
await ws.refresh()
|
|
41
|
+
return created
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function update(presetId: string, patch: UpdateMergePresetInput) {
|
|
45
|
+
const ws = useWorkspaceStore()
|
|
46
|
+
const updated = await api.updateMergePreset(ws.requireId(), presetId, patch)
|
|
47
|
+
await ws.refresh()
|
|
48
|
+
return updated
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function remove(presetId: string) {
|
|
52
|
+
const ws = useWorkspaceStore()
|
|
53
|
+
await api.deleteMergePreset(ws.requireId(), presetId)
|
|
54
|
+
await ws.refresh()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { presets, defaultPreset, resolve, hydrate, create, update, remove }
|
|
58
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The workspace's per-agent-kind default model overrides — the map an agent step
|
|
7
|
+
* resolves its model from when the task pins none (a block-pinned model still
|
|
8
|
+
* wins; a kind absent from the map falls back to the deployment's env routing).
|
|
9
|
+
* Hydrated from the workspace snapshot; edited via the Default-models settings
|
|
10
|
+
* panel, which replaces the whole map on save.
|
|
11
|
+
*/
|
|
12
|
+
export const useModelDefaultsStore = defineStore('modelDefaults', () => {
|
|
13
|
+
const api = useApi()
|
|
14
|
+
|
|
15
|
+
/** agentKind → model catalog id. */
|
|
16
|
+
const defaults = ref<Record<string, string>>({})
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The deployment's env-routing defaults as `provider:model` refs — what a kind
|
|
20
|
+
* runs on when neither the task nor this workspace pins a model. `default` is the
|
|
21
|
+
* global fallback; `byKind` carries kinds the operator routed specifically. Used
|
|
22
|
+
* only to NAME the fallback in the settings panel; it never overrides a pin.
|
|
23
|
+
*/
|
|
24
|
+
const deployment = ref<{ default: string; byKind: Record<string, string> }>({
|
|
25
|
+
default: '',
|
|
26
|
+
byKind: {},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
function hydrate(map: Record<string, string> | undefined) {
|
|
30
|
+
defaults.value = { ...map }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hydrateDeployment(
|
|
34
|
+
next: { default: string; byKind: Record<string, string> } | undefined,
|
|
35
|
+
) {
|
|
36
|
+
deployment.value = next
|
|
37
|
+
? { default: next.default, byKind: { ...next.byKind } }
|
|
38
|
+
: {
|
|
39
|
+
default: '',
|
|
40
|
+
byKind: {},
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The model id chosen for a kind, or undefined when it falls back to routing. */
|
|
45
|
+
function forKind(kind: string): string | undefined {
|
|
46
|
+
return defaults.value[kind]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The deployment-routing model ref a kind falls back to (`byKind[kind] ?? default`). */
|
|
50
|
+
function deploymentRefForKind(kind: string): string | undefined {
|
|
51
|
+
return deployment.value.byKind[kind] || deployment.value.default || undefined
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Set (or, with `null`, clear) the default model for a single agent kind, then
|
|
56
|
+
* persist the whole map. The backend replaces the stored set on every write.
|
|
57
|
+
*/
|
|
58
|
+
async function set(kind: string, modelId: string | null) {
|
|
59
|
+
const next = { ...defaults.value }
|
|
60
|
+
if (modelId) next[kind] = modelId
|
|
61
|
+
else delete next[kind]
|
|
62
|
+
const ws = useWorkspaceStore()
|
|
63
|
+
const saved = await api.setModelDefaults(ws.requireId(), next)
|
|
64
|
+
defaults.value = { ...saved.defaults }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
defaults,
|
|
69
|
+
deployment,
|
|
70
|
+
hydrate,
|
|
71
|
+
hydrateDeployment,
|
|
72
|
+
forKind,
|
|
73
|
+
deploymentRefForKind,
|
|
74
|
+
set,
|
|
75
|
+
}
|
|
76
|
+
})
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import type { ModelCost, ModelOption, SubscriptionVendor } from '~/types/domain'
|
|
4
|
+
|
|
5
|
+
/** The flavour of a model to actually display/run, given configured subscriptions. */
|
|
6
|
+
export interface DisplayFlavor {
|
|
7
|
+
providerLabel: string
|
|
8
|
+
provider: string
|
|
9
|
+
model: string
|
|
10
|
+
contextTokens?: number
|
|
11
|
+
cost?: ModelCost
|
|
12
|
+
/** True ⇒ flat-rate quota; its cost is a quota burn rate, not budget spend. */
|
|
13
|
+
quotaBased: boolean
|
|
14
|
+
vendor?: SubscriptionVendor
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The flavour a model resolves to in the picker given the workspace's configured
|
|
19
|
+
* subscription vendors. A dual-mode model (GLM/Kimi) collapses to its subscription
|
|
20
|
+
* flavour once that vendor is connected ("subscriptions always win"); otherwise the
|
|
21
|
+
* base (cloudflare/direct, or the subscription itself for subscription-only models).
|
|
22
|
+
*/
|
|
23
|
+
export function displayFlavor(m: ModelOption, configured: Set<SubscriptionVendor>): DisplayFlavor {
|
|
24
|
+
if (m.subscription && configured.has(m.subscription.vendor)) {
|
|
25
|
+
return {
|
|
26
|
+
providerLabel: m.subscription.providerLabel,
|
|
27
|
+
provider: m.subscription.provider,
|
|
28
|
+
model: m.subscription.model,
|
|
29
|
+
contextTokens: m.subscription.contextTokens,
|
|
30
|
+
cost: m.subscription.cost,
|
|
31
|
+
quotaBased: true,
|
|
32
|
+
vendor: m.subscription.vendor,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
providerLabel: m.providerLabel,
|
|
37
|
+
provider: m.provider,
|
|
38
|
+
model: m.model,
|
|
39
|
+
contextTokens: m.contextTokens,
|
|
40
|
+
cost: m.cost,
|
|
41
|
+
quotaBased: m.quotaBased ?? false,
|
|
42
|
+
vendor: m.vendor,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether a model is selectable. On the per-workspace catalog the backend already
|
|
48
|
+
* computes `available` from the configured API keys / subscriptions / Cloudflare opt-in,
|
|
49
|
+
* so honour it directly. On the deployment catalog (`available` absent) fall back to the
|
|
50
|
+
* subscription-token heuristic so the picker still gates subscription-only models.
|
|
51
|
+
*/
|
|
52
|
+
export function isSelectable(m: ModelOption, configured: Set<SubscriptionVendor>): boolean {
|
|
53
|
+
if (m.available !== undefined) return m.available
|
|
54
|
+
if (m.flavor === 'subscription' && m.vendor) return configured.has(m.vendor)
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Compact context-window label, e.g. `200K`. */
|
|
59
|
+
export function contextLabel(tokens: number | undefined): string | undefined {
|
|
60
|
+
if (!tokens) return undefined
|
|
61
|
+
return tokens >= 1000 ? `${Math.round(tokens / 1000)}K` : `${tokens}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** One-line cost/quota suffix for the picker. */
|
|
65
|
+
export function costLabel(flavor: DisplayFlavor): string | undefined {
|
|
66
|
+
if (!flavor.cost) return undefined
|
|
67
|
+
const { inputPerMillion, outputPerMillion, currency } = flavor.cost
|
|
68
|
+
const body = `${inputPerMillion}/${outputPerMillion} ${currency} per Mtok`
|
|
69
|
+
return flavor.quotaBased ? `quota burn ~${body}` : body
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The model picker catalog. Served by `GET /models`, where each model is already
|
|
74
|
+
* resolved to the flavour in use for this deployment (direct when the provider's
|
|
75
|
+
* key is configured, else the Cloudflare fallback). Fetched once and cached for
|
|
76
|
+
* the per-block picker, and used to label which model produced a step's output.
|
|
77
|
+
*/
|
|
78
|
+
export const useModelsStore = defineStore('models', () => {
|
|
79
|
+
const api = useApi()
|
|
80
|
+
const models = ref<ModelOption[]>([])
|
|
81
|
+
const loaded = ref(false)
|
|
82
|
+
const loadedWorkspaceId = ref<string | null>(null)
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Fetch the catalog. Pass a `workspaceId` for the per-workspace catalog (selectability
|
|
86
|
+
* reflects that workspace's configured keys/subscriptions); re-fetches when the
|
|
87
|
+
* workspace changes. Without one, the deployment-level catalog is loaded once.
|
|
88
|
+
*/
|
|
89
|
+
async function ensureLoaded(workspaceId?: string) {
|
|
90
|
+
if (workspaceId) {
|
|
91
|
+
if (loaded.value && loadedWorkspaceId.value === workspaceId) return
|
|
92
|
+
models.value = await api.getWorkspaceModels(workspaceId)
|
|
93
|
+
loadedWorkspaceId.value = workspaceId
|
|
94
|
+
loaded.value = true
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
if (loaded.value) return
|
|
98
|
+
models.value = await api.getModels()
|
|
99
|
+
loaded.value = true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Force a re-fetch of the per-workspace catalog (e.g. after adding an API key). */
|
|
103
|
+
async function refresh(workspaceId: string) {
|
|
104
|
+
models.value = await api.getWorkspaceModels(workspaceId)
|
|
105
|
+
loadedWorkspaceId.value = workspaceId
|
|
106
|
+
loaded.value = true
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const byId = computed(() => {
|
|
110
|
+
const map = new Map<string, ModelOption>()
|
|
111
|
+
for (const m of models.value) map.set(m.id, m)
|
|
112
|
+
return map
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
function getModel(id: string | undefined) {
|
|
116
|
+
return id ? byId.value.get(id) : undefined
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Friendly label for a recorded `provider:model` identifier (as carried on a
|
|
121
|
+
* pipeline step). Matches it against the catalog's effective refs; falls back
|
|
122
|
+
* to the bare model id for anything not in the catalog (e.g. a pinned override).
|
|
123
|
+
*/
|
|
124
|
+
function labelForRef(ref: string | undefined): string | undefined {
|
|
125
|
+
if (!ref) return undefined
|
|
126
|
+
const idx = ref.indexOf(':')
|
|
127
|
+
const provider = idx === -1 ? ref : ref.slice(0, idx)
|
|
128
|
+
const model = idx === -1 ? '' : ref.slice(idx + 1)
|
|
129
|
+
const hit = models.value.find((m) => m.provider === provider && m.model === model)
|
|
130
|
+
return hit ? `${hit.label} · ${hit.providerLabel}` : model || ref
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { models, loaded, ensureLoaded, refresh, byId, getModel, labelForRef }
|
|
134
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import type { Notification } from '~/types/domain'
|
|
4
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Open, human-actionable notifications surfaced on the board (a PR awaiting a
|
|
8
|
+
* merge decision, a completed pipeline awaiting confirmation, CI that gave up).
|
|
9
|
+
* Hydrated from the workspace snapshot and patched live by the `notification`
|
|
10
|
+
* WorkspaceEvent (see `useWorkspaceStream`). The board renders an inbox + a
|
|
11
|
+
* per-block badge from `open` / `byBlock`.
|
|
12
|
+
*/
|
|
13
|
+
export const useNotificationsStore = defineStore('notifications', () => {
|
|
14
|
+
const api = useApi()
|
|
15
|
+
|
|
16
|
+
/** All open notifications, newest-first. */
|
|
17
|
+
const open = ref<Notification[]>([])
|
|
18
|
+
|
|
19
|
+
/** Replace the cache from a server snapshot. */
|
|
20
|
+
function hydrate(notifications: Notification[]) {
|
|
21
|
+
open.value = [...notifications]
|
|
22
|
+
.filter((n) => n.status === 'open')
|
|
23
|
+
.sort((a, b) => b.createdAt - a.createdAt)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Patch one notification from a real-time event: an `open` one is inserted /
|
|
28
|
+
* replaced in place; a resolved one (acted/dismissed) is removed from the inbox.
|
|
29
|
+
*/
|
|
30
|
+
function upsert(notification: Notification) {
|
|
31
|
+
const i = open.value.findIndex((n) => n.id === notification.id)
|
|
32
|
+
if (notification.status !== 'open') {
|
|
33
|
+
if (i >= 0) open.value.splice(i, 1)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
if (i >= 0) open.value[i] = notification
|
|
37
|
+
else open.value.unshift(notification)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Open notifications for a given block (for the board card badge). */
|
|
41
|
+
const byBlock = computed<Record<string, Notification[]>>(() => {
|
|
42
|
+
const map: Record<string, Notification[]> = {}
|
|
43
|
+
for (const n of open.value) {
|
|
44
|
+
if (!n.blockId) continue
|
|
45
|
+
;(map[n.blockId] ??= []).push(n)
|
|
46
|
+
}
|
|
47
|
+
return map
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
/** Total open count, for the toolbar badge. */
|
|
51
|
+
const count = computed(() => open.value.length)
|
|
52
|
+
|
|
53
|
+
/** Act on a notification (merge / confirm / retry); the board patches via the event. */
|
|
54
|
+
async function act(id: string) {
|
|
55
|
+
const ws = useWorkspaceStore()
|
|
56
|
+
const resolved = await api.actNotification(ws.requireId(), id)
|
|
57
|
+
upsert(resolved)
|
|
58
|
+
// The action (merge/confirm/retry) changed block/run state — reconcile fully.
|
|
59
|
+
await ws.refresh()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Dismiss a notification without acting. */
|
|
63
|
+
async function dismiss(id: string) {
|
|
64
|
+
const ws = useWorkspaceStore()
|
|
65
|
+
const resolved = await api.dismissNotification(ws.requireId(), id)
|
|
66
|
+
upsert(resolved)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { open, hydrate, upsert, byBlock, count, act, dismiss }
|
|
70
|
+
})
|