kin: KIN-108-frontend_dev
This commit is contained in:
parent
8b409fd7db
commit
353416ead1
16 changed files with 799 additions and 212 deletions
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { api, ApiError, type TaskFull, type PipelineStep, type PendingAction, type DeployResult, type Attachment } from '../api'
|
||||
import Badge from '../components/Badge.vue'
|
||||
import Modal from '../components/Modal.vue'
|
||||
|
|
@ -11,6 +12,7 @@ import LiveConsole from '../components/LiveConsole.vue'
|
|||
const props = defineProps<{ id: string }>()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
const task = ref<TaskFull | null>(null)
|
||||
const loading = ref(true)
|
||||
|
|
@ -45,15 +47,14 @@ const parsedSelectedOutput = computed<ParsedAgentOutput | null>(() => {
|
|||
// Auto/Review mode (per-task, persisted in DB; falls back to localStorage per project)
|
||||
const autoMode = ref(false)
|
||||
|
||||
function loadMode(t: typeof task.value) {
|
||||
if (!t) return
|
||||
if (t.execution_mode) {
|
||||
autoMode.value = t.execution_mode === 'auto_complete'
|
||||
} else if (t.status === 'review') {
|
||||
// Task is in review — always show Approve/Reject regardless of localStorage
|
||||
function loadMode(t_val: typeof task.value) {
|
||||
if (!t_val) return
|
||||
if (t_val.execution_mode) {
|
||||
autoMode.value = t_val.execution_mode === 'auto_complete'
|
||||
} else if (t_val.status === 'review') {
|
||||
autoMode.value = false
|
||||
} else {
|
||||
autoMode.value = localStorage.getItem(`kin-mode-${t.project_id}`) === 'auto_complete'
|
||||
autoMode.value = localStorage.getItem(`kin-mode-${t_val.project_id}`) === 'auto_complete'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,11 +75,9 @@ async function load() {
|
|||
const prev = task.value
|
||||
task.value = await api.taskFull(props.id)
|
||||
loadMode(task.value)
|
||||
// Auto-start polling if task is in_progress
|
||||
if (task.value.status === 'in_progress' && !polling.value) {
|
||||
startPolling()
|
||||
}
|
||||
// Stop polling when pipeline done
|
||||
if (prev?.status === 'in_progress' && task.value.status !== 'in_progress') {
|
||||
stopPolling()
|
||||
}
|
||||
|
|
@ -241,7 +240,6 @@ async function runPipeline() {
|
|||
claudeLoginError.value = false
|
||||
pipelineStarting.value = true
|
||||
try {
|
||||
// Sync task execution_mode with current toggle state before running
|
||||
const targetMode = autoMode.value ? 'auto_complete' : 'review'
|
||||
if (task.value && task.value.execution_mode !== targetMode) {
|
||||
const updated = await api.patchTask(props.id, { execution_mode: targetMode })
|
||||
|
|
@ -254,7 +252,7 @@ async function runPipeline() {
|
|||
if (e instanceof ApiError && e.code === 'claude_auth_required') {
|
||||
claudeLoginError.value = true
|
||||
} else if (e instanceof ApiError && e.code === 'task_already_running') {
|
||||
error.value = 'Pipeline уже запущен'
|
||||
error.value = t('taskDetail.pipeline_already_running')
|
||||
} else {
|
||||
error.value = e.message
|
||||
}
|
||||
|
|
@ -271,7 +269,7 @@ const resolvingManually = ref(false)
|
|||
|
||||
async function resolveManually() {
|
||||
if (!task.value) return
|
||||
if (!confirm('Пометить задачу как решённую вручную?')) return
|
||||
if (!confirm(t('taskDetail.mark_resolved_confirm'))) return
|
||||
resolvingManually.value = true
|
||||
try {
|
||||
const updated = await api.patchTask(props.id, { status: 'done' })
|
||||
|
|
@ -386,7 +384,7 @@ async function saveEdit() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="loading && !task" class="text-gray-500 text-sm">Loading...</div>
|
||||
<div v-if="loading && !task" class="text-gray-500 text-sm">{{ t('taskDetail.loading') }}</div>
|
||||
<div v-else-if="error && !task" class="text-red-400 text-sm">{{ error }}</div>
|
||||
<div v-else-if="task">
|
||||
<!-- Header -->
|
||||
|
|
@ -422,7 +420,7 @@ async function saveEdit() {
|
|||
<!-- Manual escalation context banner -->
|
||||
<div v-if="isManualEscalation" class="mb-3 px-3 py-2 border border-orange-800/60 bg-orange-950/20 rounded">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="text-xs font-semibold text-orange-400">⚠ Требует ручного решения</span>
|
||||
<span class="text-xs font-semibold text-orange-400">{{ t('taskDetail.requires_manual') }}</span>
|
||||
<span v-if="task.parent_task_id" class="text-xs text-gray-600">
|
||||
— эскалация из
|
||||
<router-link :to="`/task/${task.parent_task_id}`" class="text-orange-600 hover:text-orange-400">
|
||||
|
|
@ -432,15 +430,15 @@ async function saveEdit() {
|
|||
</div>
|
||||
<p class="text-xs text-orange-300">{{ task.title }}</p>
|
||||
<p v-if="task.brief?.description" class="text-xs text-gray-400 mt-1">{{ task.brief.description }}</p>
|
||||
<p class="text-xs text-gray-600 mt-1">Автопилот не смог выполнить это автоматически. Примите меры вручную и нажмите «Решить вручную».</p>
|
||||
<p class="text-xs text-gray-600 mt-1">{{ t('taskDetail.autopilot_failed') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Dangerous skip warning banner -->
|
||||
<div v-if="task.dangerously_skipped" class="mb-3 px-3 py-2 border border-red-700 bg-red-950/40 rounded flex items-start gap-2">
|
||||
<span class="text-red-400 text-base shrink-0">⚠</span>
|
||||
<div>
|
||||
<span class="text-xs font-semibold text-red-400">--dangerously-skip-permissions использовался в этой задаче</span>
|
||||
<p class="text-xs text-red-300/70 mt-0.5">Агент выполнял команды с обходом проверок разрешений. Проверьте pipeline-шаги и сделанные изменения.</p>
|
||||
<span class="text-xs font-semibold text-red-400">{{ t('taskDetail.dangerously_skipped') }}</span>
|
||||
<p class="text-xs text-red-300/70 mt-0.5">{{ t('taskDetail.dangerously_skipped_hint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -448,7 +446,7 @@ async function saveEdit() {
|
|||
Brief: {{ JSON.stringify(task.brief) }}
|
||||
</div>
|
||||
<div v-if="task.acceptance_criteria" class="mb-2 px-3 py-2 border border-gray-700 bg-gray-900/40 rounded">
|
||||
<div class="text-xs font-semibold text-gray-400 mb-1">Критерии приёмки</div>
|
||||
<div class="text-xs font-semibold text-gray-400 mb-1">{{ t('taskDetail.acceptance_criteria') }}</div>
|
||||
<p class="text-xs text-gray-300 whitespace-pre-wrap">{{ task.acceptance_criteria }}</p>
|
||||
</div>
|
||||
<div v-if="task.status === 'blocked' && task.blocked_reason" class="text-xs text-red-400 mb-1 bg-red-950/30 border border-red-800/40 rounded px-2 py-1">
|
||||
|
|
@ -462,8 +460,8 @@ async function saveEdit() {
|
|||
<!-- Pipeline Graph -->
|
||||
<div v-if="hasSteps || isRunning" class="mb-6">
|
||||
<h2 class="text-sm font-semibold text-gray-300 mb-3">
|
||||
Pipeline
|
||||
<span v-if="isRunning" class="text-blue-400 text-xs font-normal ml-2 animate-pulse">running...</span>
|
||||
{{ t('taskDetail.pipeline') }}
|
||||
<span v-if="isRunning" class="text-blue-400 text-xs font-normal ml-2 animate-pulse">{{ t('taskDetail.running') }}</span>
|
||||
</h2>
|
||||
<div class="flex items-center gap-1 overflow-x-auto pb-2">
|
||||
<template v-for="(step, i) in task.pipeline_steps" :key="step.id">
|
||||
|
|
@ -493,7 +491,7 @@ async function saveEdit() {
|
|||
|
||||
<!-- No pipeline -->
|
||||
<div v-if="!hasSteps && !isRunning" class="mb-6 text-sm text-gray-600">
|
||||
No pipeline steps yet.
|
||||
{{ t('taskDetail.no_pipeline') }}
|
||||
</div>
|
||||
|
||||
<!-- Live Console -->
|
||||
|
|
@ -516,7 +514,7 @@ async function saveEdit() {
|
|||
<div class="p-4">
|
||||
<p class="text-sm text-gray-200 leading-relaxed whitespace-pre-wrap">{{ parsedSelectedOutput.verdict }}</p>
|
||||
<details v-if="parsedSelectedOutput.details !== null" class="mt-3">
|
||||
<summary class="text-xs text-gray-500 cursor-pointer hover:text-gray-400 select-none">↓ подробнее</summary>
|
||||
<summary class="text-xs text-gray-500 cursor-pointer hover:text-gray-400 select-none">{{ t('taskDetail.more_details') }}</summary>
|
||||
<pre class="mt-2 text-xs text-gray-500 overflow-x-auto whitespace-pre-wrap max-h-[400px] overflow-y-auto">{{ parsedSelectedOutput.details }}</pre>
|
||||
</details>
|
||||
</div>
|
||||
|
|
@ -542,7 +540,7 @@ async function saveEdit() {
|
|||
|
||||
<!-- Attachments -->
|
||||
<div class="mb-6">
|
||||
<h2 class="text-sm font-semibold text-gray-300 mb-2">Вложения</h2>
|
||||
<h2 class="text-sm font-semibold text-gray-300 mb-2">{{ t('taskDetail.attachments') }}</h2>
|
||||
<AttachmentList :attachments="attachments" :task-id="props.id" @deleted="loadAttachments" />
|
||||
<AttachmentUploader :task-id="props.id" @uploaded="loadAttachments" />
|
||||
</div>
|
||||
|
|
@ -552,22 +550,22 @@ async function saveEdit() {
|
|||
<div v-if="autoMode && (isRunning || task.status === 'review')"
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 bg-yellow-900/20 border border-yellow-800/50 rounded text-xs text-yellow-400">
|
||||
<span class="inline-block w-2 h-2 bg-yellow-400 rounded-full animate-pulse"></span>
|
||||
Автопилот активен
|
||||
{{ t('taskDetail.autopilot_active') }}
|
||||
</div>
|
||||
<button v-if="task.status === 'review' && !autoMode"
|
||||
@click="showApprove = true"
|
||||
class="px-4 py-2 text-sm bg-green-900/50 text-green-400 border border-green-800 rounded hover:bg-green-900">
|
||||
✓ Approve
|
||||
{{ t('taskDetail.approve_task') }}
|
||||
</button>
|
||||
<button v-if="task.status === 'review' && !autoMode"
|
||||
@click="showRevise = true"
|
||||
class="px-4 py-2 text-sm bg-orange-900/50 text-orange-400 border border-orange-800 rounded hover:bg-orange-900">
|
||||
🔄 Revise
|
||||
{{ t('taskDetail.revise_task') }}
|
||||
</button>
|
||||
<button v-if="(task.status === 'review' || task.status === 'in_progress') && !autoMode"
|
||||
@click="showReject = true"
|
||||
class="px-4 py-2 text-sm bg-red-900/50 text-red-400 border border-red-800 rounded hover:bg-red-900">
|
||||
✗ Reject
|
||||
{{ t('taskDetail.reject_task') }}
|
||||
</button>
|
||||
<button v-if="task.status === 'pending' || task.status === 'blocked' || task.status === 'review'"
|
||||
@click="toggleMode"
|
||||
|
|
@ -581,28 +579,28 @@ async function saveEdit() {
|
|||
<button v-if="task.status === 'pending'"
|
||||
@click="openEdit"
|
||||
class="px-3 py-2 text-sm bg-gray-800/50 text-gray-400 border border-gray-700 rounded hover:bg-gray-800">
|
||||
✎ Edit
|
||||
{{ t('taskDetail.edit') }}
|
||||
</button>
|
||||
<button v-if="task.status === 'pending' || task.status === 'blocked'"
|
||||
@click="runPipeline"
|
||||
:disabled="polling || pipelineStarting"
|
||||
class="px-4 py-2 text-sm bg-blue-900/50 text-blue-400 border border-blue-800 rounded hover:bg-blue-900 disabled:opacity-50">
|
||||
<span v-if="polling || pipelineStarting" class="inline-block w-3 h-3 border-2 border-blue-400 border-t-transparent rounded-full animate-spin mr-1"></span>
|
||||
{{ (polling || pipelineStarting) ? 'Pipeline running...' : '▶ Run Pipeline' }}
|
||||
{{ (polling || pipelineStarting) ? t('taskDetail.pipeline_running') : t('taskDetail.run_pipeline') }}
|
||||
</button>
|
||||
<button v-if="isManualEscalation && task.status !== 'done' && task.status !== 'cancelled'"
|
||||
@click="resolveManually"
|
||||
:disabled="resolvingManually"
|
||||
class="px-4 py-2 text-sm bg-orange-900/50 text-orange-400 border border-orange-800 rounded hover:bg-orange-900 disabled:opacity-50">
|
||||
<span v-if="resolvingManually" class="inline-block w-3 h-3 border-2 border-orange-400 border-t-transparent rounded-full animate-spin mr-1"></span>
|
||||
{{ resolvingManually ? 'Сохраняем...' : '✓ Решить вручную' }}
|
||||
{{ resolvingManually ? t('taskDetail.resolving') : t('taskDetail.resolve_manually') }}
|
||||
</button>
|
||||
<button v-if="task.status === 'done' && (task.project_deploy_command || task.project_deploy_runtime)"
|
||||
@click.stop="runDeploy"
|
||||
:disabled="deploying"
|
||||
class="px-4 py-2 text-sm bg-teal-900/50 text-teal-400 border border-teal-800 rounded hover:bg-teal-900 disabled:opacity-50">
|
||||
<span v-if="deploying" class="inline-block w-3 h-3 border-2 border-teal-400 border-t-transparent rounded-full animate-spin mr-1"></span>
|
||||
{{ deploying ? 'Deploying...' : '🚀 Deploy' }}
|
||||
{{ deploying ? t('taskDetail.deploying') : t('taskDetail.deploy') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -611,9 +609,9 @@ async function saveEdit() {
|
|||
<div class="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-yellow-300">⚠ Claude CLI requires login</p>
|
||||
<p class="text-xs text-yellow-200/80 mt-1">Откройте терминал и выполните:</p>
|
||||
<p class="text-xs text-yellow-200/80 mt-1">{{ t('taskDetail.terminal_login_hint') }}</p>
|
||||
<code class="text-xs text-yellow-400 font-mono bg-black/30 px-2 py-0.5 rounded mt-1 inline-block">claude login</code>
|
||||
<p class="text-xs text-gray-500 mt-1">После входа повторите запуск pipeline.</p>
|
||||
<p class="text-xs text-gray-500 mt-1">{{ t('taskDetail.login_after_hint') }}</p>
|
||||
</div>
|
||||
<button @click="claudeLoginError = false" class="text-gray-600 hover:text-gray-400 bg-transparent border-none cursor-pointer text-xs shrink-0">✕</button>
|
||||
</div>
|
||||
|
|
@ -624,7 +622,7 @@ async function saveEdit() {
|
|||
:class="deployResult.overall_success !== false && deployResult.success ? 'border-teal-800 bg-teal-950/30 text-teal-300' : 'border-red-800 bg-red-950/30 text-red-300'">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span :class="deployResult.overall_success !== false && deployResult.success ? 'text-teal-400' : 'text-red-400'" class="font-semibold">
|
||||
{{ deployResult.overall_success !== false && deployResult.success ? 'Deploy succeeded' : 'Deploy failed' }}
|
||||
{{ deployResult.overall_success !== false && deployResult.success ? t('taskDetail.deploy_succeeded') : t('taskDetail.deploy_failed') }}
|
||||
</span>
|
||||
<span class="text-gray-500">{{ deployResult.duration_seconds }}s</span>
|
||||
<button @click.stop="deployResult = null" class="ml-auto text-gray-600 hover:text-gray-400 bg-transparent border-none cursor-pointer text-xs">x</button>
|
||||
|
|
@ -650,7 +648,7 @@ async function saveEdit() {
|
|||
</template>
|
||||
<!-- Dependents -->
|
||||
<div v-if="deployResult.dependents_deployed?.length" class="mt-2 border-t border-gray-700 pt-2">
|
||||
<p class="text-xs text-gray-400 font-semibold mb-1">Зависимые проекты:</p>
|
||||
<p class="text-xs text-gray-400 font-semibold mb-1">{{ t('taskDetail.dependent_projects') }}</p>
|
||||
<div v-for="dep in deployResult.dependents_deployed" :key="dep" class="flex items-center gap-2 px-2 py-0.5">
|
||||
<span class="text-teal-400 font-semibold text-[10px]">ok</span>
|
||||
<span class="text-gray-300 text-[11px]">{{ dep }}</span>
|
||||
|
|
@ -704,9 +702,9 @@ async function saveEdit() {
|
|||
Create follow-up tasks from pipeline results
|
||||
</label>
|
||||
<p class="text-xs text-gray-500">Optionally record a decision:</p>
|
||||
<input v-model="approveForm.title" placeholder="Decision title (optional)"
|
||||
<input v-model="approveForm.title" :placeholder="t('taskDetail.decision_title_placeholder')"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
|
||||
<textarea v-if="approveForm.title" v-model="approveForm.description" placeholder="Description"
|
||||
<textarea v-if="approveForm.title" v-model="approveForm.description" :placeholder="t('taskDetail.description_placeholder')"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600 resize-y" rows="2"></textarea>
|
||||
<button type="submit" :disabled="approveLoading"
|
||||
class="w-full py-2 bg-green-900/50 text-green-400 border border-green-800 rounded text-sm hover:bg-green-900 disabled:opacity-50">
|
||||
|
|
@ -728,14 +726,14 @@ async function saveEdit() {
|
|||
</Modal>
|
||||
|
||||
<!-- Revise Modal -->
|
||||
<Modal v-if="showRevise" title="🔄 Revise Task" @close="showRevise = false">
|
||||
<Modal v-if="showRevise" :title="t('taskDetail.send_to_revision')" @close="showRevise = false">
|
||||
<form @submit.prevent="revise" class="space-y-3">
|
||||
<p class="text-xs text-gray-500">Опишите, что доработать или уточнить агенту. Задача вернётся в работу с вашим комментарием.</p>
|
||||
<textarea v-model="reviseComment" placeholder="Что доработать / уточнить..." rows="4" required
|
||||
<textarea v-model="reviseComment" :placeholder="t('taskDetail.revise_placeholder')" rows="4" required
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600 resize-y"></textarea>
|
||||
<button type="submit"
|
||||
class="w-full py-2 bg-orange-900/50 text-orange-400 border border-orange-800 rounded text-sm hover:bg-orange-900">
|
||||
🔄 Отправить на доработку
|
||||
{{ t('taskDetail.send_to_revision') }}
|
||||
</button>
|
||||
</form>
|
||||
</Modal>
|
||||
|
|
@ -744,30 +742,30 @@ async function saveEdit() {
|
|||
<Modal v-if="showEdit" title="Edit Task" @close="showEdit = false">
|
||||
<form @submit.prevent="saveEdit" class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Title</label>
|
||||
<label class="block text-xs text-gray-500 mb-1">{{ t('taskDetail.title_label') }}</label>
|
||||
<input v-model="editForm.title" required
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Brief</label>
|
||||
<label class="block text-xs text-gray-500 mb-1">{{ t('taskDetail.brief_label') }}</label>
|
||||
<textarea v-model="editForm.briefText" rows="4" placeholder="Task description..."
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600 resize-y"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Priority (1–10)</label>
|
||||
<label class="block text-xs text-gray-500 mb-1">{{ t('taskDetail.priority_label') }}</label>
|
||||
<input v-model.number="editForm.priority" type="number" min="1" max="10" required
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Критерии приёмки</label>
|
||||
<label class="block text-xs text-gray-500 mb-1">{{ t('taskDetail.acceptance_criteria_label') }}</label>
|
||||
<textarea v-model="editForm.acceptanceCriteria" rows="3"
|
||||
placeholder="Что должно быть на выходе? Какой результат считается успешным?"
|
||||
:placeholder="t('taskDetail.acceptance_criteria_placeholder')"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600 resize-y"></textarea>
|
||||
</div>
|
||||
<p v-if="editError" class="text-red-400 text-xs">{{ editError }}</p>
|
||||
<button type="submit" :disabled="editLoading"
|
||||
class="w-full py-2 bg-blue-900/50 text-blue-400 border border-blue-800 rounded text-sm hover:bg-blue-900 disabled:opacity-50">
|
||||
{{ editLoading ? 'Saving...' : 'Save' }}
|
||||
{{ editLoading ? t('common.saving') : t('common.save') }}
|
||||
</button>
|
||||
</form>
|
||||
</Modal>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue