3 Commits
Author SHA1 Message Date
nesquena-hermes c71312b2e8 feat(kanban): edit task button, real Run dispatcher, assignee dropdown
Three connected gaps in the Kanban UX, fixed together because they're
load-bearing for the actual work-queue lifecycle:

1. Edit task — the detail view had only status-transition buttons (Triage/
   Todo/Ready/Blocked/Done/Archived) plus Block/Unblock and Add comment.
   No way to edit title, body, assignee, tenant, or priority once the task
   was created. Backend already supported it via PATCH /api/kanban/tasks/<id>
   (api/kanban_bridge.py::_patch_task) — purely a UI gap.

   Now: an Edit button on the task-detail header opens the existing modal
   pre-filled with current values, switches the modal title to 'Edit task'
   and the submit button to 'Save', PATCHes instead of POSTing on submit.

2. Run dispatcher — the existing 'Preview dispatcher' button always passed
   ?dry_run=1 (nudgeKanbanDispatcher), so it was preview-only. There was
   literally no UI button anywhere in the WebUI that actually ran the
   dispatcher to claim Ready tasks and spawn workers. Users had to drop
   to the CLI.

   Now: new runKanbanDispatcher() entry point hits /api/kanban/dispatch
   without dry_run=1, after a showConfirmDialog confirmation because it
   spawns subprocess workers. Two UI surfaces: a lightning-bolt button in
   the board header (visually distinct from the dry-run preview ▶), and
   a primary 'Run dispatcher' button in the sidebar bulk bar next to a
   relabeled 'Preview' button. Toast result shows concrete numbers from
   dispatch_once(): 'Dispatched: 1 spawned, 2 skipped (no assignee)' —
   not just a generic 'OK'.

3. Assignee dropdown — the previous create modal accepted free-text
   assignee with no validation. The dispatcher (kanban_db.py:3567) only
   spawns workers when row['assignee'] is a real Hermes profile name; any
   typo or blank value made the task sit in Ready forever.

   Now: <select> populated from /api/profiles (Hermes profile names) with
   historical board assignees grouped under 'Other (CLI lanes / removed
   profiles)', plus an explicit '— Unassigned (won't auto-run) —' option.
   Default selection is the first profile, not Unassigned. Custom SVG
   chevron so the field reads visually as a dropdown. Helper text under
   the field explains the dispatcher claim contract. Soft warning if user
   explicitly picks Unassigned + Ready ('You picked Unassigned + Ready.
   The dispatcher will skip this task. Submit again to confirm, or pick
   a profile.'); proceeds on second submit.

Side effect: default new-task status changed from triage to ready, since
'ready' is what users want for tasks they intend to actually run. Triage
is still in the dropdown for tasks that need staging review.

i18n: 19 new keys translated across all 8 supported locales.

Tests: 3 new regression tests in tests/test_kanban_ui_static.py:
- test_kanban_task_detail_has_edit_button_and_modal_supports_edit_mode
- test_kanban_assignee_dropdown_uses_select_not_freetext
- test_kanban_run_dispatcher_button_exists_and_is_distinct_from_preview

Verified end-to-end in browser: created board → opened modal with profile
dropdown → created task with assignee=archivist → clicked Edit → changed
all 5 fields → saved → verified persistence → clicked Run dispatcher →
confirm dialog → confirmed → toast 'Dispatched: 1 spawned' → task moved
Ready → Running.

Test suite: 5042 passed, 11 skipped, 3 xpassed, 0 regressions in 151s.
2026-05-09 20:48:28 +00:00
nesquena-hermes a3af4a3c8f fix(profile/mcp): discover MCP tools after per-session HERMES_HOME mutation
Issue #1968: switching to a non-default profile in the WebUI dropdown
had no effect on which MCP servers were available. Every chat session,
regardless of profile, only saw the default profile's mcp_servers from
~/.hermes/config.yaml. Non-default profile MCP servers (postgres, custom
stdio servers, anything in <profile>/config.yaml) never registered.

Root cause: api/streaming.py:1922 called discover_mcp_tools() at the
TOP of _run_agent_streaming(), about 100 lines BEFORE the per-session
'os.environ["HERMES_HOME"] = _profile_home' mutation at line 2053.
discover_mcp_tools() reads ~/.hermes/config.yaml via get_hermes_home(),
which uses os.environ['HERMES_HOME']. So at the call site, HERMES_HOME
was still whatever the WebUI server process had at startup — the default
profile, every time.

Fix: relocate the discover_mcp_tools() call past the _ENV_LOCK block so
get_hermes_home() resolves to the session's actual profile home. Same
try/except wrapping is preserved; same idempotency semantics on
already-connected servers; same lazy-import pattern.

Caveat (out of scope, agent-side): _servers in tools/mcp_tool.py is a
process-global Dict[str, MCPServerTask] keyed only by server name. So
once profile A registers a server named e.g. 'postgres', profile B's
discovery sees 'postgres' as already connected and skips it — even if
B's config points at a different binary or DB. Concurrent multi-profile
WebUI processes will still hit 'first profile wins per server name'.
Fully fixing that requires keying _servers by (profile_home, name)
upstream in hermes-agent. This PR ships layer 1 only — fixes the
single-non-default-profile case (the headline symptom).

Tests: tests/test_issue1968_mcp_profile_discovery.py — 4 static tests
pinning the lexical ordering invariants. Verified mutation-safety: a
proof-of-concept revert (re-adding a discover call before the
HERMES_HOME mutation) makes the 'only called once' test fail.

Test suite: 5047 passed, 4 skipped, 3 xpassed, 0 regressions.

Closes #1968
2026-05-09 20:08:16 +00:00
nesquena-hermes 10ea2a014f fix(kanban): header '+' button opens create-task modal
The Kanban sidebar panel's header '+' button (#kanbanNewTaskBtn) was
wired straight to createKanbanTask(), which reads the inline
#kanbanNewTaskTitle input and silently returns when empty. The inline
input lives below five rows of filters (search, assignee, tenant,
archived/mine toggles, stats, bulk-action bar) and is typically off-screen
on first panel open, so the header button looked dead — clicking it with
no title typed did nothing visible (no modal, no scroll, no focus shift,
no toast).

Now the header '+' opens #kanbanTaskModal — a centered overlay with the
same .kanban-modal-overlay shell the existing create-board modal uses,
so the two flows look and behave identically (centered card, dim
backdrop, ESC closes, click-on-backdrop closes). The modal exposes the
fields the backend already accepts at /api/kanban/tasks: Title, Description,
Status (Triage/Todo/Ready), Priority, Assignee (datalist suggestions from
the active board), Tenant (datalist).

UX details:
- Title is required; submit-with-empty shows a properly styled red error
- Title field auto-focuses on open
- ESC closes the modal; backdrop click closes; Enter on simple inputs
  submits, Enter in the description textarea inserts a newline
- Submit POSTs only the fields the user filled in (no forced empty strings)
  and auto-opens the new task's detail view
- Submit button disables while posting to prevent double-submit
- Inline quick-add (Enter on #kanbanNewTaskTitle) is preserved as a
  power-user shortcut

Side effect: .kanban-modal-error styling improved (proper red alert with
border + tinted background) so the existing create-board modal benefits
from the same polish for free.

i18n: 11 new keys added across all 8 supported locales (en, ja, ru, es,
de, zh, pt, ko).

Tests: tests/test_kanban_ui_static.py::test_kanban_new_task_header_button_opens_modal
covers the modal markup, button wiring, ESC/Enter handling, datalist
population, submit behavior, and inline-quick-add fallthrough.

Verified end-to-end in the browser on an isolated test env (port 8789):
created a board from scratch, opened the modal via header '+',
submitted with title/description/status/priority/assignee/tenant filled in,
moved the task through statuses (Triage → Todo → Ready → Blocked → Archived),
added a comment, verified Cancel + ESC + backdrop-click all close cleanly,
verified validation error rendering, verified inline quick-add still works.

Closes #1964
2026-05-09 19:33:07 +00:00