@cat-factory/app 0.153.3 → 0.154.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/app/components/settings/WorkspaceSettingsPanel.vue +19 -0
- package/app/composables/useRunDeepLink.spec.ts +51 -0
- package/app/composables/useRunDeepLink.ts +91 -0
- package/app/pages/index.vue +4 -0
- package/app/stores/auth/mothership.ts +108 -0
- package/app/stores/auth/session.ts +118 -0
- package/app/stores/auth.ts +17 -167
- package/app/stores/board/mutations.ts +6 -191
- package/app/stores/board/placement.ts +203 -0
- package/app/stores/board.ts +5 -1
- package/app/stores/execution/commands.ts +197 -0
- package/app/stores/execution.ts +12 -171
- package/app/stores/github/connection.ts +60 -0
- package/app/stores/github/context.ts +46 -0
- package/app/stores/github/repoActions.ts +151 -0
- package/app/stores/github.ts +30 -183
- package/app/stores/initiative/curation.ts +89 -0
- package/app/stores/initiative/planning.ts +121 -0
- package/app/stores/initiative.ts +14 -175
- package/app/stores/workspace/hydrate.ts +105 -0
- package/app/stores/workspace.ts +6 -94
- package/app/stores/workspaceSettings.ts +1 -0
- package/i18n/locales/de.json +5 -0
- package/i18n/locales/en.json +5 -0
- package/i18n/locales/es.json +5 -0
- package/i18n/locales/fr.json +5 -0
- package/i18n/locales/he.json +5 -0
- package/i18n/locales/it.json +5 -0
- package/i18n/locales/ja.json +5 -0
- package/i18n/locales/pl.json +5 -0
- package/i18n/locales/tr.json +5 -0
- package/i18n/locales/uk.json +5 -0
- package/package.json +2 -2
package/app/stores/github.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { computed, ref } from 'vue'
|
|
3
3
|
import type {
|
|
4
|
-
CreateBranchInput,
|
|
5
4
|
GitHubAvailableRepo,
|
|
6
5
|
GitHubBranch,
|
|
7
6
|
GitHubConnection,
|
|
@@ -9,18 +8,15 @@ import type {
|
|
|
9
8
|
GitHubIssue,
|
|
10
9
|
GitHubPullRequest,
|
|
11
10
|
GitHubRepo,
|
|
12
|
-
MergePullRequestInput,
|
|
13
|
-
OpenPullRequestInput,
|
|
14
11
|
RepoTreeEntry,
|
|
15
|
-
ResyncRequest,
|
|
16
12
|
} from '~/types/domain'
|
|
17
13
|
import { useSingleFlightProbe } from '~/composables/useSingleFlightProbe'
|
|
18
14
|
import { useUpsertList } from '~/composables/useUpsertList'
|
|
19
15
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
20
16
|
import { useServicesStore } from '~/stores/services'
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
import { pullKey, type GitHubStoreContext } from '~/stores/github/context'
|
|
18
|
+
import { createGitHubConnectionActions } from '~/stores/github/connection'
|
|
19
|
+
import { createGitHubRepoActions } from '~/stores/github/repoActions'
|
|
24
20
|
|
|
25
21
|
/**
|
|
26
22
|
* GitHub integration state: the workspace's App installation, the projected
|
|
@@ -155,170 +151,35 @@ export const useGitHubStore = defineStore('github', () => {
|
|
|
155
151
|
if (connected.value && repos.value.length === 0) await load()
|
|
156
152
|
}
|
|
157
153
|
|
|
158
|
-
/**
|
|
159
|
-
* Load the repos the installation can access, with this workspace's link state.
|
|
160
|
-
* With a `q` the backend filters `owner/name` server-side (the add-service picker
|
|
161
|
-
* searches instead of prefetching a huge installation); without one it browses all
|
|
162
|
-
* (the repo-link panel). A blank/short `q` clears the list rather than fetching.
|
|
163
|
-
*/
|
|
164
|
-
async function loadAvailableRepos(q?: string) {
|
|
165
|
-
if (!connected.value) return
|
|
166
|
-
if (q !== undefined && q.trim() === '') {
|
|
167
|
-
availableRepos.value = []
|
|
168
|
-
return
|
|
169
|
-
}
|
|
170
|
-
loadingAvailable.value = true
|
|
171
|
-
try {
|
|
172
|
-
availableRepos.value = await api.listGitHubAvailableRepos(workspace.requireId(), q)
|
|
173
|
-
} finally {
|
|
174
|
-
loadingAvailable.value = false
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Search the installation/PAT-accessible repos server-side WITHOUT touching the shared
|
|
180
|
-
* `availableRepos`/`loadingAvailable` singleton — it returns the matches to the caller instead.
|
|
181
|
-
* This is the reusable form behind {@link useRepoSearch}: two independent pickers (the
|
|
182
|
-
* add-service modal and the doc-task reference-repo picker) can search concurrently without
|
|
183
|
-
* clobbering each other's results. A blank/short `q` (or no connection) returns `[]`.
|
|
184
|
-
*/
|
|
185
|
-
async function searchAvailableRepos(q: string): Promise<GitHubAvailableRepo[]> {
|
|
186
|
-
if (!connected.value || q.trim() === '') return []
|
|
187
|
-
return api.listGitHubAvailableRepos(workspace.requireId(), q)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** Set the exact set of repos this workspace links, then refresh projections. */
|
|
191
|
-
async function setLinkedRepos(repoGithubIds: number[]) {
|
|
192
|
-
savingRepos.value = true
|
|
193
|
-
try {
|
|
194
|
-
repos.value = await api.setGitHubLinkedRepos(workspace.requireId(), repoGithubIds)
|
|
195
|
-
// Reflect the new link state in the picker and refresh PRs/issues.
|
|
196
|
-
const linked = new Set(repoGithubIds)
|
|
197
|
-
availableRepos.value = availableRepos.value.map((r) => ({
|
|
198
|
-
...r,
|
|
199
|
-
linked: linked.has(r.githubId),
|
|
200
|
-
}))
|
|
201
|
-
await load()
|
|
202
|
-
} finally {
|
|
203
|
-
savingRepos.value = false
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/** Lazily load (and cache) the branches for a single repo. */
|
|
208
|
-
async function loadBranches(repoGithubId: number): Promise<GitHubBranch[]> {
|
|
209
|
-
const list = await api.listGitHubBranches(workspace.requireId(), repoGithubId)
|
|
210
|
-
branches.value = { ...branches.value, [repoGithubId]: list }
|
|
211
|
-
return list
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/** List one level of a (monorepo) repo's tree, for the service-directory picker. */
|
|
215
|
-
function loadRepoTree(repoGithubId: number, path = '') {
|
|
216
|
-
return api.listGitHubRepoTree(workspace.requireId(), repoGithubId, path)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
154
|
/** Full file listing per repo (recursive tree), cached by GitHub numeric id. */
|
|
220
155
|
const repoFiles = ref<Record<number, RepoTreeEntry[]>>({})
|
|
221
156
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
async function loadInstallations() {
|
|
246
|
-
loadingInstallations.value = true
|
|
247
|
-
try {
|
|
248
|
-
const { installations: list } = await api.listGitHubInstallations(workspace.requireId())
|
|
249
|
-
installations.value = list
|
|
250
|
-
} finally {
|
|
251
|
-
loadingInstallations.value = false
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/** Programmatic bind by installation id (the browser flow uses the redirect). */
|
|
256
|
-
async function connect(installationId: number) {
|
|
257
|
-
connection.value = await api.connectGitHub(workspace.requireId(), installationId)
|
|
258
|
-
available.value = true
|
|
259
|
-
await load()
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async function disconnect() {
|
|
263
|
-
await api.disconnectGitHub(workspace.requireId())
|
|
264
|
-
connection.value = null
|
|
265
|
-
repos.value = []
|
|
266
|
-
availableRepos.value = []
|
|
267
|
-
pulls.value = []
|
|
268
|
-
issues.value = []
|
|
269
|
-
branches.value = {}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/** Trigger a resync, then refresh projections (no-op for queued/backfill). */
|
|
273
|
-
async function resync(body: ResyncRequest = {}) {
|
|
274
|
-
syncing.value = true
|
|
275
|
-
try {
|
|
276
|
-
const res = await api.resyncGitHub(workspace.requireId(), body)
|
|
277
|
-
await load()
|
|
278
|
-
return res
|
|
279
|
-
} finally {
|
|
280
|
-
syncing.value = false
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// ---- repo writes ----------------------------------------------------------
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Create a repository under the connected account (privileged App tier). Only
|
|
288
|
-
* meaningful when `canCreateRepos`; the backend 409s otherwise. Returns the
|
|
289
|
-
* created repo so the caller can confirm/link it.
|
|
290
|
-
*/
|
|
291
|
-
function createRepo(input: Parameters<typeof api.createGitHubRepo>[1]) {
|
|
292
|
-
return api.createGitHubRepo(workspace.requireId(), input)
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
async function createBranch(repoGithubId: number, input: CreateBranchInput) {
|
|
296
|
-
const branch = await api.createGitHubBranch(workspace.requireId(), repoGithubId, input)
|
|
297
|
-
const next = branches.value[repoGithubId] ?? []
|
|
298
|
-
branches.value = { ...branches.value, [repoGithubId]: [branch, ...next] }
|
|
299
|
-
return branch
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
async function openPullRequest(repoGithubId: number, input: OpenPullRequestInput) {
|
|
303
|
-
const pr = await api.openGitHubPullRequest(workspace.requireId(), repoGithubId, input)
|
|
304
|
-
upsertPull(pr)
|
|
305
|
-
return pr
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
async function mergePullRequest(
|
|
309
|
-
repoGithubId: number,
|
|
310
|
-
number: number,
|
|
311
|
-
input: MergePullRequestInput = {},
|
|
312
|
-
) {
|
|
313
|
-
await api.mergeGitHubPullRequest(workspace.requireId(), repoGithubId, number, input)
|
|
314
|
-
// Optimistically reflect the merge until the next sync confirms it.
|
|
315
|
-
const existing = getPull(pullKey(repoGithubId, number))
|
|
316
|
-
if (existing) upsertPull({ ...existing, state: 'closed', merged: true })
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function comment(repoGithubId: number, number: number, body: string) {
|
|
320
|
-
return api.commentGitHubIssue(workspace.requireId(), repoGithubId, number, body)
|
|
157
|
+
// The installation lifecycle + the per-repo reads/writes, split into cohesive factories
|
|
158
|
+
// sharing the state above (a size-only extraction mirroring `stores/board/` — behaviour is
|
|
159
|
+
// identical to the former in-closure functions).
|
|
160
|
+
const context: GitHubStoreContext = {
|
|
161
|
+
api,
|
|
162
|
+
workspace,
|
|
163
|
+
available,
|
|
164
|
+
connection,
|
|
165
|
+
installations,
|
|
166
|
+
loadingInstallations,
|
|
167
|
+
repos,
|
|
168
|
+
availableRepos,
|
|
169
|
+
loadingAvailable,
|
|
170
|
+
savingRepos,
|
|
171
|
+
pulls,
|
|
172
|
+
upsertPull,
|
|
173
|
+
getPull,
|
|
174
|
+
issues,
|
|
175
|
+
branches,
|
|
176
|
+
repoFiles,
|
|
177
|
+
syncing,
|
|
178
|
+
connected,
|
|
179
|
+
load,
|
|
321
180
|
}
|
|
181
|
+
const connectionActions = createGitHubConnectionActions(context)
|
|
182
|
+
const repoActions = createGitHubRepoActions(context)
|
|
322
183
|
|
|
323
184
|
/**
|
|
324
185
|
* Drop the per-workspace projection + connection state (called on workspace switch)
|
|
@@ -365,23 +226,9 @@ export const useGitHubStore = defineStore('github', () => {
|
|
|
365
226
|
ensureProbed,
|
|
366
227
|
load,
|
|
367
228
|
ensureLoaded,
|
|
368
|
-
loadAvailableRepos,
|
|
369
|
-
searchAvailableRepos,
|
|
370
|
-
setLinkedRepos,
|
|
371
|
-
loadRepoTree,
|
|
372
229
|
repoFiles,
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
getInstallUrl,
|
|
376
|
-
loadInstallations,
|
|
377
|
-
connect,
|
|
378
|
-
disconnect,
|
|
379
|
-
resync,
|
|
380
|
-
createRepo,
|
|
381
|
-
createBranch,
|
|
382
|
-
openPullRequest,
|
|
383
|
-
mergePullRequest,
|
|
384
|
-
comment,
|
|
230
|
+
...connectionActions,
|
|
231
|
+
...repoActions,
|
|
385
232
|
reset,
|
|
386
233
|
}
|
|
387
234
|
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
import type {
|
|
3
|
+
InitiativeExecutionPolicy,
|
|
4
|
+
PromoteInitiativeFollowUpInput,
|
|
5
|
+
UpdateInitiativeItemInput,
|
|
6
|
+
} from '~/types/domain'
|
|
7
|
+
import type { InitiativeActionContext } from './planning'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The tracker-curation actions: promoting / dismissing a harvested follow-up, editing one
|
|
11
|
+
* tracker item (or driving its status), and replacing the execution policy. Every one runs
|
|
12
|
+
* through the shared `curate` wrapper so the window has a single in-flight flag.
|
|
13
|
+
*/
|
|
14
|
+
export function createInitiativeCurationActions(ctx: InitiativeActionContext) {
|
|
15
|
+
const { api, workspace, upsert } = ctx
|
|
16
|
+
|
|
17
|
+
/** True while a curation action (promote/dismiss/edit item/edit policy) is in flight. */
|
|
18
|
+
const curating = ref(false)
|
|
19
|
+
|
|
20
|
+
async function curate<T>(fn: () => Promise<T>): Promise<T> {
|
|
21
|
+
if (!workspace.workspaceId) throw new Error('No active workspace')
|
|
22
|
+
curating.value = true
|
|
23
|
+
try {
|
|
24
|
+
return await fn()
|
|
25
|
+
} finally {
|
|
26
|
+
curating.value = false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Promote an `open` harvested follow-up into a new pending tracker item. */
|
|
31
|
+
async function promoteFollowUp(
|
|
32
|
+
initiativeId: string,
|
|
33
|
+
followUpId: string,
|
|
34
|
+
input: PromoteInitiativeFollowUpInput,
|
|
35
|
+
) {
|
|
36
|
+
return curate(async () => {
|
|
37
|
+
const updated = await api.promoteInitiativeFollowUp(
|
|
38
|
+
workspace.workspaceId!,
|
|
39
|
+
initiativeId,
|
|
40
|
+
followUpId,
|
|
41
|
+
input,
|
|
42
|
+
)
|
|
43
|
+
upsert(updated)
|
|
44
|
+
return updated
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Dismiss a harvested follow-up. */
|
|
49
|
+
async function dismissFollowUp(initiativeId: string, followUpId: string) {
|
|
50
|
+
return curate(async () => {
|
|
51
|
+
const updated = await api.dismissInitiativeFollowUp(
|
|
52
|
+
workspace.workspaceId!,
|
|
53
|
+
initiativeId,
|
|
54
|
+
followUpId,
|
|
55
|
+
)
|
|
56
|
+
upsert(updated)
|
|
57
|
+
return updated
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Edit one tracker item and/or drive its status (retry a blocked item / skip it). */
|
|
62
|
+
async function updateItem(
|
|
63
|
+
initiativeId: string,
|
|
64
|
+
itemId: string,
|
|
65
|
+
input: UpdateInitiativeItemInput,
|
|
66
|
+
) {
|
|
67
|
+
return curate(async () => {
|
|
68
|
+
const updated = await api.updateInitiativeItem(
|
|
69
|
+
workspace.workspaceId!,
|
|
70
|
+
initiativeId,
|
|
71
|
+
itemId,
|
|
72
|
+
input,
|
|
73
|
+
)
|
|
74
|
+
upsert(updated)
|
|
75
|
+
return updated
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Replace the execution policy (concurrency + pipeline rules). */
|
|
80
|
+
async function updatePolicy(initiativeId: string, policy: InitiativeExecutionPolicy) {
|
|
81
|
+
return curate(async () => {
|
|
82
|
+
const updated = await api.updateInitiativePolicy(workspace.workspaceId!, initiativeId, policy)
|
|
83
|
+
upsert(updated)
|
|
84
|
+
return updated
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { curating, promoteFollowUp, dismissFollowUp, updateItem, updatePolicy }
|
|
89
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Ref } from 'vue'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import type { Initiative } from '~/types/domain'
|
|
4
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shared reactive state + injected dependencies the initiatives-store action factories close
|
|
8
|
+
* over. Created once in the `initiatives` store setup and threaded into
|
|
9
|
+
* {@link createInitiativePlanningActions} / {@link createInitiativeCurationActions} so the split
|
|
10
|
+
* operations stay behaviourally identical to the original single-closure store — a size-only
|
|
11
|
+
* extraction mirroring `stores/board/` and `stores/pipelines/`, not a new seam.
|
|
12
|
+
*/
|
|
13
|
+
export interface InitiativeActionContext {
|
|
14
|
+
api: ReturnType<typeof useApi>
|
|
15
|
+
workspace: ReturnType<typeof useWorkspaceStore>
|
|
16
|
+
/** Patch an entity a call returned into the by-block cache (newest rev wins). */
|
|
17
|
+
upsert: (initiative: Initiative) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The planning-window actions: answering / dismissing the interviewer's questions, asking it to
|
|
22
|
+
* recommend an answer, and the two loop-resuming controls (continue / proceed). Owns the
|
|
23
|
+
* in-flight flags the window renders its spinners from.
|
|
24
|
+
*/
|
|
25
|
+
export function createInitiativePlanningActions(ctx: InitiativeActionContext) {
|
|
26
|
+
const { api, workspace, upsert } = ctx
|
|
27
|
+
|
|
28
|
+
/** True while a planning-window action (continue/proceed) is resuming the run. */
|
|
29
|
+
const resuming = ref(false)
|
|
30
|
+
/** Question ids the interviewer is currently drafting a recommendation for (window spinner). */
|
|
31
|
+
const recommending: Ref<Set<string>> = ref(new Set())
|
|
32
|
+
|
|
33
|
+
/** Record the human's answer to one pending interview question (no run resume). */
|
|
34
|
+
async function answerQuestion(blockId: string, questionId: string, answer: string) {
|
|
35
|
+
if (!workspace.workspaceId) throw new Error('No active workspace')
|
|
36
|
+
const updated = await api.answerInitiativeQuestion(
|
|
37
|
+
workspace.workspaceId,
|
|
38
|
+
blockId,
|
|
39
|
+
questionId,
|
|
40
|
+
answer,
|
|
41
|
+
)
|
|
42
|
+
upsert(updated)
|
|
43
|
+
return updated
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Mark a planning question not-relevant (`dismissed`) or reopen it (no run resume). */
|
|
47
|
+
async function setQuestionStatus(
|
|
48
|
+
blockId: string,
|
|
49
|
+
questionId: string,
|
|
50
|
+
status: 'open' | 'dismissed',
|
|
51
|
+
) {
|
|
52
|
+
if (!workspace.workspaceId) throw new Error('No active workspace')
|
|
53
|
+
const updated = await api.setInitiativeQuestionStatus(
|
|
54
|
+
workspace.workspaceId,
|
|
55
|
+
blockId,
|
|
56
|
+
questionId,
|
|
57
|
+
status,
|
|
58
|
+
)
|
|
59
|
+
upsert(updated)
|
|
60
|
+
return updated
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Ask the interviewer to recommend a suggested answer for one pending question. Runs the
|
|
65
|
+
* interviewer LLM inline server-side; the returned entity carries the suggestion on the question.
|
|
66
|
+
* Tracks the in-flight id so the window can show a per-question spinner.
|
|
67
|
+
*/
|
|
68
|
+
async function recommendAnswer(blockId: string, questionId: string) {
|
|
69
|
+
if (!workspace.workspaceId) throw new Error('No active workspace')
|
|
70
|
+
recommending.value = new Set(recommending.value).add(questionId)
|
|
71
|
+
try {
|
|
72
|
+
const updated = await api.recommendInitiativeAnswer(
|
|
73
|
+
workspace.workspaceId,
|
|
74
|
+
blockId,
|
|
75
|
+
questionId,
|
|
76
|
+
)
|
|
77
|
+
upsert(updated)
|
|
78
|
+
return updated
|
|
79
|
+
} finally {
|
|
80
|
+
const next = new Set(recommending.value)
|
|
81
|
+
next.delete(questionId)
|
|
82
|
+
recommending.value = next
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Submit the answers and resume the interview (the interviewer re-runs, may ask more). */
|
|
87
|
+
async function continuePlanning(blockId: string) {
|
|
88
|
+
if (!workspace.workspaceId) throw new Error('No active workspace')
|
|
89
|
+
resuming.value = true
|
|
90
|
+
try {
|
|
91
|
+
const updated = await api.continueInitiativePlanning(workspace.workspaceId, blockId)
|
|
92
|
+
upsert(updated)
|
|
93
|
+
return updated
|
|
94
|
+
} finally {
|
|
95
|
+
resuming.value = false
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Skip remaining questions: the interviewer converges and the run advances. */
|
|
100
|
+
async function proceedPlanning(blockId: string) {
|
|
101
|
+
if (!workspace.workspaceId) throw new Error('No active workspace')
|
|
102
|
+
resuming.value = true
|
|
103
|
+
try {
|
|
104
|
+
const updated = await api.proceedInitiativePlanning(workspace.workspaceId, blockId)
|
|
105
|
+
upsert(updated)
|
|
106
|
+
return updated
|
|
107
|
+
} finally {
|
|
108
|
+
resuming.value = false
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
resuming,
|
|
114
|
+
recommending,
|
|
115
|
+
answerQuestion,
|
|
116
|
+
setQuestionStatus,
|
|
117
|
+
recommendAnswer,
|
|
118
|
+
continuePlanning,
|
|
119
|
+
proceedPlanning,
|
|
120
|
+
}
|
|
121
|
+
}
|
package/app/stores/initiative.ts
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { computed, ref } from 'vue'
|
|
3
|
-
import type {
|
|
4
|
-
Initiative,
|
|
5
|
-
InitiativeExecutionPolicy,
|
|
6
|
-
InitiativePresetDescriptor,
|
|
7
|
-
InitiativePresetInputs,
|
|
8
|
-
PromoteInitiativeFollowUpInput,
|
|
9
|
-
UpdateInitiativeItemInput,
|
|
10
|
-
} from '~/types/domain'
|
|
3
|
+
import type { Initiative, InitiativePresetDescriptor, InitiativePresetInputs } from '~/types/domain'
|
|
11
4
|
|
|
12
5
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
13
6
|
import { useBoardStore } from '~/stores/board'
|
|
7
|
+
import {
|
|
8
|
+
createInitiativePlanningActions,
|
|
9
|
+
type InitiativeActionContext,
|
|
10
|
+
} from '~/stores/initiative/planning'
|
|
11
|
+
import { createInitiativeCurationActions } from '~/stores/initiative/curation'
|
|
14
12
|
|
|
15
13
|
/** The built-in generic preset id (mirrors kernel's `GENERIC_INITIATIVE_PRESET_ID`). */
|
|
16
14
|
export const GENERIC_PRESET_ID = 'preset_generic'
|
|
@@ -165,91 +163,6 @@ export const useInitiativesStore = defineStore('initiatives', () => {
|
|
|
165
163
|
}
|
|
166
164
|
}
|
|
167
165
|
|
|
168
|
-
/** True while a planning-window action (continue/proceed) is resuming the run. */
|
|
169
|
-
const resuming = ref(false)
|
|
170
|
-
|
|
171
|
-
/** Record the human's answer to one pending interview question (no run resume). */
|
|
172
|
-
async function answerQuestion(blockId: string, questionId: string, answer: string) {
|
|
173
|
-
if (!workspace.workspaceId) throw new Error('No active workspace')
|
|
174
|
-
const updated = await api.answerInitiativeQuestion(
|
|
175
|
-
workspace.workspaceId,
|
|
176
|
-
blockId,
|
|
177
|
-
questionId,
|
|
178
|
-
answer,
|
|
179
|
-
)
|
|
180
|
-
upsert(updated)
|
|
181
|
-
return updated
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/** Question ids the interviewer is currently drafting a recommendation for (window spinner). */
|
|
185
|
-
const recommending = ref<Set<string>>(new Set())
|
|
186
|
-
|
|
187
|
-
/** Mark a planning question not-relevant (`dismissed`) or reopen it (no run resume). */
|
|
188
|
-
async function setQuestionStatus(
|
|
189
|
-
blockId: string,
|
|
190
|
-
questionId: string,
|
|
191
|
-
status: 'open' | 'dismissed',
|
|
192
|
-
) {
|
|
193
|
-
if (!workspace.workspaceId) throw new Error('No active workspace')
|
|
194
|
-
const updated = await api.setInitiativeQuestionStatus(
|
|
195
|
-
workspace.workspaceId,
|
|
196
|
-
blockId,
|
|
197
|
-
questionId,
|
|
198
|
-
status,
|
|
199
|
-
)
|
|
200
|
-
upsert(updated)
|
|
201
|
-
return updated
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Ask the interviewer to recommend a suggested answer for one pending question. Runs the
|
|
206
|
-
* interviewer LLM inline server-side; the returned entity carries the suggestion on the question.
|
|
207
|
-
* Tracks the in-flight id so the window can show a per-question spinner.
|
|
208
|
-
*/
|
|
209
|
-
async function recommendAnswer(blockId: string, questionId: string) {
|
|
210
|
-
if (!workspace.workspaceId) throw new Error('No active workspace')
|
|
211
|
-
recommending.value = new Set(recommending.value).add(questionId)
|
|
212
|
-
try {
|
|
213
|
-
const updated = await api.recommendInitiativeAnswer(
|
|
214
|
-
workspace.workspaceId,
|
|
215
|
-
blockId,
|
|
216
|
-
questionId,
|
|
217
|
-
)
|
|
218
|
-
upsert(updated)
|
|
219
|
-
return updated
|
|
220
|
-
} finally {
|
|
221
|
-
const next = new Set(recommending.value)
|
|
222
|
-
next.delete(questionId)
|
|
223
|
-
recommending.value = next
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/** Submit the answers and resume the interview (the interviewer re-runs, may ask more). */
|
|
228
|
-
async function continuePlanning(blockId: string) {
|
|
229
|
-
if (!workspace.workspaceId) throw new Error('No active workspace')
|
|
230
|
-
resuming.value = true
|
|
231
|
-
try {
|
|
232
|
-
const updated = await api.continueInitiativePlanning(workspace.workspaceId, blockId)
|
|
233
|
-
upsert(updated)
|
|
234
|
-
return updated
|
|
235
|
-
} finally {
|
|
236
|
-
resuming.value = false
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/** Skip remaining questions: the interviewer converges and the run advances. */
|
|
241
|
-
async function proceedPlanning(blockId: string) {
|
|
242
|
-
if (!workspace.workspaceId) throw new Error('No active workspace')
|
|
243
|
-
resuming.value = true
|
|
244
|
-
try {
|
|
245
|
-
const updated = await api.proceedInitiativePlanning(workspace.workspaceId, blockId)
|
|
246
|
-
upsert(updated)
|
|
247
|
-
return updated
|
|
248
|
-
} finally {
|
|
249
|
-
resuming.value = false
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
166
|
/** True while a loop control (pause/resume/cancel) is in flight. */
|
|
254
167
|
const controlling = ref(false)
|
|
255
168
|
|
|
@@ -272,76 +185,12 @@ export const useInitiativesStore = defineStore('initiatives', () => {
|
|
|
272
185
|
}
|
|
273
186
|
}
|
|
274
187
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
try {
|
|
282
|
-
return await fn()
|
|
283
|
-
} finally {
|
|
284
|
-
curating.value = false
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/** Promote an `open` harvested follow-up into a new pending tracker item. */
|
|
289
|
-
async function promoteFollowUp(
|
|
290
|
-
initiativeId: string,
|
|
291
|
-
followUpId: string,
|
|
292
|
-
input: PromoteInitiativeFollowUpInput,
|
|
293
|
-
) {
|
|
294
|
-
return curate(async () => {
|
|
295
|
-
const updated = await api.promoteInitiativeFollowUp(
|
|
296
|
-
workspace.workspaceId!,
|
|
297
|
-
initiativeId,
|
|
298
|
-
followUpId,
|
|
299
|
-
input,
|
|
300
|
-
)
|
|
301
|
-
upsert(updated)
|
|
302
|
-
return updated
|
|
303
|
-
})
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/** Dismiss a harvested follow-up. */
|
|
307
|
-
async function dismissFollowUp(initiativeId: string, followUpId: string) {
|
|
308
|
-
return curate(async () => {
|
|
309
|
-
const updated = await api.dismissInitiativeFollowUp(
|
|
310
|
-
workspace.workspaceId!,
|
|
311
|
-
initiativeId,
|
|
312
|
-
followUpId,
|
|
313
|
-
)
|
|
314
|
-
upsert(updated)
|
|
315
|
-
return updated
|
|
316
|
-
})
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/** Edit one tracker item and/or drive its status (retry a blocked item / skip it). */
|
|
320
|
-
async function updateItem(
|
|
321
|
-
initiativeId: string,
|
|
322
|
-
itemId: string,
|
|
323
|
-
input: UpdateInitiativeItemInput,
|
|
324
|
-
) {
|
|
325
|
-
return curate(async () => {
|
|
326
|
-
const updated = await api.updateInitiativeItem(
|
|
327
|
-
workspace.workspaceId!,
|
|
328
|
-
initiativeId,
|
|
329
|
-
itemId,
|
|
330
|
-
input,
|
|
331
|
-
)
|
|
332
|
-
upsert(updated)
|
|
333
|
-
return updated
|
|
334
|
-
})
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/** Replace the execution policy (concurrency + pipeline rules). */
|
|
338
|
-
async function updatePolicy(initiativeId: string, policy: InitiativeExecutionPolicy) {
|
|
339
|
-
return curate(async () => {
|
|
340
|
-
const updated = await api.updateInitiativePolicy(workspace.workspaceId!, initiativeId, policy)
|
|
341
|
-
upsert(updated)
|
|
342
|
-
return updated
|
|
343
|
-
})
|
|
344
|
-
}
|
|
188
|
+
// The planning-window + tracker-curation actions, split into cohesive factories sharing the
|
|
189
|
+
// state above (a size-only extraction mirroring `stores/board/` — behaviour is identical to the
|
|
190
|
+
// former in-closure functions). Each owns the in-flight flags its window renders.
|
|
191
|
+
const context: InitiativeActionContext = { api, workspace, upsert }
|
|
192
|
+
const planning = createInitiativePlanningActions(context)
|
|
193
|
+
const curation = createInitiativeCurationActions(context)
|
|
345
194
|
|
|
346
195
|
function reset() {
|
|
347
196
|
byBlock.value = {}
|
|
@@ -353,10 +202,7 @@ export const useInitiativesStore = defineStore('initiatives', () => {
|
|
|
353
202
|
presets,
|
|
354
203
|
all,
|
|
355
204
|
creating,
|
|
356
|
-
resuming,
|
|
357
205
|
controlling,
|
|
358
|
-
curating,
|
|
359
|
-
recommending,
|
|
360
206
|
forBlock,
|
|
361
207
|
presetById,
|
|
362
208
|
planningPipelineIdFor,
|
|
@@ -366,16 +212,9 @@ export const useInitiativesStore = defineStore('initiatives', () => {
|
|
|
366
212
|
create,
|
|
367
213
|
probePreset,
|
|
368
214
|
load,
|
|
369
|
-
answerQuestion,
|
|
370
|
-
setQuestionStatus,
|
|
371
|
-
recommendAnswer,
|
|
372
|
-
continuePlanning,
|
|
373
|
-
proceedPlanning,
|
|
374
215
|
control,
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
updateItem,
|
|
378
|
-
updatePolicy,
|
|
216
|
+
...planning,
|
|
217
|
+
...curation,
|
|
379
218
|
reset,
|
|
380
219
|
}
|
|
381
220
|
})
|