Files
hermes-web/packages/client/src/components/hermes/kanban/KanbanCreateForm.vue
T
toor 4baccd5d88 Phase 005: vendor ArkScope, land the Arco style baseline, and fix bridge interpreter resolution
- Vendor ArkScope decorative layers (ScrambleTitle / IsometricWave) with provenance notes; decorative-only per AGENTS.md red lines
- Migrate theme tokens to Arco, register the Arco theme, and add hook adapter composables (useMessage / useDialog / useNotification) with matching signatures
- Sample pages: LoginView and AppSidebar on the new baseline; docs/arco-mapping.md and docs/ui-baseline.md land
- agent-bridge: prefer the PM-selected venv from facts.json over legacy in-tree venvs (legacy lacks the mcp extra, so every MCP server showed 0 tools / not connected)
- agent-bridge python (vendored, deliberate deviation): MCP discovery import fallback and pyyaml -> hermes_yaml so plugins and MCP work under the PM env; install the mcp extra via `hermes pm install --extra mcp`
- Chat-chain fragment: docs/chat-chain-changes/2026-10-17-phase005-arkscope-baseline.md

Verified: harness:check, test (2020 passed), test:e2e (30 passed), build all green; manual: plugins 100/0 warnings, MCP 6/6 servers connected with 104 tools.
2026-10-17 09:45:30 +08:00

219 lines
7.7 KiB
Vue

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { NModal, NForm, NFormItem, NInput, NInputNumber, NSelect, NButton, NCheckbox } from 'naive-ui'
import { useMessage } from '@/composables/useMessage'
import { useI18n } from 'vue-i18n'
import { useKanbanStore } from '@/stores/hermes/kanban'
import { withDefaultAssignee } from '@/utils/hermes/kanban-assignees'
import { fetchSkills } from '@/api/hermes/skills'
import type { SkillInfo } from '@/api/hermes/skills'
const emit = defineEmits<{
close: []
created: []
}>()
const { t } = useI18n()
const message = useMessage()
const kanbanStore = useKanbanStore()
const title = ref('')
const body = ref('')
const assignee = ref<string | null>(null)
const priority = ref<number | null>(null)
const tenant = ref('')
const triage = ref(false)
const workspaceKind = ref<'scratch' | 'dir' | 'worktree'>('scratch')
const workspacePath = ref('')
const branch = ref('')
const skills = ref<string[]>([])
const maxRuntime = ref('')
const maxRetries = ref<number | null>(null)
const goalMode = ref(false)
const goalMaxTurns = ref<number | null>(null)
const saving = ref(false)
const skillsLoading = ref(false)
const skillOptions = ref<Array<{ label: string; value: string }>>([])
const priorityOptions = computed(() => [
{ label: t('kanban.card.priority.low'), value: 1 },
{ label: t('kanban.card.priority.medium'), value: 2 },
{ label: t('kanban.card.priority.high'), value: 3 },
])
const assigneeOptions = computed(() => {
return withDefaultAssignee(kanbanStore.assignees, kanbanStore.stats?.by_assignee || {})
.map(a => ({ label: a.name, value: a.name }))
})
const workspaceOptions = computed(() => [
{ label: t('kanban.form.workspaceScratch'), value: 'scratch' },
{ label: t('kanban.form.workspaceDir'), value: 'dir' },
{ label: t('kanban.form.workspaceWorktree'), value: 'worktree' },
])
function workspaceValue(): string | undefined {
if (workspaceKind.value === 'scratch') return undefined
const path = workspacePath.value.trim()
if (!path) return workspaceKind.value
return `${workspaceKind.value}:${path}`
}
function skillList(): string[] | undefined {
return skills.value.length ? skills.value : undefined
}
function buildSkillOptions(skills: SkillInfo[]): Array<{ label: string; value: string }> {
const byName = new Map<string, SkillInfo>()
for (const skill of skills) {
if (skill.enabled === false) continue
if (!byName.has(skill.name)) byName.set(skill.name, skill)
}
return [...byName.values()]
.map(skill => ({ label: skill.name, value: skill.name }))
.sort((a, b) => a.label.localeCompare(b.label))
}
onMounted(async () => {
skillsLoading.value = true
try {
const data = await fetchSkills()
skillOptions.value = buildSkillOptions(data.categories.flatMap(category => category.skills || []))
} catch {
skillOptions.value = []
} finally {
skillsLoading.value = false
}
})
async function handleSubmit() {
if (!title.value.trim()) {
message.warning(t('kanban.form.titleRequired'))
return
}
saving.value = true
try {
const payload: Parameters<typeof kanbanStore.createTask>[0] = {
title: title.value.trim(),
body: body.value.trim() || undefined,
assignee: assignee.value || undefined,
priority: priority.value ?? undefined,
}
const tenantValue = tenant.value.trim()
const selectedWorkspace = workspaceValue()
const branchValue = branch.value.trim()
const selectedSkills = skillList()
const runtimeValue = maxRuntime.value.trim()
if (tenantValue) payload.tenant = tenantValue
if (triage.value) payload.triage = true
if (selectedWorkspace) payload.workspace = selectedWorkspace
if (workspaceKind.value === 'worktree' && branchValue) payload.branch = branchValue
if (selectedSkills) payload.skills = selectedSkills
if (runtimeValue) payload.maxRuntime = runtimeValue
if (maxRetries.value !== null) payload.maxRetries = maxRetries.value
if (goalMode.value) {
payload.goalMode = true
if (goalMaxTurns.value !== null) payload.goalMaxTurns = goalMaxTurns.value
}
await kanbanStore.createTask(payload)
message.success(t('kanban.message.taskCreated'))
emit('created')
emit('close')
} catch (err: any) {
message.error(err.message)
} finally {
saving.value = false
}
}
</script>
<template>
<NModal :show="true" preset="dialog" :title="t('kanban.createTask')" style="width: 480px;" @close="emit('close')">
<NForm label-placement="top">
<NFormItem :label="t('kanban.form.title')">
<NInput v-model:value="title" :placeholder="t('kanban.form.titlePlaceholder')" />
</NFormItem>
<NFormItem :label="t('kanban.form.body')">
<NInput v-model:value="body" type="textarea" :rows="3" :placeholder="t('kanban.form.bodyPlaceholder')" />
</NFormItem>
<NFormItem :label="t('kanban.form.assignee')">
<NSelect v-model:value="assignee" :options="assigneeOptions" :placeholder="t('kanban.form.selectAssignee')" clearable />
</NFormItem>
<NFormItem :label="t('kanban.form.priority')">
<NSelect v-model:value="priority" :options="priorityOptions" :placeholder="t('kanban.form.selectPriority')" clearable />
</NFormItem>
<NFormItem :label="t('kanban.form.tenant')">
<NInput v-model:value="tenant" :placeholder="t('kanban.form.tenantPlaceholder')" />
</NFormItem>
<NFormItem :label="t('kanban.form.workspace')">
<NSelect v-model:value="workspaceKind" :options="workspaceOptions" />
</NFormItem>
<NFormItem v-if="workspaceKind !== 'scratch'" :label="t('kanban.form.workspacePath')">
<NInput v-model:value="workspacePath" :placeholder="t('kanban.form.workspacePathPlaceholder')" />
</NFormItem>
<NFormItem v-if="workspaceKind === 'worktree'" :label="t('kanban.form.branch')">
<NInput v-model:value="branch" :placeholder="t('kanban.form.branchPlaceholder')" />
</NFormItem>
<NFormItem :label="t('kanban.form.skills')">
<NSelect
v-model:value="skills"
multiple
filterable
clearable
:loading="skillsLoading"
:options="skillOptions"
:placeholder="t('kanban.form.skillsPlaceholder')"
/>
</NFormItem>
<div class="advanced-row">
<NCheckbox v-model:checked="triage">{{ t('kanban.form.triage') }}</NCheckbox>
<NCheckbox v-model:checked="goalMode">{{ t('kanban.form.goalMode') }}</NCheckbox>
</div>
<div class="advanced-grid">
<NFormItem :label="t('kanban.form.maxRuntime')">
<NInput v-model:value="maxRuntime" :placeholder="t('kanban.form.maxRuntimePlaceholder')" />
</NFormItem>
<NFormItem :label="t('kanban.form.maxRetries')">
<NInputNumber
v-model:value="maxRetries"
:min="1"
:max="100"
:precision="0"
:placeholder="t('kanban.form.maxRetriesPlaceholder')"
clearable
/>
</NFormItem>
<NFormItem v-if="goalMode" :label="t('kanban.form.goalMaxTurns')">
<NInputNumber
v-model:value="goalMaxTurns"
:min="1"
:max="100"
:precision="0"
:placeholder="t('kanban.form.goalMaxTurnsPlaceholder')"
clearable
/>
</NFormItem>
</div>
</NForm>
<template #action>
<NButton @click="emit('close')">{{ t('common.cancel') }}</NButton>
<NButton type="primary" :loading="saving" @click="handleSubmit">{{ t('common.create') }}</NButton>
</template>
</NModal>
</template>
<style scoped lang="scss">
.advanced-row {
display: flex;
gap: 16px;
margin: 0 0 12px;
flex-wrap: wrap;
}
.advanced-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
</style>