Add permission-aware follow-up flow with interactive resolution
When follow-up agent detects permission-blocked items ("ручное
применение", "permission denied", etc.), they become pending_actions
instead of auto-created tasks. User chooses per item:
1. Rerun with --dangerously-skip-permissions
2. Create manual task
3. Skip
core/followup.py:
_is_permission_blocked() — regex detection of 9 permission patterns
generate_followups() returns {created, pending_actions}
resolve_pending_action() — handles rerun/manual_task/skip
agents/runner.py:
_run_claude(allow_write=True) adds --dangerously-skip-permissions
run_agent/run_pipeline pass allow_write through
CLI: kin approve --followup — interactive 1/2/3 prompt per blocked item
API: POST /approve returns {needs_decision, pending_actions}
POST /resolve resolves individual actions
Frontend: pending actions shown as cards with 3 buttons in approve modal
136 tests, all passing. Frontend builds clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9264415776
commit
ab693d3c4d
7 changed files with 356 additions and 73 deletions
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { api, type TaskFull, type PipelineStep } from '../api'
|
||||
import { api, type TaskFull, type PipelineStep, type PendingAction } from '../api'
|
||||
import Badge from '../components/Badge.vue'
|
||||
import Modal from '../components/Modal.vue'
|
||||
|
||||
|
|
@ -18,6 +18,8 @@ const showApprove = ref(false)
|
|||
const approveForm = ref({ title: '', description: '', type: 'decision', createFollowups: true })
|
||||
const approveLoading = ref(false)
|
||||
const followupResults = ref<{ id: string; title: string }[]>([])
|
||||
const pendingActions = ref<PendingAction[]>([])
|
||||
const resolvingAction = ref(false)
|
||||
|
||||
// Reject modal
|
||||
const showReject = ref(false)
|
||||
|
|
@ -78,6 +80,7 @@ async function approve() {
|
|||
if (!task.value) return
|
||||
approveLoading.value = true
|
||||
followupResults.value = []
|
||||
pendingActions.value = []
|
||||
try {
|
||||
const data: Record<string, unknown> = {
|
||||
create_followups: approveForm.value.createFollowups,
|
||||
|
|
@ -90,7 +93,11 @@ async function approve() {
|
|||
const res = await api.approveTask(props.id, data as any)
|
||||
if (res.followup_tasks?.length) {
|
||||
followupResults.value = res.followup_tasks.map(t => ({ id: t.id, title: t.title }))
|
||||
} else {
|
||||
}
|
||||
if (res.pending_actions?.length) {
|
||||
pendingActions.value = res.pending_actions
|
||||
}
|
||||
if (!res.followup_tasks?.length && !res.pending_actions?.length) {
|
||||
showApprove.value = false
|
||||
}
|
||||
approveForm.value = { title: '', description: '', type: 'decision', createFollowups: true }
|
||||
|
|
@ -102,6 +109,25 @@ async function approve() {
|
|||
}
|
||||
}
|
||||
|
||||
async function resolveAction(action: PendingAction, choice: string) {
|
||||
resolvingAction.value = true
|
||||
try {
|
||||
const res = await api.resolveAction(props.id, action, choice)
|
||||
// Remove resolved action from list
|
||||
pendingActions.value = pendingActions.value.filter(a => a !== action)
|
||||
if (choice === 'manual_task' && res.result && typeof res.result === 'object' && 'id' in res.result) {
|
||||
followupResults.value.push({ id: (res.result as any).id, title: (res.result as any).title })
|
||||
}
|
||||
if (choice === 'rerun') {
|
||||
await load()
|
||||
}
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
} finally {
|
||||
resolvingAction.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function reject() {
|
||||
if (!task.value || !rejectReason.value) return
|
||||
try {
|
||||
|
|
@ -242,9 +268,31 @@ const hasSteps = computed(() => (task.value?.pipeline_steps?.length ?? 0) > 0)
|
|||
</div>
|
||||
|
||||
<!-- Approve Modal -->
|
||||
<Modal v-if="showApprove" title="Approve Task" @close="showApprove = false; followupResults = []">
|
||||
<Modal v-if="showApprove" title="Approve Task" @close="showApprove = false; followupResults = []; pendingActions = []">
|
||||
<!-- Pending permission actions -->
|
||||
<div v-if="pendingActions.length" class="space-y-3">
|
||||
<p class="text-sm text-yellow-400">Permission issues need your decision:</p>
|
||||
<div v-for="(action, i) in pendingActions" :key="i"
|
||||
class="border border-yellow-900/50 rounded-lg p-3 space-y-2">
|
||||
<p class="text-sm text-gray-300">{{ action.description }}</p>
|
||||
<div class="flex gap-2">
|
||||
<button @click="resolveAction(action, 'rerun')" :disabled="resolvingAction"
|
||||
class="px-3 py-1 text-xs bg-blue-900/50 text-blue-400 border border-blue-800 rounded hover:bg-blue-900 disabled:opacity-50">
|
||||
Rerun (skip permissions)
|
||||
</button>
|
||||
<button @click="resolveAction(action, 'manual_task')" :disabled="resolvingAction"
|
||||
class="px-3 py-1 text-xs bg-gray-800 text-gray-300 border border-gray-700 rounded hover:bg-gray-700 disabled:opacity-50">
|
||||
Create task
|
||||
</button>
|
||||
<button @click="resolveAction(action, 'skip')" :disabled="resolvingAction"
|
||||
class="px-3 py-1 text-xs bg-gray-800 text-gray-500 border border-gray-700 rounded hover:bg-gray-700 disabled:opacity-50">
|
||||
Skip
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Follow-up results -->
|
||||
<div v-if="followupResults.length" class="space-y-3">
|
||||
<div v-if="followupResults.length && !pendingActions.length" class="space-y-3">
|
||||
<p class="text-sm text-green-400">Task approved. Created {{ followupResults.length }} follow-up tasks:</p>
|
||||
<div class="space-y-1">
|
||||
<router-link v-for="f in followupResults" :key="f.id" :to="`/task/${f.id}`"
|
||||
|
|
@ -258,7 +306,7 @@ const hasSteps = computed(() => (task.value?.pipeline_steps?.length ?? 0) > 0)
|
|||
</button>
|
||||
</div>
|
||||
<!-- Approve form -->
|
||||
<form v-else @submit.prevent="approve" class="space-y-3">
|
||||
<form v-if="!followupResults.length && !pendingActions.length" @submit.prevent="approve" class="space-y-3">
|
||||
<label class="flex items-center gap-2 text-sm text-gray-300 cursor-pointer">
|
||||
<input type="checkbox" v-model="approveForm.createFollowups"
|
||||
class="rounded border-gray-600 bg-gray-800 text-blue-500" />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue