/** * Hermes Kanban — Dashboard Plugin * * Board view for the multi-agent collaboration board backed by * ~/.hermes/kanban.db. Calls the plugin's backend at /api/plugins/kanban/ * and tails task_events over a WebSocket for live updates. * * Plain IIFE, no build step. Uses window.__HERMES_PLUGIN_SDK__ for React + * shadcn primitives; HTML5 drag-and-drop for card movement on desktop and * a pointer-based fallback for touch. */ (function () { "use strict"; const SDK = window.__HERMES_PLUGIN_SDK__; if (!SDK) return; const { React } = SDK; const h = React.createElement; const { Card, CardContent, Badge, Button, Input, Label, Select, SelectOption, } = SDK.components; const { useState, useEffect, useCallback, useMemo, useRef } = SDK.hooks; const { cn, timeAgo } = SDK.utils; // Order matches BOARD_COLUMNS in plugin_api.py. const COLUMN_ORDER = ["triage", "todo", "ready", "running", "blocked", "done"]; const COLUMN_LABEL = { triage: "Triage", todo: "Todo", ready: "Ready", running: "In Progress", blocked: "Blocked", done: "Done", archived: "Archived", }; const COLUMN_HELP = { triage: "Raw ideas — a specifier will flesh out the spec", todo: "Waiting on dependencies or unassigned", ready: "Assigned and waiting for a dispatcher tick", running: "Claimed by a worker — in-flight", blocked: "Worker asked for human input", done: "Completed", archived: "Archived", }; const COLUMN_DOT = { triage: "hermes-kanban-dot-triage", todo: "hermes-kanban-dot-todo", ready: "hermes-kanban-dot-ready", running: "hermes-kanban-dot-running", blocked: "hermes-kanban-dot-blocked", done: "hermes-kanban-dot-done", archived: "hermes-kanban-dot-archived", }; const DESTRUCTIVE_TRANSITIONS = { done: "Mark this task as done? The worker's claim is released and dependent children become ready.", archived: "Archive this task? It disappears from the default board view.", blocked: "Mark this task as blocked? The worker's claim is released.", }; // Event kinds that indicate a hallucinated/phantom task-id reference // in a completion. ``completion_blocked_hallucination`` is emitted when // the kernel's ``created_cards`` gate rejects a completion; the task is // left in its prior state and the worker can retry. ``suspected_ // hallucinated_references`` is the advisory prose-scan result — the // completion succeeded but the summary text references task ids that // do not resolve. const HALLUCINATION_EVENT_KINDS = [ "completion_blocked_hallucination", "suspected_hallucinated_references", ]; const HALLUCINATION_EVENT_LABELS = { completion_blocked_hallucination: "Completion blocked — phantom card ids", suspected_hallucinated_references: "Prose referenced phantom card ids", }; function isHallucinationEvent(kind) { return HALLUCINATION_EVENT_KINDS.indexOf(kind) !== -1; } function phantomIdsFromEvent(ev) { // Payload shapes: // completion_blocked_hallucination: {phantom_cards, verified_cards, summary_preview} // suspected_hallucinated_references: {phantom_refs, source} if (!ev || !ev.payload) return []; const p = ev.payload; return p.phantom_cards || p.phantom_refs || []; } function withCompletionSummary(patch, count) { if (!patch || patch.status !== "done") return patch; const label = count && count > 1 ? `${count} selected task(s)` : "this task"; const value = window.prompt( `Completion summary for ${label}. This is stored as the task result.`, "", ); if (value === null) return null; const summary = value.trim(); if (!summary) { window.alert("Completion summary is required before marking a task done."); return null; } return Object.assign({}, patch, { result: summary, summary }); } const API = "/api/plugins/kanban"; const MIME_TASK = "text/x-hermes-task"; // localStorage key for the user's selected board. Independent of the // CLI's on-disk ``/kanban/current`` pointer so browser users // can inspect any board without shifting the CLI's active board out // from under a terminal they left open. const LS_BOARD_KEY = "hermes.kanban.selectedBoard"; function readSelectedBoard() { try { const v = window.localStorage.getItem(LS_BOARD_KEY); return (v || "").trim() || null; } catch (_e) { return null; } } function writeSelectedBoard(slug) { try { if (slug && slug !== "default") window.localStorage.setItem(LS_BOARD_KEY, slug); else window.localStorage.removeItem(LS_BOARD_KEY); } catch (_e) { /* ignore quota / private mode */ } } function withBoard(url, board) { // Append ?board= when a non-default board is active. Omitted // for default so the URL stays clean and the backend falls through // to its own resolution chain (env var → ``current`` file → // default) which is already correct. if (!board || board === "default") return url; const sep = url.indexOf("?") >= 0 ? "&" : "?"; return `${url}${sep}board=${encodeURIComponent(board)}`; } // The SDK's Select component fires ``onValueChange(value)`` directly // (it's a shadcn-style popup, not a native