You've already forked agentic-coding-workflow
ACP / MCP 完善
This commit is contained in:
@@ -67,15 +67,37 @@ describe('AcpSessionCreateView', () => {
|
||||
it('mounts without error', async () => {
|
||||
const wrapper = mount(AcpSessionCreateView)
|
||||
await new Promise((r) => setTimeout(r, 20))
|
||||
expect(wrapper.html()).toContain('New ACP Session')
|
||||
expect(wrapper.html()).toContain('新建 ACP 会话')
|
||||
})
|
||||
|
||||
it('prefills requirement/issue number from query', async () => {
|
||||
routeMock.query = { requirement_number: '7', issue_number: '42' }
|
||||
it('prefills issue number from query', async () => {
|
||||
routeMock.query = { issue_number: '42' }
|
||||
const wrapper = mount(AcpSessionCreateView)
|
||||
await new Promise((r) => setTimeout(r, 20))
|
||||
const inputs = wrapper.findAll('input[type="number"]')
|
||||
expect((inputs[0].element as HTMLInputElement).value).toBe('42')
|
||||
expect((inputs[1].element as HTMLInputElement).value).toBe('7')
|
||||
const issueInput = wrapper.find('[data-testid="issue-number-input"] input')
|
||||
const reqInput = wrapper.find('[data-testid="requirement-number-input"] input')
|
||||
expect((issueInput.element as HTMLInputElement).value).toBe('42')
|
||||
expect((reqInput.element as HTMLInputElement).value).toBe('')
|
||||
})
|
||||
|
||||
it('prefills requirement number from query', async () => {
|
||||
routeMock.query = { requirement_number: '7' }
|
||||
const wrapper = mount(AcpSessionCreateView)
|
||||
await new Promise((r) => setTimeout(r, 20))
|
||||
const issueInput = wrapper.find('[data-testid="issue-number-input"] input')
|
||||
const reqInput = wrapper.find('[data-testid="requirement-number-input"] input')
|
||||
expect((issueInput.element as HTMLInputElement).value).toBe('')
|
||||
expect((reqInput.element as HTMLInputElement).value).toBe('7')
|
||||
})
|
||||
|
||||
it('rejects submit when issue # and requirement # are both filled', async () => {
|
||||
const wrapper = mount(AcpSessionCreateView)
|
||||
await new Promise((r) => setTimeout(r, 20))
|
||||
const issueInput = wrapper.find('[data-testid="issue-number-input"] input')
|
||||
const reqInput = wrapper.find('[data-testid="requirement-number-input"] input')
|
||||
await issueInput.setValue('42')
|
||||
await reqInput.setValue('7')
|
||||
await wrapper.find('form').trigger('submit.prevent')
|
||||
expect(wrapper.text()).toContain('最多只能填写一项')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
NButton,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NInputNumber,
|
||||
NSelect,
|
||||
NSpin,
|
||||
} from 'naive-ui'
|
||||
|
||||
import AppShell from '@/layouts/AppShell.vue'
|
||||
import { useAcpStore } from '@/stores/acp'
|
||||
import { useWorkspacesStore } from '@/stores/workspaces'
|
||||
|
||||
@@ -12,36 +23,62 @@ const wsStore = useWorkspacesStore()
|
||||
const form = ref({
|
||||
agent_kind_id: '',
|
||||
branch: '',
|
||||
issue_number: '' as string,
|
||||
requirement_number: '' as string,
|
||||
issue_number: null as number | null,
|
||||
requirement_number: null as number | null,
|
||||
initial_prompt: '',
|
||||
})
|
||||
const submitting = ref(false)
|
||||
const errorMsg = ref<string | null>(null)
|
||||
|
||||
const projectSlug = computed(() => route.params.slug as string)
|
||||
const workspaceSlug = computed(() => route.params.wsSlug as string)
|
||||
|
||||
const agentKindOptions = computed(() =>
|
||||
acpStore.enabledAgentKinds.map((k) => ({
|
||||
label: k.display_name,
|
||||
value: k.id,
|
||||
})),
|
||||
)
|
||||
|
||||
const exclusiveCount = computed(
|
||||
() =>
|
||||
[form.value.branch, form.value.issue_number, form.value.requirement_number].filter(Boolean)
|
||||
.length,
|
||||
)
|
||||
|
||||
const canSubmit = computed(
|
||||
() => !!form.value.agent_kind_id && exclusiveCount.value <= 1 && !submitting.value,
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
// 来自需求详情页等入口的预填(?requirement_number= / ?issue_number=)。
|
||||
if (typeof route.query.requirement_number === 'string') {
|
||||
form.value.requirement_number = route.query.requirement_number
|
||||
form.value.requirement_number = Number(route.query.requirement_number)
|
||||
}
|
||||
if (typeof route.query.issue_number === 'string') {
|
||||
form.value.issue_number = route.query.issue_number
|
||||
form.value.issue_number = Number(route.query.issue_number)
|
||||
}
|
||||
await acpStore.listAgentKinds(false)
|
||||
if (wsStore.list.length === 0) {
|
||||
await wsStore.fetchByProject(route.params.slug as string)
|
||||
await wsStore.fetchByProject(projectSlug.value)
|
||||
}
|
||||
})
|
||||
|
||||
async function submit() {
|
||||
errorMsg.value = null
|
||||
|
||||
if (exclusiveCount.value > 1) {
|
||||
errorMsg.value = 'Branch、Issue # 和 Requirement # 最多只能填写一项'
|
||||
return
|
||||
}
|
||||
|
||||
const ws = wsStore.list.find((w) => w.slug === workspaceSlug.value)
|
||||
if (!ws) {
|
||||
errorMsg.value = '未找到工作区,请刷新页面后重试'
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const ws = wsStore.list.find((w) => w.slug === route.params.wsSlug)
|
||||
if (!ws) {
|
||||
errorMsg.value = 'Workspace not found; please reload the page.'
|
||||
return
|
||||
}
|
||||
const req: {
|
||||
workspace_id: string
|
||||
agent_kind_id: string
|
||||
@@ -54,114 +91,116 @@ async function submit() {
|
||||
agent_kind_id: form.value.agent_kind_id,
|
||||
}
|
||||
if (form.value.branch) req.branch = form.value.branch
|
||||
if (form.value.issue_number) req.issue_number = Number(form.value.issue_number)
|
||||
if (form.value.requirement_number)
|
||||
req.requirement_number = Number(form.value.requirement_number)
|
||||
if (form.value.issue_number != null) req.issue_number = form.value.issue_number
|
||||
if (form.value.requirement_number != null) req.requirement_number = form.value.requirement_number
|
||||
if (form.value.initial_prompt) req.initial_prompt = form.value.initial_prompt
|
||||
|
||||
const sess = await acpStore.createSession(req)
|
||||
router.push(
|
||||
`/p/${route.params.slug}/w/${route.params.wsSlug}/acp-sessions/${sess.id}`,
|
||||
)
|
||||
router.push({
|
||||
name: 'acp-sessions-detail',
|
||||
params: {
|
||||
slug: projectSlug.value,
|
||||
wsSlug: workspaceSlug.value,
|
||||
sessionId: sess.id,
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
errorMsg.value = `Create failed: ${e instanceof Error ? e.message : 'unknown'}`
|
||||
errorMsg.value = `创建失败:${e instanceof Error ? e.message : 'unknown'}`
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
router.push(
|
||||
`/p/${route.params.slug}/w/${route.params.wsSlug}/acp-sessions`,
|
||||
)
|
||||
router.push({
|
||||
name: 'acp-sessions-list',
|
||||
params: { slug: projectSlug.value, wsSlug: workspaceSlug.value },
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 max-w-2xl">
|
||||
<h1 class="text-2xl font-semibold mb-4">New ACP Session</h1>
|
||||
<AppShell>
|
||||
<NSpin :show="acpStore.loading || wsStore.loading">
|
||||
<div class="p-6 max-w-2xl">
|
||||
<h1 class="text-2xl font-semibold mb-4">
|
||||
新建 ACP 会话
|
||||
</h1>
|
||||
|
||||
<form class="space-y-4" @submit.prevent="submit">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Agent Kind</label>
|
||||
<select
|
||||
v-model="form.agent_kind_id"
|
||||
class="block w-full border rounded px-2 py-1 text-sm"
|
||||
required
|
||||
>
|
||||
<option value="" disabled>-- choose --</option>
|
||||
<option
|
||||
v-for="k in acpStore.enabledAgentKinds"
|
||||
:key="k.id"
|
||||
:value="k.id"
|
||||
<NForm @submit.prevent="submit">
|
||||
<NFormItem
|
||||
label="Agent Kind"
|
||||
required
|
||||
>
|
||||
{{ k.display_name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<NSelect
|
||||
v-model:value="form.agent_kind_id"
|
||||
:options="agentKindOptions"
|
||||
placeholder="请选择 Agent Kind"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">
|
||||
Branch <span class="text-xs text-gray-500">(optional, takes priority)</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="form.branch"
|
||||
class="block w-full border rounded px-2 py-1 text-sm font-mono"
|
||||
placeholder="feat/x or main"
|
||||
/>
|
||||
</div>
|
||||
<NFormItem label="Branch">
|
||||
<NInput
|
||||
v-model:value="form.branch"
|
||||
placeholder="feat/x 或 main"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Issue # (optional)</label>
|
||||
<input
|
||||
v-model="form.issue_number"
|
||||
type="number"
|
||||
class="block w-full border rounded px-2 py-1 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Requirement # (optional)</label>
|
||||
<input
|
||||
v-model="form.requirement_number"
|
||||
type="number"
|
||||
class="block w-full border rounded px-2 py-1 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<NFormItem label="Issue #">
|
||||
<NInputNumber
|
||||
v-model:value="form.issue_number"
|
||||
:min="1"
|
||||
placeholder="可选"
|
||||
data-testid="issue-number-input"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="Requirement #">
|
||||
<NInputNumber
|
||||
v-model:value="form.requirement_number"
|
||||
:min="1"
|
||||
placeholder="可选"
|
||||
data-testid="requirement-number-input"
|
||||
/>
|
||||
</NFormItem>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500">
|
||||
Branch resolution: explicit > issue > requirement > auto temp branch.
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 mb-4">
|
||||
Branch、Issue #、Requirement # 最多填写一项;全部留空将使用自动临时分支。
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Initial Prompt (optional)</label>
|
||||
<textarea
|
||||
v-model="form.initial_prompt"
|
||||
rows="3"
|
||||
class="block w-full border rounded px-2 py-1 text-sm"
|
||||
placeholder="Send this immediately after agent ready..."
|
||||
/>
|
||||
</div>
|
||||
<NFormItem label="Initial Prompt">
|
||||
<NInput
|
||||
v-model:value="form.initial_prompt"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="会话启动后立即发送给 Agent 的提示词(可选)"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<p v-if="errorMsg" class="text-sm text-red-600">{{ errorMsg }}</p>
|
||||
<div class="flex justify-end gap-2">
|
||||
<NButton @click="cancel">
|
||||
取消
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
attr-type="submit"
|
||||
:loading="submitting"
|
||||
:disabled="!canSubmit"
|
||||
data-testid="submit-btn"
|
||||
>
|
||||
创建会话
|
||||
</NButton>
|
||||
</div>
|
||||
</NForm>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
class="px-3 py-1 rounded text-sm font-medium bg-blue-600 text-white hover:bg-blue-700 disabled:bg-gray-300"
|
||||
:disabled="submitting || !form.agent_kind_id"
|
||||
<p
|
||||
v-if="errorMsg"
|
||||
class="text-red-500 text-sm mt-2"
|
||||
>
|
||||
{{ submitting ? 'Creating...' : 'Create Session' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-1 rounded text-sm border hover:bg-gray-50"
|
||||
@click="cancel"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{{ errorMsg }}
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</NSpin>
|
||||
</AppShell>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user