diff --git a/public/kanban.html b/public/kanban.html
index 3a3ae85..5a04b26 100644
--- a/public/kanban.html
+++ b/public/kanban.html
@@ -495,7 +495,7 @@
'nav.chat':'Чат','nav.kanban':'Kanban дошка','nav.aria':'Навігація між розділами',
'stat.total':'всього',
'card.sched_tip':'Чекає запуску:','modal.sched_wait':'Чекає запуску:','locale':'uk-UA',
- 'card.paused':'Ліміт використання','card.paused_tip':'Виконання зупинив ліміт використання. Автопродовження:','card.auth':'Автентифікація','card.auth_tip':'Зупинено помилкою автентифікації. Виконайте `claude login`, щоб під’єднатися знову:','card.blocked':'Заблоковано',
+ 'card.paused':'Ліміт використання','card.paused_tip':'Виконання зупинив ліміт використання. Автопродовження:','card.auth':'Автентифікація','card.auth_tip':'Зупинено помилкою автентифікації. Виконайте `claude login`, щоб під’єднатися знову:','card.blocked':'Заблоковано','card.resume_tip':'Відповісти й відновити','card.resume_prompt':'Що відповісти боту? (можна залишити порожнім)','card.resume_note_prefix':'Відповідь на блокування','card.resume_note_empty':'(без відповіді — просто спробувати ще раз)','card.resumed':'Задачу відновлено',
'hdr.group':'Група','group.add':'Нова група','group.edit':'Редагувати групу',
'group.title':'Назва групи *','group.tasks':'Завдання','group.add_task':'+ Додати завдання',
'group.empty':'Додайте завдання до групи','group.activate':'Запустити',
@@ -543,7 +543,7 @@
'nav.chat':'Chat','nav.kanban':'Kanban board','nav.aria':'Navigation',
'stat.total':'total',
'card.sched_tip':'Waiting for launch:','modal.sched_wait':'Waiting for launch:','locale':'en-US',
- 'card.paused':'Usage limit','card.paused_tip':'Stopped by a usage limit. Resumes automatically:','card.auth':'Auth','card.auth_tip':'Stopped by an authentication failure. Run `claude login` to reconnect:','card.blocked':'Blocked',
+ 'card.paused':'Usage limit','card.paused_tip':'Stopped by a usage limit. Resumes automatically:','card.auth':'Auth','card.auth_tip':'Stopped by an authentication failure. Run `claude login` to reconnect:','card.blocked':'Blocked','card.resume_tip':'Reply & resume','card.resume_prompt':'What should the bot know? (leave blank to just retry)','card.resume_note_prefix':'Reply to the block','card.resume_note_empty':'(no reply — just try again)','card.resumed':'Task resumed',
'hdr.group':'Group','group.add':'New group','group.edit':'Edit group',
'group.title':'Group title *','group.tasks':'Tasks','group.add_task':'+ Add task',
'group.empty':'Add tasks to the group','group.activate':'Run',
@@ -591,7 +591,7 @@
'nav.chat':'Чат','nav.kanban':'Kanban доска','nav.aria':'Навигация',
'stat.total':'всего',
'card.sched_tip':'Ожидает запуска:','modal.sched_wait':'Ожидает запуска:','locale':'ru-RU',
- 'card.paused':'Лимит использования','card.paused_tip':'Выполнение остановил лимит использования. Автопродолжение:','card.auth':'Аутентификация','card.auth_tip':'Остановлено ошибкой аутентификации. Выполните `claude login`, чтобы подключиться снова:','card.blocked':'Заблокировано',
+ 'card.paused':'Лимит использования','card.paused_tip':'Выполнение остановил лимит использования. Автопродолжение:','card.auth':'Аутентификация','card.auth_tip':'Остановлено ошибкой аутентификации. Выполните `claude login`, чтобы подключиться снова:','card.blocked':'Заблокировано','card.resume_tip':'Ответить и возобновить','card.resume_prompt':'Что ответить боту? (можно оставить пустым)','card.resume_note_prefix':'Ответ на блокировку','card.resume_note_empty':'(без ответа — просто попробовать ещё раз)','card.resumed':'Задача возобновлена',
'hdr.group':'Группа','group.add':'Новая группа','group.edit':'Редактировать группу',
'group.title':'Название группы *','group.tasks':'Задания','group.add_task':'+ Добавить задание',
'group.empty':'Добавьте задания в группу','group.activate':'Запустить',
@@ -639,7 +639,7 @@
'nav.chat':'Discussion','nav.kanban':'Tableau Kanban','nav.aria':'Navigation',
'stat.total':'au total',
'card.sched_tip':'En attente du lancement :','modal.sched_wait':'En attente du lancement :','locale':'fr-FR',
- 'card.paused':"Limite d'utilisation",'card.paused_tip':"Arrêté par une limite d'utilisation. Reprise automatique :",'card.auth':'Authentification','card.auth_tip':"Arrêté par un échec d'authentification. Exécutez `claude login` pour vous reconnecter :",'card.blocked':'Bloqué',
+ 'card.paused':"Limite d'utilisation",'card.paused_tip':"Arrêté par une limite d'utilisation. Reprise automatique :",'card.auth':'Authentification','card.auth_tip':"Arrêté par un échec d'authentification. Exécutez `claude login` pour vous reconnecter :",'card.blocked':'Bloqué','card.resume_tip':'Répondre et reprendre','card.resume_prompt':'Que faut-il dire au bot ? (laisser vide pour juste réessayer)','card.resume_note_prefix':'Réponse au blocage','card.resume_note_empty':'(pas de réponse — réessayer simplement)','card.resumed':'Tâche reprise',
'hdr.group':'Groupe','group.add':'Nouveau groupe','group.edit':'Modifier le groupe',
'group.title':'Titre du groupe *','group.tasks':'Tâches','group.add_task':'+ Ajouter une tâche',
'group.empty':'Ajoutez des tâches au groupe','group.activate':'Lancer',
@@ -687,7 +687,7 @@
'nav.chat':'צ׳אט','nav.kanban':'לוח קנבן','nav.aria':'ניווט',
'stat.total':'סה״כ',
'card.sched_tip':'ממתין להפעלה:','modal.sched_wait':'ממתין להפעלה:','locale':'he-IL',
- 'card.paused':'מגבלת שימוש','card.paused_tip':'נעצר בגלל מגבלת שימוש. יתחדש אוטומטית:','card.auth':'אימות','card.auth_tip':'נעצר עקב כשל אימות. הרץ `claude login` כדי להתחבר מחדש:','card.blocked':'חסום',
+ 'card.paused':'מגבלת שימוש','card.paused_tip':'נעצר בגלל מגבלת שימוש. יתחדש אוטומטית:','card.auth':'אימות','card.auth_tip':'נעצר עקב כשל אימות. הרץ `claude login` כדי להתחבר מחדש:','card.blocked':'חסום','card.resume_tip':'להשיב ולהמשיך','card.resume_prompt':'מה להגיד לבוט? (אפשר להשאיר ריק)','card.resume_note_prefix':'תשובה לחסימה','card.resume_note_empty':'(בלי תשובה — פשוט לנסות שוב)','card.resumed':'המשימה חודשה',
'hdr.group':'קבוצה','group.add':'קבוצה חדשה','group.edit':'עריכת הקבוצה',
'group.title':'שם הקבוצה *','group.tasks':'משימות','group.add_task':'+ הוספת משימה',
'group.empty':'הוסיפו משימות לקבוצה','group.activate':'הפעלה',
@@ -1337,6 +1337,7 @@
${projBadge}${pausedBadge}${authBadge}${blockedBadge}${schedBadge}${sessBadge}${cfgBadges}${retryBadge}
${relTime(tk.updated_at)}
+ ${tk.status==='blocked'?``:''}
@@ -2264,6 +2265,29 @@
}catch(e){if(e.message!=='401')toast(t('toast.err')+e.message,true);}
}
+// ─── Resume a blocked task ──────────────────────────────────────────────
+// The agent escalated via report_result({blocked:true, reason}) instead of guessing or
+// going silent — this is the human side of that: read the reason, answer it (or leave
+// blank to just retry), and the task goes back to 'todo'. Appending the reply to notes
+// (not overwriting) means the NEXT run's prompt carries it — see the '---\nУточнення:'
+// block in startTask's prompt builder — and --resume on the task's existing session
+// means the agent picks up exactly where it left off, reason and all.
+async function resumeBlockedTask(id){
+ const tk=tasks.find(x=>x.id===id);
+ if(!tk)return;
+ const reply=prompt(t('card.resume_prompt')+(tk.failure_reason?'\n\n⛔ '+tk.failure_reason:''),'');
+ if(reply===null)return; // cancelled — leave it blocked
+ const stamp=new Date().toLocaleString(UI_LOCALES[lang]||UI_LOCALES.en);
+ const note=(tk.notes?tk.notes+'\n\n':'')+`--- ${t('card.resume_note_prefix')} (${stamp}) ---\n`+(reply.trim()||t('card.resume_note_empty'));
+ try{
+ await apiFetch(`/api/tasks/${id}`,{method:'PUT',headers:{'Content-Type':'application/json'},
+ body:JSON.stringify({status:'todo',notes:note})});
+ try{const bc=new BroadcastChannel('tasks');bc.postMessage({type:'task_changed',id});bc.close();}catch{}
+ toast(t('card.resumed'));
+ await refresh(true);
+ }catch(e){if(e.message!=='401')toast(t('toast.err')+e.message,true);}
+}
+
// ─── Delete ───────────────────────────────────────────────────────────────
function confirmDelete(id){
const tk=tasks.find(x=>x.id===id);
diff --git a/test/kanban-run-badges.test.js b/test/kanban-run-badges.test.js
index 404b629..3ce430e 100644
--- a/test/kanban-run-badges.test.js
+++ b/test/kanban-run-badges.test.js
@@ -143,5 +143,18 @@ check('makeCard puts them in the footer', /\$\{cfgBadges\}/.test(makeCardBody),
check('the sess_model-only badge is gone from the task card',
/tk\.sess_model\?`/.test(makeCardBody), false);
+// ── Resume a blocked task (the human side of report_result({blocked:true})) ──
+check('a blocked card gets a resume action', /tk\.status==='blocked'.*resumeBlockedTask/.test(makeCardBody), true);
+{
+ const fnStart = SRC.indexOf('async function resumeBlockedTask(');
+ check('resumeBlockedTask is defined', fnStart !== -1, true);
+ const fnBody = SRC.slice(fnStart, fnStart + 1200);
+ check('it moves the task back to todo', /status:\s*'todo'/.test(fnBody), true);
+ check('it appends to notes rather than replacing them', /tk\.notes\s*\?\s*tk\.notes\s*\+/.test(fnBody), true);
+ check('a cancelled prompt (null) leaves the task blocked — no PUT sent', /if\(reply===null\)return;/.test(fnBody), true);
+ check('it goes through the real PUT /api/tasks/:id endpoint, not a bespoke one',
+ new RegExp(String.raw`apiFetch\(\`/api/tasks/\$\{id\}\`,\{method:'PUT'`).test(fnBody), true);
+}
+
console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);