fix(kanban): show unassigned tasks as explicit profile lane on mobile

- Add KANBAN_UNASSIGNED_LANE constant and _kanbanLaneKey/_kanbanLaneLabel
  helpers so unassigned tasks map to a stable internal key '__unassigned__'
  rather than a locale-dependent translated string
- _kanbanLaneNames now places 'default' first, other assignees alphabetically,
  and the Unassigned lane last
- _kanbanRenderProfileLanes filters by _kanbanLaneKey, renders label via
  _kanbanLaneLabel, and adds kanban-profile-lane-unassigned CSS class
- _kanbanTaskMeta always emits the unassigned label so sidebar shows it too
- _kanbanRenderBoard checks unfiltered total and shows a 'Tasks hidden by
  filters' state with a Clear filters button when text search hides all tasks
- Add kanban_tasks_hidden_by_filters and kanban_clear_filters i18n keys to
  all 13 locale blocks
- Add .kanban-profile-lane-unassigned header border using warning token

Fixes: on Dynasty board mobile, Ready=3 CLI but default lane showed Ready=0
because those tasks were unassigned and had no visible lane.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Brett Yelverton 2026-06-18 16:57:29 +00:00 committed by nesquena-hermes
parent b8cf8dfc4e
commit dfcd94a808
3 changed files with 59 additions and 7 deletions

View file

@ -849,6 +849,8 @@ const LOCALES = {
kanban_no_matching_tasks: 'No matching tasks',
kanban_no_data: 'No Kanban data',
kanban_work_queue_hint: 'This is the Hermes Agent work queue. Create or triage a task, assign it, move it to Ready, then let the dispatcher claim it.',
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
kanban_clear_filters: 'Clear filters',
kanban_unavailable: 'Kanban unavailable',
kanban_read_only: 'Read-only view',
kanban_empty: 'Empty',
@ -2445,6 +2447,8 @@ const LOCALES = {
kanban_no_matching_tasks: 'Nessun task corrispondente',
kanban_no_data: 'Nessun dato Kanban',
kanban_work_queue_hint: 'Questa è la coda di lavoro di Hermes Agent. Crea o classifica un task, assegnalo, spostalo in Pronto, poi lascia che il dispatcher lo reclami.',
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
kanban_clear_filters: 'Clear filters',
kanban_unavailable: 'Kanban non disponibile',
kanban_read_only: 'Vista sola lettura',
kanban_empty: 'Vuoto',
@ -4032,6 +4036,8 @@ const LOCALES = {
kanban_no_matching_tasks: '一致するタスクがありません',
kanban_no_data: 'カンバンデータがありません',
kanban_work_queue_hint: 'これは Hermes Agent のワークキューです。タスクを作成またはトリアージし、担当者を割り当て、Ready に移動すると、ディスパッチャーがそれをクレームします。',
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
kanban_clear_filters: 'Clear filters',
kanban_unavailable: 'カンバンを利用できません',
kanban_read_only: '読み取り専用',
kanban_empty: '空',
@ -5353,6 +5359,8 @@ const LOCALES = {
kanban_no_matching_tasks: 'No matching tasks',
kanban_no_data: 'No Kanban data',
kanban_work_queue_hint: 'This is the Hermes Agent work queue. Create or triage a task, assign it, move it to Ready, then let the dispatcher claim it.',
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
kanban_clear_filters: 'Clear filters',
kanban_unavailable: 'Kanban unavailable',
kanban_read_only: 'Read-only view',
kanban_empty: 'Empty',
@ -6877,6 +6885,8 @@ const LOCALES = {
kanban_no_matching_tasks: 'No matching tasks',
kanban_no_data: 'No Kanban data',
kanban_work_queue_hint: 'This is the Hermes Agent work queue. Create or triage a task, assign it, move it to Ready, then let the dispatcher claim it.',
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
kanban_clear_filters: 'Clear filters',
kanban_unavailable: 'Kanban unavailable',
kanban_read_only: 'Read-only view',
kanban_empty: 'Empty',
@ -8379,6 +8389,8 @@ const LOCALES = {
kanban_no_matching_tasks: 'No matching tasks',
kanban_no_data: 'No Kanban data',
kanban_work_queue_hint: 'This is the Hermes Agent work queue. Create or triage a task, assign it, move it to Ready, then let the dispatcher claim it.',
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
kanban_clear_filters: 'Clear filters',
kanban_unavailable: 'Kanban unavailable',
kanban_read_only: 'Read-only view',
kanban_empty: 'Empty',
@ -9935,7 +9947,9 @@ const LOCALES = {
kanban_include_archived: '包含已归档',
kanban_no_matching_tasks: '没有匹配的任务',
kanban_no_data: '无看板数据',
kanban_work_queue_hint: '这是 Hermes Agent 的工作队列。创建或分类任务,分配任务,将其移至“就绪”状态,然后让调度员认领任务。',
kanban_work_queue_hint: '这是 Hermes Agent 的工作队列。创建或分类任务,分配任务,将其移至”就绪”状态,然后让调度员认领任务。',
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
kanban_clear_filters: 'Clear filters',
kanban_unavailable: '看板不可用',
kanban_read_only: '只读视图',
kanban_empty: '空',
@ -11718,6 +11732,8 @@ const LOCALES = {
kanban_no_matching_tasks: '沒有符合的任務',
kanban_no_data: '沒有看板資料',
kanban_work_queue_hint: '這是 Hermes Agent 的工作佇列。建立或分類任務、指派對象、移至 Ready,然後讓調度器認領。',
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
kanban_clear_filters: 'Clear filters',
kanban_unavailable: '看板無法使用',
kanban_read_only: '唯讀檢視',
kanban_empty: '空',
@ -13192,6 +13208,8 @@ const LOCALES = {
kanban_no_matching_tasks: 'No matching tasks',
kanban_no_data: 'No Kanban data',
kanban_work_queue_hint: 'This is the Hermes Agent work queue. Create or triage a task, assign it, move it to Ready, then let the dispatcher claim it.',
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
kanban_clear_filters: 'Clear filters',
kanban_unavailable: 'Kanban unavailable',
kanban_read_only: 'Read-only view',
kanban_empty: 'Empty',
@ -14680,6 +14698,8 @@ const LOCALES = {
kanban_no_matching_tasks: 'No matching tasks',
kanban_no_data: 'No Kanban data',
kanban_work_queue_hint: 'This is the Hermes Agent work queue. Create or triage a task, assign it, move it to Ready, then let the dispatcher claim it.',
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
kanban_clear_filters: 'Clear filters',
kanban_unavailable: 'Kanban unavailable',
kanban_read_only: 'Read-only view',
kanban_empty: 'Empty',
@ -16197,6 +16217,8 @@ const LOCALES = {
kanban_no_matching_tasks: 'Aucune tâche correspondante',
kanban_no_data: 'Aucune donnée Kanban',
kanban_work_queue_hint: 'Il s\'agit de la file d\'attente de travail de l\'agent Hermes. Créez ou triez une tâche, attribuez-la, déplacez-la vers Prêt, puis laissez le répartiteur la réclamer.',
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
kanban_clear_filters: 'Clear filters',
kanban_unavailable: 'Kanban indisponible',
kanban_read_only: 'Vue en lecture seule',
kanban_empty: 'Vide',
@ -17795,6 +17817,8 @@ const LOCALES = {
kanban_no_matching_tasks: 'Eşleşen görev yok',
kanban_no_data: 'Kanban verisi yok',
kanban_work_queue_hint: 'Bu Hermes Ajanının iş kuyruğudur. Bir görev oluşturun veya önceliklendirin, atayın, Hazır\'a taşıyın ve sevk görevlisinin görevi talep etmesine izin verin.',
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
kanban_clear_filters: 'Clear filters',
kanban_unavailable: 'Kanban kullanılamıyor',
kanban_read_only: 'Salt okunur görünüm',
kanban_empty: 'Boş',
@ -19397,6 +19421,8 @@ const LOCALES = {
kanban_no_matching_tasks: 'Brak pasujących zadań',
kanban_no_data: 'Brak danych Kanban',
kanban_work_queue_hint: 'To jest kolejka pracy Agenta Hermes. Utwórz zadanie lub dokonaj triażu, przypisz je, przenieś do statusu "Gotowe", a następnie pozwól dyspozytorowi je pobrać.',
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
kanban_clear_filters: 'Clear filters',
kanban_unavailable: 'Kanban niedostępny',
kanban_read_only: 'Widok tylko do odczytu',
kanban_empty: 'Puste',

View file

@ -1541,7 +1541,7 @@ function _kanbanTaskTitle(task){ return task.title || task.summary || task.id ||
function _kanbanTaskBody(task){ return task.body || task.description || task.prompt || ''; }
function _kanbanTaskMeta(task){
const bits = [];
if (task.assignee) bits.push(task.assignee);
bits.push(task.assignee ? task.assignee : t('kanban_unassigned'));
if (task.tenant) bits.push(task.tenant);
if (task.priority !== undefined && task.priority !== null) bits.push('P' + task.priority);
if (task.comment_count) bits.push('💬 ' + task.comment_count);
@ -1920,10 +1920,20 @@ async function dropKanbanTask(event, status){
_kanbanSuppressNextCardClick();
}
const KANBAN_UNASSIGNED_LANE = '__unassigned__';
function _kanbanLaneKey(task){ return task && task.assignee ? String(task.assignee) : KANBAN_UNASSIGNED_LANE; }
function _kanbanLaneLabel(lane){ return lane === KANBAN_UNASSIGNED_LANE ? t('kanban_unassigned') : lane; }
function _kanbanLaneNames(columns){
const names = new Set();
columns.forEach(col => (col.tasks || []).forEach(task => names.add(task.assignee || t('kanban_unassigned'))));
return Array.from(names).sort((a, b) => String(a).localeCompare(String(b)));
columns.forEach(col => (col.tasks || []).forEach(task => names.add(_kanbanLaneKey(task))));
const assigned = Array.from(names).filter(n => n !== KANBAN_UNASSIGNED_LANE).sort((a, b) => {
if (a === 'default') return -1;
if (b === 'default') return 1;
return String(a).localeCompare(String(b));
});
if (names.has(KANBAN_UNASSIGNED_LANE)) assigned.push(KANBAN_UNASSIGNED_LANE);
return assigned;
}
function _kanbanRenderColumn(col){
@ -1943,9 +1953,10 @@ function _kanbanRenderProfileLanes(columns){
const lanes = _kanbanLaneNames(columns);
if (!lanes.length) return columns.map(_kanbanRenderColumn).join('');
return `<div class="kanban-profile-lanes">${lanes.map(lane => {
const laneCols = columns.map(col => ({...col, tasks: (col.tasks || []).filter(task => (task.assignee || t('kanban_unassigned')) === lane)}));
const laneCols = columns.map(col => ({...col, tasks: (col.tasks || []).filter(task => _kanbanLaneKey(task) === lane)}));
const count = laneCols.reduce((sum, col) => sum + (col.tasks || []).length, 0);
return `<section class="kanban-profile-lane" data-kanban-lane="${esc(lane)}"><header class="kanban-profile-lane-head"><span>${esc(lane)}</span><span class="kanban-count">${count}</span></header><div class="kanban-board kanban-board-in-lane">${laneCols.map(_kanbanRenderColumn).join('')}</div></section>`;
const laneClass = lane === KANBAN_UNASSIGNED_LANE ? ' kanban-profile-lane-unassigned' : '';
return `<section class="kanban-profile-lane${laneClass}" data-kanban-lane="${esc(lane)}"><header class="kanban-profile-lane-head"><span>${esc(_kanbanLaneLabel(lane))}</span><span class="kanban-count">${count}</span></header><div class="kanban-board kanban-board-in-lane">${laneCols.map(_kanbanRenderColumn).join('')}</div></section>`;
}).join('')}</div>`;
}
@ -1953,6 +1964,19 @@ function _kanbanEmptyBoardHtml(){
return `<div class="main-view-empty"><div class="main-view-empty-title">${esc(t('kanban_no_data'))}</div><div class="main-view-empty-sub">${esc(t('kanban_work_queue_hint'))}</div></div>`;
}
function _kanbanHiddenByFiltersHtml(){
return `<div class="main-view-empty"><div class="main-view-empty-title">${esc(t('kanban_tasks_hidden_by_filters'))}</div><div class="main-view-empty-sub"><button class="btn-link" onclick="clearKanbanFilters()">${esc(t('kanban_clear_filters'))}</button></div></div>`;
}
function clearKanbanFilters(){
const s = $('kanbanSearch'); if (s) s.value = '';
const a = $('kanbanAssigneeFilter'); if (a) { a.value = ''; a.dataset.defaultValue = ''; }
const te = $('kanbanTenantFilter'); if (te) { te.value = ''; te.dataset.defaultValue = ''; }
const ai = $('kanbanIncludeArchived'); if (ai) ai.checked = false;
const om = $('kanbanOnlyMine'); if (om) om.checked = false;
loadKanban(true);
}
function _kanbanRenderBoard(){
const board = $('kanbanBoard');
if (!board) return;
@ -1965,7 +1989,8 @@ function _kanbanRenderBoard(){
if ($('kanbanSummary')) $('kanbanSummary').textContent = String(t('kanban_visible_tasks')).replace('{0}', total);
_kanbanRenderSidebar(columns);
if (total === 0) {
board.innerHTML = _kanbanEmptyBoardHtml();
const unfilteredTotal = (_kanbanBoard.columns || []).reduce((n, col) => n + (col.tasks || []).length, 0);
board.innerHTML = unfilteredTotal > 0 ? _kanbanHiddenByFiltersHtml() : _kanbanEmptyBoardHtml();
return;
}
board.innerHTML = _kanbanLanesByProfile ? _kanbanRenderProfileLanes(columns) : columns.map(_kanbanRenderColumn).join('');

View file

@ -6037,6 +6037,7 @@ main.main.showing-insights > #mainInsights{display:flex;overflow-y:auto;}
.kanban-profile-lanes{display:flex;flex-direction:column;gap:18px;min-width:100%}
.kanban-profile-lane{border:1px solid var(--border);border-radius:12px;background:rgba(255,255,255,.02);padding:10px}
.kanban-profile-lane-head{display:flex;justify-content:space-between;align-items:center;color:var(--text);font-size:13px;font-weight:600;margin-bottom:8px}
.kanban-profile-lane-unassigned .kanban-profile-lane-head{border-bottom:1px solid color-mix(in srgb,var(--warning,#f5c542) 45%,var(--border));padding-bottom:6px}
.kanban-board-in-lane{min-height:0;overflow-x:auto}
.kanban-card-topline{display:flex;gap:6px;align-items:center;margin-bottom:4px;font-size:10px;color:var(--muted)}
.kanban-card-id{font-family:var(--mono);opacity:.8}