day 1: Kin from zero to production - agents, GUI, autopilot, 352 tests
This commit is contained in:
parent
8d9facda4f
commit
8a6f280cbd
22 changed files with 1907 additions and 103 deletions
|
|
@ -15,7 +15,28 @@ const error = ref('')
|
|||
const activeTab = ref<'tasks' | 'decisions' | 'modules'>('tasks')
|
||||
|
||||
// Filters
|
||||
const taskStatusFilter = ref((route.query.status as string) || '')
|
||||
const ALL_TASK_STATUSES = ['pending', 'in_progress', 'review', 'blocked', 'decomposed', 'done', 'cancelled']
|
||||
|
||||
function initStatusFilter(): string[] {
|
||||
const q = route.query.status as string
|
||||
if (q) return q.split(',').filter((s: string) => s)
|
||||
const stored = localStorage.getItem(`kin-task-statuses-${props.id}`)
|
||||
if (stored) { try { return JSON.parse(stored) } catch {} }
|
||||
return []
|
||||
}
|
||||
|
||||
const selectedStatuses = ref<string[]>(initStatusFilter())
|
||||
|
||||
function toggleStatus(s: string) {
|
||||
const idx = selectedStatuses.value.indexOf(s)
|
||||
if (idx >= 0) selectedStatuses.value.splice(idx, 1)
|
||||
else selectedStatuses.value.push(s)
|
||||
}
|
||||
|
||||
function clearStatusFilter() {
|
||||
selectedStatuses.value = []
|
||||
}
|
||||
|
||||
const decisionTypeFilter = ref('')
|
||||
const decisionSearch = ref('')
|
||||
|
||||
|
|
@ -98,16 +119,17 @@ async function load() {
|
|||
}
|
||||
}
|
||||
|
||||
watch(taskStatusFilter, (val) => {
|
||||
router.replace({ query: { ...route.query, status: val || undefined } })
|
||||
})
|
||||
watch(selectedStatuses, (val) => {
|
||||
localStorage.setItem(`kin-task-statuses-${props.id}`, JSON.stringify(val))
|
||||
router.replace({ query: { ...route.query, status: val.length ? val.join(',') : undefined } })
|
||||
}, { deep: true })
|
||||
|
||||
onMounted(() => { load(); loadMode() })
|
||||
|
||||
const filteredTasks = computed(() => {
|
||||
if (!project.value) return []
|
||||
let tasks = project.value.tasks
|
||||
if (taskStatusFilter.value) tasks = tasks.filter(t => t.status === taskStatusFilter.value)
|
||||
if (selectedStatuses.value.length > 0) tasks = tasks.filter(t => selectedStatuses.value.includes(t.status))
|
||||
return tasks
|
||||
})
|
||||
|
||||
|
|
@ -145,12 +167,6 @@ function modTypeColor(t: string) {
|
|||
return m[t] || 'gray'
|
||||
}
|
||||
|
||||
const taskStatuses = computed(() => {
|
||||
if (!project.value) return []
|
||||
const s = new Set(project.value.tasks.map(t => t.status))
|
||||
return Array.from(s).sort()
|
||||
})
|
||||
|
||||
const decTypes = computed(() => {
|
||||
if (!project.value) return []
|
||||
const s = new Set(project.value.decisions.map(d => d.type))
|
||||
|
|
@ -179,7 +195,7 @@ async function runTask(taskId: string, event: Event) {
|
|||
event.stopPropagation()
|
||||
if (!confirm(`Run pipeline for ${taskId}?`)) return
|
||||
try {
|
||||
await api.runTask(taskId, autoMode.value)
|
||||
await api.runTask(taskId)
|
||||
await load()
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
|
|
@ -253,12 +269,17 @@ async function addDecision() {
|
|||
<!-- Tasks Tab -->
|
||||
<div v-if="activeTab === 'tasks'">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex gap-2">
|
||||
<select v-model="taskStatusFilter"
|
||||
class="bg-gray-800 border border-gray-700 rounded px-2 py-1 text-xs text-gray-300">
|
||||
<option value="">All statuses</option>
|
||||
<option v-for="s in taskStatuses" :key="s" :value="s">{{ s }}</option>
|
||||
</select>
|
||||
<div class="flex gap-1 flex-wrap items-center">
|
||||
<button v-for="s in ALL_TASK_STATUSES" :key="s"
|
||||
:data-status="s"
|
||||
@click="toggleStatus(s)"
|
||||
class="px-2 py-0.5 text-xs rounded border transition-colors"
|
||||
:class="selectedStatuses.includes(s)
|
||||
? 'bg-blue-900/40 text-blue-300 border-blue-700'
|
||||
: 'bg-gray-900 text-gray-600 border-gray-800 hover:text-gray-400 hover:border-gray-700'"
|
||||
>{{ s }}</button>
|
||||
<button v-if="selectedStatuses.length" data-action="clear-status" @click="clearStatusFilter"
|
||||
class="px-1.5 py-0.5 text-xs text-gray-600 hover:text-red-400 rounded">✕</button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button @click="toggleMode"
|
||||
|
|
@ -284,7 +305,7 @@ async function addDecision() {
|
|||
<div v-if="filteredTasks.length === 0" class="text-gray-600 text-sm">No tasks.</div>
|
||||
<div v-else class="space-y-1">
|
||||
<router-link v-for="t in filteredTasks" :key="t.id"
|
||||
:to="{ path: `/task/${t.id}`, query: taskStatusFilter ? { back_status: taskStatusFilter } : undefined }"
|
||||
:to="{ path: `/task/${t.id}`, query: selectedStatuses.length ? { back_status: selectedStatuses.join(',') } : undefined }"
|
||||
class="flex items-center justify-between px-3 py-2 border border-gray-800 rounded text-sm hover:border-gray-600 no-underline block transition-colors">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span class="text-gray-500 shrink-0 w-24">{{ t.id }}</span>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ function loadMode(t: typeof task.value) {
|
|||
if (!t) return
|
||||
if (t.execution_mode) {
|
||||
autoMode.value = t.execution_mode === 'auto'
|
||||
} else if (t.status === 'review') {
|
||||
// Task is in review — always show Approve/Reject regardless of localStorage
|
||||
autoMode.value = false
|
||||
} else {
|
||||
autoMode.value = localStorage.getItem(`kin-mode-${t.project_id}`) === 'auto'
|
||||
}
|
||||
|
|
@ -188,7 +191,7 @@ async function reject() {
|
|||
|
||||
async function runPipeline() {
|
||||
try {
|
||||
await api.runTask(props.id, autoMode.value)
|
||||
await api.runTask(props.id)
|
||||
startPolling()
|
||||
await load()
|
||||
} catch (e: any) {
|
||||
|
|
@ -264,6 +267,9 @@ async function changeStatus(newStatus: string) {
|
|||
<div v-if="task.brief" class="text-xs text-gray-500 mb-1">
|
||||
Brief: {{ JSON.stringify(task.brief) }}
|
||||
</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">
|
||||
Blocked: {{ task.blocked_reason }}
|
||||
</div>
|
||||
<div v-if="task.assigned_role" class="text-xs text-gray-500">
|
||||
Assigned: {{ task.assigned_role }}
|
||||
</div>
|
||||
|
|
@ -346,7 +352,7 @@ async function changeStatus(newStatus: string) {
|
|||
class="px-4 py-2 text-sm bg-red-900/50 text-red-400 border border-red-800 rounded hover:bg-red-900">
|
||||
✗ Reject
|
||||
</button>
|
||||
<button v-if="task.status === 'pending' || task.status === 'blocked'"
|
||||
<button v-if="task.status === 'pending' || task.status === 'blocked' || task.status === 'review'"
|
||||
@click="toggleMode"
|
||||
class="px-3 py-2 text-sm border rounded transition-colors"
|
||||
:class="autoMode
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue