Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions public/kanban.html
Original file line number Diff line number Diff line change
Expand Up @@ -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':'Запустити',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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':'Запустить',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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':'הפעלה',
Expand Down Expand Up @@ -1337,6 +1337,7 @@
${projBadge}${pausedBadge}${authBadge}${blockedBadge}${schedBadge}${sessBadge}${cfgBadges}${retryBadge}
<span class="card-time">${relTime(tk.updated_at)}</span>
<span class="card-actions">
${tk.status==='blocked'?`<button class="cbtn" title="${t('card.resume_tip')}" onclick="event.stopPropagation();resumeBlockedTask('${tk.id}')">▶</button>`:''}
<button class="cbtn" title="${t('modal.edit')}" onclick="event.stopPropagation();openEditModal('${tk.id}')">✎</button>
<button class="cbtn" title="${t('modal.delete')}" onclick="event.stopPropagation();confirmDelete('${tk.id}')">✕</button>
</span>
Expand Down Expand Up @@ -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);
Expand Down
13 changes: 13 additions & 0 deletions test/kanban-run-badges.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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\?`<span class="badge badge-muted">/.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);
Loading