Files
agentic-coding-workflow/web/src/stores/workspaces.ts
T
2026-06-12 14:36:41 +08:00

178 lines
5.8 KiB
TypeScript

import { defineStore } from 'pinia'
import { ref } from 'vue'
import { workspacesApi } from '@/api/workspaces'
import type {
Workspace,
Worktree,
FileStatus,
CredentialMeta,
CredentialUpsertBody,
CreateWorkspaceBody,
UpdateWorkspaceBody,
CreateWorktreeBody,
CommitBody,
} from '@/api/types'
const DEFAULT_POLL_INTERVAL_MS = 1500
const DEFAULT_POLL_MAX_MS = 5 * 60 * 1000
export const useWorkspacesStore = defineStore('workspaces', () => {
const list = ref<Workspace[]>([])
const current = ref<Workspace | null>(null)
const worktrees = ref<Worktree[]>([])
const credential = ref<CredentialMeta | null>(null)
const loading = ref(false)
async function fetchByProject(projectSlug: string) {
loading.value = true
try {
list.value = await workspacesApi.list(projectSlug)
} finally {
loading.value = false
}
}
async function fetchOne(wsID: string) {
current.value = await workspacesApi.get(wsID)
}
async function create(projectSlug: string, body: CreateWorkspaceBody) {
const ws = await workspacesApi.create(projectSlug, body)
list.value.unshift(ws)
return ws
}
async function update(wsID: string, body: UpdateWorkspaceBody) {
const ws = await workspacesApi.update(wsID, body)
if (current.value?.id === wsID) current.value = ws
// 若 wsID 不在 list 中(如深链直入 Detail 页未加载列表),map 静默 no-op;调用方按需 fetchByProject。
list.value = list.value.map((w) => (w.id === wsID ? ws : w))
return ws
}
async function remove(wsID: string) {
await workspacesApi.delete(wsID)
list.value = list.value.filter((w) => w.id !== wsID)
if (current.value?.id === wsID) current.value = null
}
async function sync(wsID: string) {
const ws = await workspacesApi.sync(wsID)
if (current.value?.id === wsID) current.value = ws
// 同 update:list 中无此 wsID 时 map 是 no-op,调用方负责拉列表。
list.value = list.value.map((w) => (w.id === wsID ? ws : w))
return ws
}
/**
* 轮询 workspace.sync_status 直到落到 idle/error,用于 Phase 9 创建/Sync 后等克隆完成。
*
* 调用方约定:
* - 若已知 ws 在 list 中,调用前请确保该项已加载(否则 list.map 静默 no-op,但 current 仍会刷新)。
* - 失败抛出 Error,message 含最后一次观测到的 sync_status,便于 UI 区分超时类型。
* - 调用方应自行处理并发:同一 wsID 多次并发轮询不会共享请求。
*
* @param wsID 目标 workspace 的 UUID。
* @param intervalMs 轮询间隔,默认 1500ms。
* @param maxMs 总超时上限,默认 5 分钟(覆盖中等仓库克隆耗时)。
*/
async function pollUntilSettled(
wsID: string,
intervalMs = DEFAULT_POLL_INTERVAL_MS,
maxMs = DEFAULT_POLL_MAX_MS,
): Promise<Workspace> {
const start = Date.now()
let lastStatus: Workspace['sync_status'] | 'unknown' = 'unknown'
while (Date.now() - start < maxMs) {
const ws = await workspacesApi.get(wsID)
lastStatus = ws.sync_status
if (current.value?.id === wsID) current.value = ws
// 同 update:list 中无此 wsID 时 map 是 no-op,仍刷新 current.value。
list.value = list.value.map((w) => (w.id === wsID ? ws : w))
if (ws.sync_status !== 'cloning' && ws.sync_status !== 'syncing') return ws
await new Promise((r) => setTimeout(r, intervalMs))
}
throw new Error(`sync polling timed out for workspace ${wsID} (last status: ${lastStatus})`)
}
// ===== credential =====
async function fetchCredential(wsID: string) {
credential.value = await workspacesApi.getCredential(wsID)
}
async function upsertCredential(wsID: string, body: CredentialUpsertBody) {
credential.value = await workspacesApi.upsertCredential(wsID, body)
}
// ===== worktrees =====
async function fetchWorktrees(wsID: string) {
worktrees.value = await workspacesApi.listWorktrees(wsID)
}
async function createWorktree(wsID: string, body: CreateWorktreeBody) {
const wt = await workspacesApi.createWorktree(wsID, body)
worktrees.value.unshift(wt)
return wt
}
async function deleteWorktree(wtID: string) {
await workspacesApi.deleteWorktree(wtID)
worktrees.value = worktrees.value.filter((w) => w.id !== wtID)
}
async function acquire(wtID: string) {
const wt = await workspacesApi.acquire(wtID)
worktrees.value = worktrees.value.map((w) => (w.id === wtID ? wt : w))
return wt
}
async function release(wtID: string) {
const wt = await workspacesApi.release(wtID)
worktrees.value = worktrees.value.map((w) => (w.id === wtID ? wt : w))
return wt
}
// ===== git ops(直透 api,store 仅负责状态变化的端点;status/commit/push 不缓存)=====
async function statusOnMain(wsID: string): Promise<FileStatus[]> {
return workspacesApi.statusOnMain(wsID)
}
async function statusOnWorktree(wtID: string): Promise<FileStatus[]> {
return workspacesApi.statusOnWorktree(wtID)
}
async function commitOnMain(wsID: string, body: CommitBody) {
return workspacesApi.commitOnMain(wsID, body)
}
async function commitOnWorktree(wtID: string, body: CommitBody) {
return workspacesApi.commitOnWorktree(wtID, body)
}
async function pushOnMain(wsID: string) {
return workspacesApi.pushOnMain(wsID)
}
async function pushOnWorktree(wtID: string) {
return workspacesApi.pushOnWorktree(wtID)
}
return {
list,
current,
worktrees,
credential,
loading,
fetchByProject,
fetchOne,
create,
update,
remove,
sync,
pollUntilSettled,
fetchCredential,
upsertCredential,
fetchWorktrees,
createWorktree,
deleteWorktree,
acquire,
release,
statusOnMain,
statusOnWorktree,
commitOnMain,
commitOnWorktree,
pushOnMain,
pushOnWorktree,
}
})