@cat-factory/app 1.0.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 +18 -0
- package/app/components/auth/UserMenu.vue +39 -0
- package/app/components/board/AgentFailureCard.vue +97 -0
- package/app/components/board/AgentStopButton.vue +61 -0
- package/app/components/board/BoardCanvas.vue +146 -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 +347 -0
- package/app/components/board/nodes/DecisionBadge.vue +21 -0
- package/app/components/board/nodes/DraggableTask.vue +69 -0
- package/app/components/board/nodes/ModuleFrame.vue +70 -0
- package/app/components/board/nodes/TaskCard.vue +237 -0
- package/app/components/bootstrap/BootstrapModal.vue +665 -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 +161 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
- package/app/components/github/GitHubConnect.vue +183 -0
- package/app/components/github/GitHubPanel.vue +584 -0
- package/app/components/layout/BoardSwitcher.vue +202 -0
- package/app/components/layout/BoardToolbar.vue +109 -0
- package/app/components/layout/SideBar.vue +193 -0
- package/app/components/layout/SpendWarningBanner.vue +107 -0
- package/app/components/palettes/AgentPalette.vue +33 -0
- package/app/components/palettes/BlockPalette.vue +41 -0
- package/app/components/palettes/PipelinePalette.vue +74 -0
- package/app/components/panels/DecisionModal.vue +71 -0
- package/app/components/panels/InspectorPanel.vue +296 -0
- package/app/components/panels/inspector/ContainerSummary.vue +74 -0
- package/app/components/panels/inspector/TaskDependencies.vue +70 -0
- package/app/components/panels/inspector/TaskExecution.vue +175 -0
- package/app/components/panels/inspector/TaskModelSettings.vue +128 -0
- package/app/components/panels/inspector/TaskStructure.vue +139 -0
- package/app/components/pipeline/PipelineBuilder.vue +227 -0
- package/app/components/pipeline/PipelineProgress.vue +246 -0
- package/app/components/requirements/RequirementReviewModal.vue +328 -0
- package/app/components/scenarios/FeatureScenarios.vue +162 -0
- package/app/components/scenarios/ScenarioCard.vue +109 -0
- package/app/components/tasks/TaskContextIssues.vue +88 -0
- package/app/components/tasks/TaskImportModal.vue +140 -0
- package/app/components/tasks/TaskSourceConnectModal.vue +122 -0
- package/app/composables/useApi.ts +535 -0
- package/app/composables/useBlockDrag.ts +75 -0
- package/app/composables/useBlockQueries.ts +136 -0
- package/app/composables/useBoardFlow.ts +11 -0
- package/app/composables/useDepLabels.ts +26 -0
- package/app/composables/useSemanticZoom.ts +16 -0
- package/app/composables/useWorkspaceStream.ts +125 -0
- package/app/docs/architecture.md +31 -0
- package/app/pages/index.vue +80 -0
- package/app/stores/accounts.ts +64 -0
- package/app/stores/agentRuns.ts +117 -0
- package/app/stores/agents.ts +40 -0
- package/app/stores/auth.ts +97 -0
- package/app/stores/board.spec.ts +197 -0
- package/app/stores/board.ts +147 -0
- package/app/stores/bootstrap.ts +97 -0
- package/app/stores/documents.ts +165 -0
- package/app/stores/execution.ts +115 -0
- package/app/stores/fragmentLibrary.ts +147 -0
- package/app/stores/fragments.ts +40 -0
- package/app/stores/github.ts +291 -0
- package/app/stores/models.ts +48 -0
- package/app/stores/pipelines.ts +77 -0
- package/app/stores/requirements.ts +133 -0
- package/app/stores/scenarios.spec.ts +82 -0
- package/app/stores/scenarios.ts +196 -0
- package/app/stores/tasks.spec.ts +71 -0
- package/app/stores/tasks.ts +149 -0
- package/app/stores/ui.ts +204 -0
- package/app/stores/workspace.ts +201 -0
- package/app/types/accounts.ts +38 -0
- package/app/types/bootstrap.ts +83 -0
- package/app/types/documents.ts +92 -0
- package/app/types/domain.ts +216 -0
- package/app/types/execution.ts +110 -0
- package/app/types/fragments.ts +72 -0
- package/app/types/github.ts +153 -0
- package/app/types/models.ts +48 -0
- package/app/types/requirements.ts +38 -0
- package/app/types/scenarios.ts +36 -0
- package/app/types/tasks.ts +67 -0
- package/app/utils/catalog.spec.ts +82 -0
- package/app/utils/catalog.ts +185 -0
- package/app/utils/dnd.ts +29 -0
- package/nuxt.config.ts +43 -0
- package/package.json +43 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Account,
|
|
3
|
+
AccountMember,
|
|
4
|
+
AddMemberInput,
|
|
5
|
+
AgentRunKind,
|
|
6
|
+
AuthUser,
|
|
7
|
+
Block,
|
|
8
|
+
BlockType,
|
|
9
|
+
BootstrapJob,
|
|
10
|
+
BootstrapRepoInput,
|
|
11
|
+
DocumentBoardPlan,
|
|
12
|
+
DocumentConnection,
|
|
13
|
+
DocumentSourceDescriptor,
|
|
14
|
+
DocumentSourceKind,
|
|
15
|
+
SourceDocument,
|
|
16
|
+
CreateReferenceArchitectureInput,
|
|
17
|
+
ExecutionInstance,
|
|
18
|
+
CommitFilesInput,
|
|
19
|
+
CreateBranchInput,
|
|
20
|
+
CreatedRepo,
|
|
21
|
+
CreateRepoRequest,
|
|
22
|
+
GitHubAvailableRepo,
|
|
23
|
+
GitHubBranch,
|
|
24
|
+
GitHubConnection,
|
|
25
|
+
GitHubInstallationOption,
|
|
26
|
+
GitHubIssue,
|
|
27
|
+
GitHubPullRequest,
|
|
28
|
+
GitHubRepo,
|
|
29
|
+
MergePullRequestInput,
|
|
30
|
+
ModelOption,
|
|
31
|
+
OpenPullRequestInput,
|
|
32
|
+
Pipeline,
|
|
33
|
+
PromptFragment,
|
|
34
|
+
CreatePromptFragmentInput,
|
|
35
|
+
UpdatePromptFragmentInput,
|
|
36
|
+
ResolvedFragment,
|
|
37
|
+
FragmentOwnerKind,
|
|
38
|
+
FragmentSource,
|
|
39
|
+
LinkFragmentSourceInput,
|
|
40
|
+
FragmentSourceStatus,
|
|
41
|
+
FragmentSyncResult,
|
|
42
|
+
ReferenceArchitecture,
|
|
43
|
+
ResyncRequest,
|
|
44
|
+
SpawnResult,
|
|
45
|
+
TaskConnection,
|
|
46
|
+
TaskSourceDescriptor,
|
|
47
|
+
TaskSourceKind,
|
|
48
|
+
SourceTask,
|
|
49
|
+
UpdateReferenceArchitectureInput,
|
|
50
|
+
Workspace,
|
|
51
|
+
WorkspaceSnapshot,
|
|
52
|
+
} from '~/types/domain'
|
|
53
|
+
import type { RequirementReview, ReviewItemStatus } from '~/types/requirements'
|
|
54
|
+
|
|
55
|
+
type Position = { x: number; y: number }
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Thin typed client over the cat-factory backend (a Hono worker). Every method
|
|
59
|
+
* maps to one REST endpoint; the request/response shapes mirror
|
|
60
|
+
* `@cat-factory/contracts`, so responses drop straight into the Pinia stores.
|
|
61
|
+
*
|
|
62
|
+
* The base URL comes from runtime config (`NUXT_PUBLIC_API_BASE`), defaulting to
|
|
63
|
+
* the local wrangler dev server — see `nuxt.config.ts`.
|
|
64
|
+
*/
|
|
65
|
+
export function useApi() {
|
|
66
|
+
const apiBase = useRuntimeConfig().public.apiBase
|
|
67
|
+
const http = $fetch.create({
|
|
68
|
+
baseURL: apiBase,
|
|
69
|
+
// Attach the session token (when signed in) so the backend's auth gate lets
|
|
70
|
+
// the request through. Read lazily from the store so a fresh token applies
|
|
71
|
+
// without rebuilding the client.
|
|
72
|
+
onRequest({ options }) {
|
|
73
|
+
const token = useAuthStore().token
|
|
74
|
+
if (!token) return
|
|
75
|
+
const headers = new Headers(options.headers)
|
|
76
|
+
headers.set('Authorization', `Bearer ${token}`)
|
|
77
|
+
options.headers = headers
|
|
78
|
+
},
|
|
79
|
+
// A 401 means our token lapsed or was revoked — drop it so the UI re-gates.
|
|
80
|
+
onResponseError({ response }) {
|
|
81
|
+
if (response?.status === 401) useAuthStore().handleUnauthorized()
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const ws = (workspaceId: string) => `/workspaces/${encodeURIComponent(workspaceId)}`
|
|
86
|
+
// Prompt-fragment library routes exist at both tiers; resolve the prefix from
|
|
87
|
+
// the owner scope (ADR 0006 §8).
|
|
88
|
+
const scope = (kind: FragmentOwnerKind, id: string) =>
|
|
89
|
+
kind === 'account'
|
|
90
|
+
? `/accounts/${encodeURIComponent(id)}`
|
|
91
|
+
: `/workspaces/${encodeURIComponent(id)}`
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
// ---- auth -------------------------------------------------------------
|
|
95
|
+
getAuthConfig: () => http<{ enabled: boolean }>('/auth/config'),
|
|
96
|
+
|
|
97
|
+
getMe: () => http<{ user: AuthUser | null; enabled: boolean }>('/auth/me'),
|
|
98
|
+
|
|
99
|
+
logout: () => http('/auth/logout', { method: 'POST' }),
|
|
100
|
+
|
|
101
|
+
// Mint a short-lived, workspace-scoped ticket for the events WebSocket. A
|
|
102
|
+
// browser can't set Authorization on a WS handshake, so the socket auths from
|
|
103
|
+
// this `?ticket=` instead of the long-lived session token. Empty string when
|
|
104
|
+
// auth is disabled (dev) — the handshake is open in that case.
|
|
105
|
+
mintEventsTicket: (workspaceId: string) =>
|
|
106
|
+
http<{ ticket: string; expiresInMs?: number }>(`${ws(workspaceId)}/events/ticket`, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
}),
|
|
109
|
+
|
|
110
|
+
// ---- prompt fragments (best-practice catalog) -------------------------
|
|
111
|
+
getPromptFragments: () => http<PromptFragment[]>('/prompt-fragments'),
|
|
112
|
+
|
|
113
|
+
// ---- prompt-fragment library (managed, tenant-scoped; ADR 0006) -------
|
|
114
|
+
// The merged catalog an agent actually sees for a board (builtin∪account∪ws).
|
|
115
|
+
getResolvedFragments: (workspaceId: string) =>
|
|
116
|
+
http<ResolvedFragment[]>(`${ws(workspaceId)}/prompt-fragments/resolved`),
|
|
117
|
+
|
|
118
|
+
// Per-tier management (scope = account or workspace).
|
|
119
|
+
listFragments: (kind: FragmentOwnerKind, id: string) =>
|
|
120
|
+
http<PromptFragment[]>(`${scope(kind, id)}/prompt-fragments`),
|
|
121
|
+
|
|
122
|
+
createFragment: (kind: FragmentOwnerKind, id: string, body: CreatePromptFragmentInput) =>
|
|
123
|
+
http<PromptFragment>(`${scope(kind, id)}/prompt-fragments`, { method: 'POST', body }),
|
|
124
|
+
|
|
125
|
+
updateFragment: (
|
|
126
|
+
kind: FragmentOwnerKind,
|
|
127
|
+
id: string,
|
|
128
|
+
fragmentId: string,
|
|
129
|
+
body: UpdatePromptFragmentInput,
|
|
130
|
+
) =>
|
|
131
|
+
http<PromptFragment>(
|
|
132
|
+
`${scope(kind, id)}/prompt-fragments/${encodeURIComponent(fragmentId)}`,
|
|
133
|
+
{ method: 'PATCH', body },
|
|
134
|
+
),
|
|
135
|
+
|
|
136
|
+
deleteFragment: (kind: FragmentOwnerKind, id: string, fragmentId: string) =>
|
|
137
|
+
http(`${scope(kind, id)}/prompt-fragments/${encodeURIComponent(fragmentId)}`, {
|
|
138
|
+
method: 'DELETE',
|
|
139
|
+
}),
|
|
140
|
+
|
|
141
|
+
// Repo sources of guideline Markdown.
|
|
142
|
+
listFragmentSources: (kind: FragmentOwnerKind, id: string) =>
|
|
143
|
+
http<FragmentSource[]>(`${scope(kind, id)}/fragment-sources`),
|
|
144
|
+
|
|
145
|
+
linkFragmentSource: (kind: FragmentOwnerKind, id: string, body: LinkFragmentSourceInput) =>
|
|
146
|
+
http<FragmentSource>(`${scope(kind, id)}/fragment-sources`, { method: 'POST', body }),
|
|
147
|
+
|
|
148
|
+
unlinkFragmentSource: (kind: FragmentOwnerKind, id: string, sourceId: string) =>
|
|
149
|
+
http(`${scope(kind, id)}/fragment-sources/${encodeURIComponent(sourceId)}`, {
|
|
150
|
+
method: 'DELETE',
|
|
151
|
+
}),
|
|
152
|
+
|
|
153
|
+
fragmentSourceStatus: (kind: FragmentOwnerKind, id: string, sourceId: string) =>
|
|
154
|
+
http<FragmentSourceStatus>(
|
|
155
|
+
`${scope(kind, id)}/fragment-sources/${encodeURIComponent(sourceId)}/status`,
|
|
156
|
+
),
|
|
157
|
+
|
|
158
|
+
syncFragmentSource: (kind: FragmentOwnerKind, id: string, sourceId: string) =>
|
|
159
|
+
http<FragmentSyncResult>(
|
|
160
|
+
`${scope(kind, id)}/fragment-sources/${encodeURIComponent(sourceId)}/sync`,
|
|
161
|
+
{ method: 'POST' },
|
|
162
|
+
),
|
|
163
|
+
|
|
164
|
+
// ---- model picker catalog (effective per-deployment flavours) ---------
|
|
165
|
+
getModels: () => http<ModelOption[]>('/models'),
|
|
166
|
+
|
|
167
|
+
// ---- accounts (tenancy) -----------------------------------------------
|
|
168
|
+
// The accounts the user can switch between (personal + orgs), org creation
|
|
169
|
+
// and membership management. Empty when auth is disabled (dev).
|
|
170
|
+
listAccounts: () => http<Account[]>('/accounts'),
|
|
171
|
+
|
|
172
|
+
createAccount: (body: { name: string; githubAccountLogin?: string }) =>
|
|
173
|
+
http<Account>('/accounts', { method: 'POST', body }),
|
|
174
|
+
|
|
175
|
+
listAccountMembers: (accountId: string) =>
|
|
176
|
+
http<AccountMember[]>(`/accounts/${encodeURIComponent(accountId)}/members`),
|
|
177
|
+
|
|
178
|
+
addAccountMember: (accountId: string, body: AddMemberInput) =>
|
|
179
|
+
http<AccountMember>(`/accounts/${encodeURIComponent(accountId)}/members`, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
body,
|
|
182
|
+
}),
|
|
183
|
+
|
|
184
|
+
// ---- workspaces -------------------------------------------------------
|
|
185
|
+
listWorkspaces: () => http<Workspace[]>('/workspaces'),
|
|
186
|
+
|
|
187
|
+
createWorkspace: (body: { name?: string; seed?: boolean; accountId?: string } = {}) =>
|
|
188
|
+
http<WorkspaceSnapshot>('/workspaces', { method: 'POST', body }),
|
|
189
|
+
|
|
190
|
+
getWorkspace: (workspaceId: string) => http<WorkspaceSnapshot>(ws(workspaceId)),
|
|
191
|
+
|
|
192
|
+
renameWorkspace: (workspaceId: string, name: string) =>
|
|
193
|
+
http<Workspace>(ws(workspaceId), { method: 'PATCH', body: { name } }),
|
|
194
|
+
|
|
195
|
+
deleteWorkspace: (workspaceId: string) => http(ws(workspaceId), { method: 'DELETE' }),
|
|
196
|
+
|
|
197
|
+
// ---- blocks -----------------------------------------------------------
|
|
198
|
+
addFrame: (workspaceId: string, body: { type: BlockType; position: Position }) =>
|
|
199
|
+
http<Block>(`${ws(workspaceId)}/blocks`, { method: 'POST', body }),
|
|
200
|
+
|
|
201
|
+
addTask: (workspaceId: string, blockId: string, body: { title?: string } = {}) =>
|
|
202
|
+
http<Block>(`${ws(workspaceId)}/blocks/${blockId}/tasks`, { method: 'POST', body }),
|
|
203
|
+
|
|
204
|
+
addModule: (
|
|
205
|
+
workspaceId: string,
|
|
206
|
+
blockId: string,
|
|
207
|
+
body: { name: string; position?: Position },
|
|
208
|
+
) => http<Block>(`${ws(workspaceId)}/blocks/${blockId}/modules`, { method: 'POST', body }),
|
|
209
|
+
|
|
210
|
+
updateBlock: (workspaceId: string, blockId: string, body: Partial<Block>) =>
|
|
211
|
+
http<Block>(`${ws(workspaceId)}/blocks/${blockId}`, { method: 'PATCH', body }),
|
|
212
|
+
|
|
213
|
+
moveBlock: (workspaceId: string, blockId: string, body: { position: Position }) =>
|
|
214
|
+
http<Block>(`${ws(workspaceId)}/blocks/${blockId}/move`, { method: 'POST', body }),
|
|
215
|
+
|
|
216
|
+
reparentBlock: (
|
|
217
|
+
workspaceId: string,
|
|
218
|
+
blockId: string,
|
|
219
|
+
body: { parentId: string; position: Position },
|
|
220
|
+
) => http<Block>(`${ws(workspaceId)}/blocks/${blockId}/reparent`, { method: 'POST', body }),
|
|
221
|
+
|
|
222
|
+
removeBlock: (workspaceId: string, blockId: string) =>
|
|
223
|
+
http(`${ws(workspaceId)}/blocks/${blockId}`, { method: 'DELETE' }),
|
|
224
|
+
|
|
225
|
+
toggleDependency: (workspaceId: string, blockId: string, body: { sourceId: string }) =>
|
|
226
|
+
http<Block>(`${ws(workspaceId)}/blocks/${blockId}/dependencies`, { method: 'POST', body }),
|
|
227
|
+
|
|
228
|
+
// ---- pipelines --------------------------------------------------------
|
|
229
|
+
listPipelines: (workspaceId: string) => http<Pipeline[]>(`${ws(workspaceId)}/pipelines`),
|
|
230
|
+
|
|
231
|
+
createPipeline: (workspaceId: string, body: { name: string; agentKinds: string[] }) =>
|
|
232
|
+
http<Pipeline>(`${ws(workspaceId)}/pipelines`, { method: 'POST', body }),
|
|
233
|
+
|
|
234
|
+
removePipeline: (workspaceId: string, pipelineId: string) =>
|
|
235
|
+
http(`${ws(workspaceId)}/pipelines/${pipelineId}`, { method: 'DELETE' }),
|
|
236
|
+
|
|
237
|
+
// ---- executions -------------------------------------------------------
|
|
238
|
+
startExecution: (workspaceId: string, blockId: string, body: { pipelineId: string }) =>
|
|
239
|
+
http<ExecutionInstance>(`${ws(workspaceId)}/blocks/${blockId}/executions`, {
|
|
240
|
+
method: 'POST',
|
|
241
|
+
body,
|
|
242
|
+
}),
|
|
243
|
+
|
|
244
|
+
cancelExecution: (workspaceId: string, blockId: string) =>
|
|
245
|
+
http<Block>(`${ws(workspaceId)}/blocks/${blockId}/executions`, { method: 'DELETE' }),
|
|
246
|
+
|
|
247
|
+
mergeBlock: (workspaceId: string, blockId: string) =>
|
|
248
|
+
http<Block>(`${ws(workspaceId)}/blocks/${blockId}/merge`, { method: 'POST' }),
|
|
249
|
+
|
|
250
|
+
resolveDecision: (
|
|
251
|
+
workspaceId: string,
|
|
252
|
+
executionId: string,
|
|
253
|
+
decisionId: string,
|
|
254
|
+
body: { choice: string },
|
|
255
|
+
) =>
|
|
256
|
+
http<ExecutionInstance>(
|
|
257
|
+
`${ws(workspaceId)}/executions/${executionId}/decisions/${decisionId}`,
|
|
258
|
+
{ method: 'POST', body },
|
|
259
|
+
),
|
|
260
|
+
|
|
261
|
+
// ---- spend safeguard --------------------------------------------------
|
|
262
|
+
resumeSpend: (workspaceId: string) =>
|
|
263
|
+
http<ExecutionInstance[]>(`${ws(workspaceId)}/spend/resume`, { method: 'POST' }),
|
|
264
|
+
|
|
265
|
+
// ---- document sources (Confluence, Notion, …) -------------------------
|
|
266
|
+
// The configured sources + their connect/import metadata. A 503 means the
|
|
267
|
+
// integration is off (the store hides its UI on any error here).
|
|
268
|
+
listDocumentSources: (workspaceId: string) =>
|
|
269
|
+
http<{ sources: DocumentSourceDescriptor[] }>(`${ws(workspaceId)}/document-sources`),
|
|
270
|
+
|
|
271
|
+
listDocumentConnections: (workspaceId: string) =>
|
|
272
|
+
http<{ connections: DocumentConnection[] }>(
|
|
273
|
+
`${ws(workspaceId)}/document-sources/connections`,
|
|
274
|
+
),
|
|
275
|
+
|
|
276
|
+
connectDocumentSource: (
|
|
277
|
+
workspaceId: string,
|
|
278
|
+
source: DocumentSourceKind,
|
|
279
|
+
credentials: Record<string, string>,
|
|
280
|
+
) =>
|
|
281
|
+
http<DocumentConnection>(`${ws(workspaceId)}/document-sources/${source}/connect`, {
|
|
282
|
+
method: 'POST',
|
|
283
|
+
body: { credentials },
|
|
284
|
+
}),
|
|
285
|
+
|
|
286
|
+
disconnectDocumentSource: (workspaceId: string, source: DocumentSourceKind) =>
|
|
287
|
+
http(`${ws(workspaceId)}/document-sources/${source}/connection`, { method: 'DELETE' }),
|
|
288
|
+
|
|
289
|
+
listDocuments: (workspaceId: string) => http<SourceDocument[]>(`${ws(workspaceId)}/documents`),
|
|
290
|
+
|
|
291
|
+
importDocument: (workspaceId: string, source: DocumentSourceKind, body: { ref: string }) =>
|
|
292
|
+
http<SourceDocument>(`${ws(workspaceId)}/document-sources/${source}/import`, {
|
|
293
|
+
method: 'POST',
|
|
294
|
+
body,
|
|
295
|
+
}),
|
|
296
|
+
|
|
297
|
+
planDocument: (workspaceId: string, source: DocumentSourceKind, externalId: string) =>
|
|
298
|
+
http<DocumentBoardPlan>(`${ws(workspaceId)}/document-sources/${source}/plan`, {
|
|
299
|
+
method: 'POST',
|
|
300
|
+
body: { externalId },
|
|
301
|
+
}),
|
|
302
|
+
|
|
303
|
+
spawnDocument: (
|
|
304
|
+
workspaceId: string,
|
|
305
|
+
source: DocumentSourceKind,
|
|
306
|
+
body: { externalId: string; frameId?: string },
|
|
307
|
+
) =>
|
|
308
|
+
http<{ plan: DocumentBoardPlan; result: SpawnResult }>(
|
|
309
|
+
`${ws(workspaceId)}/document-sources/${source}/spawn`,
|
|
310
|
+
{ method: 'POST', body },
|
|
311
|
+
),
|
|
312
|
+
|
|
313
|
+
linkDocument: (
|
|
314
|
+
workspaceId: string,
|
|
315
|
+
body: { source: DocumentSourceKind; externalId: string; blockId: string },
|
|
316
|
+
) => http<SourceDocument>(`${ws(workspaceId)}/documents/link`, { method: 'POST', body }),
|
|
317
|
+
|
|
318
|
+
// ---- task sources (Jira, …) ------------------------------------------
|
|
319
|
+
// The configured trackers + their connect/import metadata. A 503 means the
|
|
320
|
+
// integration is off (the store hides its UI on any error here).
|
|
321
|
+
listTaskSources: (workspaceId: string) =>
|
|
322
|
+
http<{ sources: TaskSourceDescriptor[] }>(`${ws(workspaceId)}/task-sources`),
|
|
323
|
+
|
|
324
|
+
listTaskConnections: (workspaceId: string) =>
|
|
325
|
+
http<{ connections: TaskConnection[] }>(`${ws(workspaceId)}/task-sources/connections`),
|
|
326
|
+
|
|
327
|
+
connectTaskSource: (
|
|
328
|
+
workspaceId: string,
|
|
329
|
+
source: TaskSourceKind,
|
|
330
|
+
credentials: Record<string, string>,
|
|
331
|
+
) =>
|
|
332
|
+
http<TaskConnection>(`${ws(workspaceId)}/task-sources/${source}/connect`, {
|
|
333
|
+
method: 'POST',
|
|
334
|
+
body: { credentials },
|
|
335
|
+
}),
|
|
336
|
+
|
|
337
|
+
disconnectTaskSource: (workspaceId: string, source: TaskSourceKind) =>
|
|
338
|
+
http(`${ws(workspaceId)}/task-sources/${source}/connection`, { method: 'DELETE' }),
|
|
339
|
+
|
|
340
|
+
listTasks: (workspaceId: string) => http<SourceTask[]>(`${ws(workspaceId)}/tasks`),
|
|
341
|
+
|
|
342
|
+
importTask: (workspaceId: string, source: TaskSourceKind, body: { ref: string }) =>
|
|
343
|
+
http<SourceTask>(`${ws(workspaceId)}/task-sources/${source}/import`, {
|
|
344
|
+
method: 'POST',
|
|
345
|
+
body,
|
|
346
|
+
}),
|
|
347
|
+
|
|
348
|
+
linkTask: (
|
|
349
|
+
workspaceId: string,
|
|
350
|
+
body: { source: TaskSourceKind; externalId: string; blockId: string },
|
|
351
|
+
) => http<SourceTask>(`${ws(workspaceId)}/tasks/link`, { method: 'POST', body }),
|
|
352
|
+
|
|
353
|
+
// ---- requirements review (stateless reviewer agent) ------------------
|
|
354
|
+
// The current review for a block (null when none has been run). A 503 means
|
|
355
|
+
// the feature is unconfigured (the panel hides on any error here).
|
|
356
|
+
getRequirementReview: (workspaceId: string, blockId: string) =>
|
|
357
|
+
http<RequirementReview | null>(
|
|
358
|
+
`${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/requirement-review`,
|
|
359
|
+
),
|
|
360
|
+
|
|
361
|
+
// Run a fresh review (synchronous — the LLM runs inline and returns the items).
|
|
362
|
+
reviewRequirements: (workspaceId: string, blockId: string) =>
|
|
363
|
+
http<RequirementReview>(
|
|
364
|
+
`${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/requirement-review`,
|
|
365
|
+
{ method: 'POST' },
|
|
366
|
+
),
|
|
367
|
+
|
|
368
|
+
replyRequirementItem: (workspaceId: string, reviewId: string, itemId: string, reply: string) =>
|
|
369
|
+
http<RequirementReview>(
|
|
370
|
+
`${ws(workspaceId)}/requirement-reviews/${encodeURIComponent(reviewId)}/items/${encodeURIComponent(itemId)}/reply`,
|
|
371
|
+
{ method: 'POST', body: { reply } },
|
|
372
|
+
),
|
|
373
|
+
|
|
374
|
+
setRequirementItemStatus: (
|
|
375
|
+
workspaceId: string,
|
|
376
|
+
reviewId: string,
|
|
377
|
+
itemId: string,
|
|
378
|
+
status: ReviewItemStatus,
|
|
379
|
+
) =>
|
|
380
|
+
http<RequirementReview>(
|
|
381
|
+
`${ws(workspaceId)}/requirement-reviews/${encodeURIComponent(reviewId)}/items/${encodeURIComponent(itemId)}`,
|
|
382
|
+
{ method: 'PATCH', body: { status } },
|
|
383
|
+
),
|
|
384
|
+
|
|
385
|
+
// Fold the answers back into the block's requirements (all items must be settled).
|
|
386
|
+
incorporateRequirements: (workspaceId: string, reviewId: string) =>
|
|
387
|
+
http<{ review: RequirementReview; block: Block }>(
|
|
388
|
+
`${ws(workspaceId)}/requirement-reviews/${encodeURIComponent(reviewId)}/incorporate`,
|
|
389
|
+
{ method: 'POST' },
|
|
390
|
+
),
|
|
391
|
+
|
|
392
|
+
// ---- github integration ----------------------------------------------
|
|
393
|
+
// Connection management, projection reads (served from D1 — fast and
|
|
394
|
+
// rate-limit-free) and repo writes. A 503 from `getGitHubConnection` means
|
|
395
|
+
// the integration is off (the store hides its UI on any error there).
|
|
396
|
+
getGitHubInstallUrl: (workspaceId: string) =>
|
|
397
|
+
http<{ url: string }>(`${ws(workspaceId)}/github/install-url`),
|
|
398
|
+
|
|
399
|
+
getGitHubConnection: (workspaceId: string) =>
|
|
400
|
+
http<{ connection: GitHubConnection | null }>(`${ws(workspaceId)}/github/connection`),
|
|
401
|
+
|
|
402
|
+
listGitHubInstallations: (workspaceId: string) =>
|
|
403
|
+
http<{ installations: GitHubInstallationOption[] }>(
|
|
404
|
+
`${ws(workspaceId)}/github/installations`,
|
|
405
|
+
),
|
|
406
|
+
|
|
407
|
+
connectGitHub: (workspaceId: string, installationId: number) =>
|
|
408
|
+
http<GitHubConnection>(`${ws(workspaceId)}/github/connect`, {
|
|
409
|
+
method: 'POST',
|
|
410
|
+
body: { installationId },
|
|
411
|
+
}),
|
|
412
|
+
|
|
413
|
+
disconnectGitHub: (workspaceId: string) =>
|
|
414
|
+
http(`${ws(workspaceId)}/github/connection`, { method: 'DELETE' }),
|
|
415
|
+
|
|
416
|
+
resyncGitHub: (workspaceId: string, body: ResyncRequest = {}) =>
|
|
417
|
+
http<{ status: string }>(`${ws(workspaceId)}/github/resync`, { method: 'POST', body }),
|
|
418
|
+
|
|
419
|
+
listGitHubRepos: (workspaceId: string) => http<GitHubRepo[]>(`${ws(workspaceId)}/github/repos`),
|
|
420
|
+
|
|
421
|
+
// Programmatic repo creation (privileged App tier). Only called when the
|
|
422
|
+
// connection reports `canCreateRepos`; otherwise the UI opens GitHub directly.
|
|
423
|
+
createGitHubRepo: (workspaceId: string, body: CreateRepoRequest) =>
|
|
424
|
+
http<CreatedRepo>(`${ws(workspaceId)}/github/repos`, { method: 'POST', body }),
|
|
425
|
+
|
|
426
|
+
// Repos the connected installation can access, annotated with whether this
|
|
427
|
+
// workspace links each (drives the per-workspace repo picker).
|
|
428
|
+
listGitHubAvailableRepos: (workspaceId: string) =>
|
|
429
|
+
http<GitHubAvailableRepo[]>(`${ws(workspaceId)}/github/available-repos`),
|
|
430
|
+
|
|
431
|
+
// Set the exact set of repos this workspace links.
|
|
432
|
+
setGitHubLinkedRepos: (workspaceId: string, repoGithubIds: number[]) =>
|
|
433
|
+
http<GitHubRepo[]>(`${ws(workspaceId)}/github/repos`, {
|
|
434
|
+
method: 'PUT',
|
|
435
|
+
body: { repoGithubIds },
|
|
436
|
+
}),
|
|
437
|
+
|
|
438
|
+
listGitHubBranches: (workspaceId: string, repoGithubId: number) =>
|
|
439
|
+
http<GitHubBranch[]>(`${ws(workspaceId)}/github/repos/${repoGithubId}/branches`),
|
|
440
|
+
|
|
441
|
+
listGitHubPullRequests: (workspaceId: string) =>
|
|
442
|
+
http<GitHubPullRequest[]>(`${ws(workspaceId)}/github/pulls`),
|
|
443
|
+
|
|
444
|
+
listGitHubIssues: (workspaceId: string) =>
|
|
445
|
+
http<GitHubIssue[]>(`${ws(workspaceId)}/github/issues`),
|
|
446
|
+
|
|
447
|
+
createGitHubBranch: (workspaceId: string, repoGithubId: number, body: CreateBranchInput) =>
|
|
448
|
+
http<GitHubBranch>(`${ws(workspaceId)}/github/repos/${repoGithubId}/branches`, {
|
|
449
|
+
method: 'POST',
|
|
450
|
+
body,
|
|
451
|
+
}),
|
|
452
|
+
|
|
453
|
+
commitGitHubFiles: (workspaceId: string, repoGithubId: number, body: CommitFilesInput) =>
|
|
454
|
+
http<{ sha: string }>(`${ws(workspaceId)}/github/repos/${repoGithubId}/commits`, {
|
|
455
|
+
method: 'POST',
|
|
456
|
+
body,
|
|
457
|
+
}),
|
|
458
|
+
|
|
459
|
+
openGitHubPullRequest: (
|
|
460
|
+
workspaceId: string,
|
|
461
|
+
repoGithubId: number,
|
|
462
|
+
body: OpenPullRequestInput,
|
|
463
|
+
) =>
|
|
464
|
+
http<GitHubPullRequest>(`${ws(workspaceId)}/github/repos/${repoGithubId}/pulls`, {
|
|
465
|
+
method: 'POST',
|
|
466
|
+
body,
|
|
467
|
+
}),
|
|
468
|
+
|
|
469
|
+
mergeGitHubPullRequest: (
|
|
470
|
+
workspaceId: string,
|
|
471
|
+
repoGithubId: number,
|
|
472
|
+
number: number,
|
|
473
|
+
body: MergePullRequestInput = {},
|
|
474
|
+
) =>
|
|
475
|
+
http(`${ws(workspaceId)}/github/repos/${repoGithubId}/pulls/${number}/merge`, {
|
|
476
|
+
method: 'PUT',
|
|
477
|
+
body,
|
|
478
|
+
}),
|
|
479
|
+
|
|
480
|
+
commentGitHubIssue: (
|
|
481
|
+
workspaceId: string,
|
|
482
|
+
repoGithubId: number,
|
|
483
|
+
number: number,
|
|
484
|
+
bodyText: string,
|
|
485
|
+
) =>
|
|
486
|
+
http(`${ws(workspaceId)}/github/repos/${repoGithubId}/issues/${number}/comments`, {
|
|
487
|
+
method: 'POST',
|
|
488
|
+
body: { body: bodyText },
|
|
489
|
+
}),
|
|
490
|
+
|
|
491
|
+
// ---- repo bootstrap ---------------------------------------------------
|
|
492
|
+
listReferenceArchitectures: (workspaceId: string) =>
|
|
493
|
+
http<ReferenceArchitecture[]>(`${ws(workspaceId)}/bootstrap/reference-architectures`),
|
|
494
|
+
|
|
495
|
+
createReferenceArchitecture: (workspaceId: string, body: CreateReferenceArchitectureInput) =>
|
|
496
|
+
http<ReferenceArchitecture>(`${ws(workspaceId)}/bootstrap/reference-architectures`, {
|
|
497
|
+
method: 'POST',
|
|
498
|
+
body,
|
|
499
|
+
}),
|
|
500
|
+
|
|
501
|
+
updateReferenceArchitecture: (
|
|
502
|
+
workspaceId: string,
|
|
503
|
+
id: string,
|
|
504
|
+
body: UpdateReferenceArchitectureInput,
|
|
505
|
+
) =>
|
|
506
|
+
http<ReferenceArchitecture>(`${ws(workspaceId)}/bootstrap/reference-architectures/${id}`, {
|
|
507
|
+
method: 'PATCH',
|
|
508
|
+
body,
|
|
509
|
+
}),
|
|
510
|
+
|
|
511
|
+
deleteReferenceArchitecture: (workspaceId: string, id: string) =>
|
|
512
|
+
http(`${ws(workspaceId)}/bootstrap/reference-architectures/${id}`, { method: 'DELETE' }),
|
|
513
|
+
|
|
514
|
+
bootstrapRepo: (workspaceId: string, body: BootstrapRepoInput) =>
|
|
515
|
+
http<BootstrapJob>(`${ws(workspaceId)}/bootstrap/jobs`, { method: 'POST', body }),
|
|
516
|
+
|
|
517
|
+
// ---- agent runs (unified failure + retry) -----------------------------
|
|
518
|
+
// Retry any failed run (bootstrap or execution); the backend resolves the
|
|
519
|
+
// kind from the unified `agent_runs` table and re-drives the right flow.
|
|
520
|
+
retryAgentRun: (workspaceId: string, runId: string) =>
|
|
521
|
+
http<{ kind: AgentRunKind; run: ExecutionInstance | BootstrapJob }>(
|
|
522
|
+
`${ws(workspaceId)}/agent-runs/${encodeURIComponent(runId)}/retry`,
|
|
523
|
+
{ method: 'POST' },
|
|
524
|
+
),
|
|
525
|
+
|
|
526
|
+
// Explicitly stop a running run (bootstrap or execution): the backend kills the
|
|
527
|
+
// per-run container and tears down the durable driver, then marks the run
|
|
528
|
+
// terminally cancelled so the board stops showing it as running.
|
|
529
|
+
stopAgentRun: (workspaceId: string, runId: string) =>
|
|
530
|
+
http<{ kind: AgentRunKind; run: ExecutionInstance | BootstrapJob }>(
|
|
531
|
+
`${ws(workspaceId)}/agent-runs/${encodeURIComponent(runId)}/stop`,
|
|
532
|
+
{ method: 'POST' },
|
|
533
|
+
),
|
|
534
|
+
}
|
|
535
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
import type { Block } from '~/types/domain'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pointer-driven dragging for blocks positioned inside a container's 2D canvas
|
|
6
|
+
* (tasks inside services/modules, modules inside services). Movement is divided
|
|
7
|
+
* by the board zoom so the block tracks the cursor. When `reparent` is set, the
|
|
8
|
+
* drop point is hit-tested against `[data-drop-zone]` ancestors so a task can be
|
|
9
|
+
* dragged from a service into a module (or back out).
|
|
10
|
+
*/
|
|
11
|
+
export function useBlockDrag() {
|
|
12
|
+
const board = useBoardStore()
|
|
13
|
+
const ui = useUiStore()
|
|
14
|
+
const draggingId = ref<string | null>(null)
|
|
15
|
+
|
|
16
|
+
function startDrag(
|
|
17
|
+
block: Block,
|
|
18
|
+
e: PointerEvent,
|
|
19
|
+
opts: { reparent?: boolean; clamp?: boolean } = {},
|
|
20
|
+
) {
|
|
21
|
+
if (e.button !== 0) return
|
|
22
|
+
e.preventDefault()
|
|
23
|
+
e.stopPropagation()
|
|
24
|
+
const startX = e.clientX
|
|
25
|
+
const startY = e.clientY
|
|
26
|
+
const orig = { ...block.position }
|
|
27
|
+
// Container-local blocks (tasks/modules) are clamped to their parent's origin;
|
|
28
|
+
// frames live in free-floating flow space, so they opt out via `clamp: false`.
|
|
29
|
+
const clamp = opts.clamp ?? true
|
|
30
|
+
draggingId.value = block.id
|
|
31
|
+
|
|
32
|
+
const onMove = (ev: PointerEvent) => {
|
|
33
|
+
const z = ui.zoom || 1
|
|
34
|
+
const nx = orig.x + (ev.clientX - startX) / z
|
|
35
|
+
const ny = orig.y + (ev.clientY - startY) / z
|
|
36
|
+
board.moveBlock(block.id, {
|
|
37
|
+
x: clamp ? Math.max(0, nx) : nx,
|
|
38
|
+
y: clamp ? Math.max(0, ny) : ny,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
const onUp = (ev: PointerEvent) => {
|
|
42
|
+
window.removeEventListener('pointermove', onMove)
|
|
43
|
+
window.removeEventListener('pointerup', onUp)
|
|
44
|
+
if (opts.reparent) reparentAt(block, ev.clientX, ev.clientY)
|
|
45
|
+
draggingId.value = null
|
|
46
|
+
}
|
|
47
|
+
window.addEventListener('pointermove', onMove)
|
|
48
|
+
window.addEventListener('pointerup', onUp)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function reparentAt(block: Block, clientX: number, clientY: number) {
|
|
52
|
+
const el = document.querySelector(`[data-block-id="${block.id}"]`) as HTMLElement | null
|
|
53
|
+
if (!el) return
|
|
54
|
+
// hide the dragged element so elementFromPoint sees the zone beneath it
|
|
55
|
+
const prev = el.style.pointerEvents
|
|
56
|
+
el.style.pointerEvents = 'none'
|
|
57
|
+
const under = document.elementFromPoint(clientX, clientY) as HTMLElement | null
|
|
58
|
+
const zoneEl = under?.closest('[data-drop-zone]') as HTMLElement | null
|
|
59
|
+
el.style.pointerEvents = prev
|
|
60
|
+
if (!zoneEl) return
|
|
61
|
+
|
|
62
|
+
const newParent = zoneEl.getAttribute('data-drop-zone')!
|
|
63
|
+
if (newParent === block.parentId) return // same container — keep the new position
|
|
64
|
+
|
|
65
|
+
const z = ui.zoom || 1
|
|
66
|
+
const zr = zoneEl.getBoundingClientRect()
|
|
67
|
+
const er = el.getBoundingClientRect()
|
|
68
|
+
board.reparentBlock(block.id, newParent, {
|
|
69
|
+
x: Math.max(0, (er.left - zr.left) / z),
|
|
70
|
+
y: Math.max(0, (er.top - zr.top) / z),
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { draggingId, startDrag }
|
|
75
|
+
}
|