From 7d66d30d774e87b49cbe48af20c9904c9befb97e Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 7 May 2026 16:13:27 -0700 Subject: [PATCH] feat(kanban): add tooltips and docs link across dashboard (#21541) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Makes first-time use of the kanban view self-explanatory. Every control that wasn't already labelled now has a `title` tooltip describing what it does, and a `?` icon next to the board switcher opens the kanban docs page in a new tab. Coverage: - BoardSwitcher: board select, + New board button, docs-link icon (both compact and full variants) - BoardToolbar: Search, Tenant, Assignee, Show archived, Nudge dispatcher, Refresh - BulkActionBar: → ready, Complete, Archive, reassign group, Apply, Clear - Column header: hovering the header now surfaces COLUMN_HELP as a tooltip in addition to the visible sub-text; column count also labelled - Card: task id, priority badge, tenant badge, assignee/unassigned, comment count, link count, age timestamp - InlineCreate: assignee, priority, parent-task selectors Closes the community feedback from @CharlieDePew asking for tooltips and a docs link in the kanban view. Relevant docs page: https://hermes-agent.nousresearch.com/docs/user-guide/features/kanban --- plugins/kanban/dashboard/dist/index.js | 81 ++++++++++++++++++++----- plugins/kanban/dashboard/dist/style.css | 26 ++++++++ 2 files changed, 92 insertions(+), 15 deletions(-) diff --git a/plugins/kanban/dashboard/dist/index.js b/plugins/kanban/dashboard/dist/index.js index 9947e26be9..c7eef7fb54 100644 --- a/plugins/kanban/dashboard/dist/index.js +++ b/plugins/kanban/dashboard/dist/index.js @@ -97,6 +97,12 @@ const API = "/api/plugins/kanban"; const MIME_TASK = "text/x-hermes-task"; + // Docs link — surfaced as a `?` icon next to the board switcher and as + // `title=` hints on unlabelled controls. Kept in one place so rebrands or + // path changes are a single edit. + const DOCS_URL = "https://hermes-agent.nousresearch.com/docs/user-guide/features/kanban"; + const DOCS_TUTORIAL_URL = "https://hermes-agent.nousresearch.com/docs/user-guide/features/kanban-tutorial"; + // 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 @@ -1128,6 +1134,20 @@ // Board switcher (multi-project) // ------------------------------------------------------------------------- + // Small `?` affordance next to the board controls. Opens the kanban docs + // page in a new tab so users can look up what any of the widgets mean + // without losing the current board view. + function DocsLink() { + return h("a", { + href: DOCS_URL, + target: "_blank", + rel: "noopener noreferrer", + className: "hermes-kanban-docs-link", + title: "Open Hermes Kanban docs in a new tab", + "aria-label": "Hermes Kanban documentation", + }, "?"); + } + function BoardSwitcher(props) { const list = props.boardList || []; const current = list.find(function (b) { return b.slug === props.board; }); @@ -1152,6 +1172,7 @@ size: "sm", className: "h-7 text-xs", }, "+ New board"), + h(DocsLink, null), ); } @@ -1165,6 +1186,7 @@ value: props.board, className: "h-8 min-w-[220px]", "aria-label": "Switch kanban board", + title: "Boards are independent work streams. Each board has its own tasks, tenants, and assignees.", }, selectChangeHandler(function (v) { if (v) props.onSwitch(v); })), list.map(function (b) { const label = b.total > 0 @@ -1178,10 +1200,12 @@ ), ), h("div", { className: "flex-1" }), + h(DocsLink, null), h(Button, { onClick: props.onNewClick, size: "sm", className: "h-8", + title: "Create a new board. Useful when you want an unrelated work stream (different project, different team, isolated scratch area).", }, "+ New board"), props.board !== "default" ? h(Button, { @@ -1326,7 +1350,8 @@ const tenants = (props.board && props.board.tenants) || []; const assignees = (props.board && props.board.assignees) || []; return h("div", { className: "flex flex-wrap items-end gap-3" }, - h("div", { className: "flex flex-col gap-1" }, + h("div", { className: "flex flex-col gap-1", + title: "Fuzzy-match tasks by id, title, or description. Matches across all columns." }, h(Label, { className: "text-xs text-muted-foreground" }, "Search"), h(Input, { placeholder: "Filter cards…", @@ -1335,7 +1360,8 @@ className: "w-56 h-8", }), ), - h("div", { className: "flex flex-col gap-1" }, + h("div", { className: "flex flex-col gap-1", + title: "Tenants are free-form tags on a task (e.g. customer, project, team). Set them via the task drawer or kanban_create." }, h(Label, { className: "text-xs text-muted-foreground" }, "Tenant"), h(Select, Object.assign({ value: props.tenantFilter, @@ -1347,7 +1373,8 @@ }), ), ), - h("div", { className: "flex flex-col gap-1" }, + h("div", { className: "flex flex-col gap-1", + title: "Filter by assigned Hermes profile. Profiles are the named agent identities that claim and work on tasks." }, h(Label, { className: "text-xs text-muted-foreground" }, "Assignee"), h(Select, Object.assign({ value: props.assigneeFilter, @@ -1359,7 +1386,8 @@ }), ), ), - h("label", { className: "flex items-center gap-2 text-xs" }, + h("label", { className: "flex items-center gap-2 text-xs", + title: "Include archived tasks in the board view. Archived tasks are hidden by default." }, h("input", { type: "checkbox", checked: props.includeArchived, @@ -1380,10 +1408,12 @@ h(Button, { onClick: props.onNudgeDispatch, size: "sm", + title: "Wake the dispatcher to claim ready tasks now instead of waiting for the next tick. Use this after adding tasks if you want them picked up immediately.", }, "Nudge dispatcher"), h(Button, { onClick: props.onRefresh, size: "sm", + title: "Reload the board from the database. The board auto-refreshes on task events; this is for forcing a re-read.", }, "Refresh"), ); } @@ -1400,6 +1430,7 @@ h(Button, { onClick: function () { props.onApply({ status: "ready" }); }, size: "sm", + title: "Move selected tasks to Ready. Ready tasks are picked up by the dispatcher on the next tick.", }, "→ ready"), h(Button, { onClick: function () { @@ -1407,6 +1438,7 @@ `Mark ${props.count} task(s) as done?`); }, size: "sm", + title: "Mark selected tasks as done. Releases any claims and unblocks dependent children. You'll be asked for a completion summary.", }, "Complete"), h(Button, { onClick: function () { @@ -1414,8 +1446,10 @@ `Archive ${props.count} task(s)?`); }, size: "sm", + title: "Archive selected tasks. They disappear from the default board view but remain in the database.", }, "Archive"), - h("div", { className: "hermes-kanban-bulk-reassign" }, + h("div", { className: "hermes-kanban-bulk-reassign", + title: "Reassign selected tasks to a different Hermes profile. Pick a profile (or unassign) and click Apply." }, h(Select, { value: assignee, onChange: function (e) { setAssignee(e.target.value); }, @@ -1435,12 +1469,14 @@ }, disabled: !assignee, size: "sm", + title: "Apply the selected assignee to all selected tasks.", }, "Apply"), ), h("div", { className: "flex-1" }), h(Button, { onClick: props.onClear, size: "sm", + title: "Deselect all tasks and hide this bar.", }, "Clear"), ); } @@ -1521,11 +1557,13 @@ onDragLeave: handleDragLeave, onDrop: handleDrop, }, - h("div", { className: "hermes-kanban-column-header" }, + h("div", { className: "hermes-kanban-column-header", + title: COLUMN_HELP[props.column.name] || "" }, h("span", { className: cn("hermes-kanban-dot", COLUMN_DOT[props.column.name]) }), h("span", { className: "hermes-kanban-column-label" }, COLUMN_LABEL[props.column.name] || props.column.name), - h("span", { className: "hermes-kanban-column-count" }, + h("span", { className: "hermes-kanban-column-count", + title: `${props.column.tasks.length} task${props.column.tasks.length === 1 ? "" : "s"} in this column` }, props.column.tasks.length), h("button", { type: "button", @@ -1652,7 +1690,8 @@ onClick: function (e) { e.stopPropagation(); }, title: "Select for bulk actions", }), - h("span", { className: "hermes-kanban-card-id" }, t.id), + h("span", { className: "hermes-kanban-card-id", + title: `Task id: ${t.id}. Use this id with kanban_show, /kanban show, or hermes kanban show.` }, t.id), t.warnings && t.warnings.count > 0 ? h("span", { className: cn( @@ -1669,10 +1708,12 @@ t.warnings.highest_severity === "error" ? "!!" : "⚠") : null, t.priority > 0 - ? h(Badge, { className: "hermes-kanban-priority" }, `P${t.priority}`) + ? h(Badge, { className: "hermes-kanban-priority", + title: `Priority ${t.priority}. Higher-priority tasks are claimed first by the dispatcher.` }, `P${t.priority}`) : null, t.tenant - ? h(Badge, { variant: "outline", className: "hermes-kanban-tag" }, t.tenant) + ? h(Badge, { variant: "outline", className: "hermes-kanban-tag", + title: `Tenant: ${t.tenant}. Free-form tag for grouping tasks (customer, project, team).` }, t.tenant) : null, progress ? h("span", { @@ -1687,16 +1728,21 @@ h("div", { className: "hermes-kanban-card-title" }, t.title || "(untitled)"), h("div", { className: "hermes-kanban-card-row hermes-kanban-card-meta" }, t.assignee - ? h("span", { className: "hermes-kanban-assignee" }, "@", t.assignee) - : h("span", { className: "hermes-kanban-unassigned" }, "unassigned"), + ? h("span", { className: "hermes-kanban-assignee", + title: `Assigned to Hermes profile @${t.assignee}` }, "@", t.assignee) + : h("span", { className: "hermes-kanban-unassigned", + title: "No profile assigned. The dispatcher will pick one from available profiles when the task is Ready." }, "unassigned"), t.comment_count > 0 - ? h("span", { className: "hermes-kanban-count" }, "💬 ", t.comment_count) + ? h("span", { className: "hermes-kanban-count", + title: `${t.comment_count} comment${t.comment_count === 1 ? "" : "s"} on this task` }, "💬 ", t.comment_count) : null, t.link_counts && (t.link_counts.parents + t.link_counts.children) > 0 - ? h("span", { className: "hermes-kanban-count" }, + ? h("span", { className: "hermes-kanban-count", + title: `${t.link_counts.parents} parent${t.link_counts.parents === 1 ? "" : "s"}, ${t.link_counts.children} child${t.link_counts.children === 1 ? "" : "ren"}. Children stay blocked until their parent is done.` }, "↔ ", t.link_counts.parents + t.link_counts.children) : null, - h("span", { className: "hermes-kanban-ago" }, + h("span", { className: "hermes-kanban-ago", + title: t.created_at ? `Created ${t.created_at}` : "" }, timeAgo ? timeAgo(t.created_at) : ""), ), ), @@ -1777,6 +1823,9 @@ onChange: function (e) { setAssignee(e.target.value); }, placeholder: props.columnName === "triage" ? "specifier" : "assignee", className: "h-7 text-xs flex-1", + title: props.columnName === "triage" + ? "Hermes profile that will spec this task (default: the dispatcher's configured specifier). Leave blank to let the dispatcher pick." + : "Hermes profile to assign. Leave blank and the dispatcher will pick from available profiles when the task is Ready.", }), h(Input, { type: "number", @@ -1784,6 +1833,7 @@ onChange: function (e) { setPriority(e.target.value); }, placeholder: "pri", className: "h-7 text-xs w-16", + title: "Priority. Higher-priority tasks are claimed first by the dispatcher. 0 = default.", }), ), h(Input, { @@ -1815,6 +1865,7 @@ value: parent, onChange: function (e) { setParent(e.target.value); }, className: "h-7 text-xs", + title: "Optional parent task. A child stays blocked in its current column until the parent is marked done.", }, h(SelectOption, { value: "" }, "— no parent —"), (props.allTasks || []).map(function (t) { diff --git a/plugins/kanban/dashboard/dist/style.css b/plugins/kanban/dashboard/dist/style.css index 7ecf2fd61f..0e721ea9d0 100644 --- a/plugins/kanban/dashboard/dist/style.css +++ b/plugins/kanban/dashboard/dist/style.css @@ -891,6 +891,32 @@ display: flex; justify-content: flex-end; padding: 0 0.25rem; + gap: 0.5rem; + align-items: center; +} +.hermes-kanban-docs-link { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.5rem; + height: 1.5rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; + line-height: 1; + color: var(--color-muted-foreground, rgba(180, 180, 200, 0.8)); + background: var(--color-card-subtle, rgba(255, 255, 255, 0.04)); + border: 1px solid var(--color-border, rgba(120, 120, 140, 0.25)); + text-decoration: none; + cursor: help; + transition: color 0.15s, background 0.15s, border-color 0.15s; +} +.hermes-kanban-docs-link:hover, +.hermes-kanban-docs-link:focus-visible { + color: var(--color-foreground, #e7e7ee); + background: var(--color-card, rgba(255, 255, 255, 0.08)); + border-color: var(--color-border, rgba(160, 160, 190, 0.45)); + outline: none; } .hermes-kanban-dialog-backdrop { position: fixed;