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);