mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-08 03:01:47 +00:00
feat(kanban): add tooltips and docs link across dashboard (#21541)
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
This commit is contained in:
parent
7f92e5506e
commit
7d66d30d77
2 changed files with 92 additions and 15 deletions
81
plugins/kanban/dashboard/dist/index.js
vendored
81
plugins/kanban/dashboard/dist/index.js
vendored
|
|
@ -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 ``<root>/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) {
|
||||
|
|
|
|||
26
plugins/kanban/dashboard/dist/style.css
vendored
26
plugins/kanban/dashboard/dist/style.css
vendored
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue