Commit Graph

43 Commits

Author SHA1 Message Date
Al Zilla 4068162118 fix(desktop): keep SSH Bot Mode agents visible across local clicks
Clicking the local agent left connectionId null, so the roster treated
the registry primary (often an SSH box) as active and dropped its
profiles while inventing a "This device" shadow of default.

Inventory undialed SSH sources with a cached ls of ~/.hermes/profiles
instead of requiring the window to switch onto that machine. Hostile
HERMES_HOME values are rejected before the listing command runs.
2026-08-17 14:32:52 -07:00
Teknium bd9c7d87f0 feat(desktop/bots): add disband (delete) for group chats
Group chats could be created but never deleted — the only way out was
manually ungrouping every member, which still left the shared room log
in plugin storage forever.

The group-chat workspace header now has a trash button behind a
ConfirmDialog. Disband is soft: it clears every member's group
assignment (syncs cross-machine via ui_meta), drops the room log from
the atom and the persisted group-chats map, clears the needs-you badge,
and closes the room view. The members' per-group gateway sessions
("Group: <name>") are intentionally kept and remain reachable from each
bot's session browser. A room with a drive still in flight leaves a
runtime-only epoch-bumped tombstone so the round-robin loop bails at its
next member boundary; the tombstone is never persisted.
2026-08-17 14:32:11 -07:00
Teknium b779fbf423 feat(desktop): route the bots face clock through the SDK budgeted loop
Exports createBudgetedLoop (+types) through the plugin SDK and migrates the
Bots plugin's face-animation clock onto it. The hand-rolled clock only
checked document.hidden; via the shared loop it now also pauses while the
window is minimized or unfocused, matching every other desktop render loop.

- sdk/index.ts: export createBudgetedLoop/BudgetedLoop/BudgetedLoopOptions
  from @/lib/budgeted-loop with plugin-facing guidance
- hermes-bots/plugin.js: startFaceClock delegates scheduling to the SDK loop
  when present (fps 15, idleWhen = no visible faces); paint body, IO-based
  visibility tracking, and the 1Hz rescan are shared; the hand-rolled rAF
  path remains as a feature-detected fallback for older desktops, per the
  plugin's established SkillsView/McpTab pattern
- tests: new SDK-path case (fps/idleWhen wiring, visibility wake, re-entry
  wake, dispose-on-stop) driven through an injected fake loop; verified to
  fail with the SDK branch removed; existing fallback-path tests unchanged
2026-08-17 13:54:53 -07:00
Teknium 187e5b2b95 fix(desktop): stop phantom local default on remote-gateway desktops; keep the main agent named Hermes
Follow-up to #88523/#88542 from a community report (main agent listed twice,
both rows unnamed @default handles). Two distinct bugs, both specific to
remote-gateway-primary desktops:

1. Phantom "This device" default (electron): the roster enumeration dialed
   ensureRegistryBackend for the registry's local entry unconditionally. On a
   remote-primary desktop that forces resolveRegistryLocalRoute into the
   forced-local branch — SPAWNING a local backend the user never asked for.
   That backend enumerates a `default` profile, so a second default agent
   appears AND the duplicate-handle rule forces -device suffixes onto the
   real one. New shouldDeferLocalEnumeration() treats the forced-local route
   as connect-on-demand (same courtesy as undialed SSH sources): the local
   entry only enumerates when it is the delegate route (local-primary
   desktops, byte-identical behavior) or a forced-local child is already
   pooled (the user opened one).

2. Main agent renamed to a connection label (plugin): displayName keyed the
   "show the connection label for a default row" rule off sourceScoped —
   which annotation also sets on ACTIVE-source rows. A remote-gateway user's
   main agent rendered as an IP-derived label (or bare handle) instead of
   "Hermes"/their title. Key it off remoteSource: only THIN rows from
   another source trade the friendly name for their source label.

Tests: shouldDeferLocalEnumeration route matrix (delegate always enumerates;
forced-local defers until a conn:local:: child exists; bare-key remote
descriptor doesn't count), displayName regression (active default stays
Hermes/title; thin remote default still shows its source label).
2026-08-17 11:33:05 -07:00
Teknium 577f5cf207 fix(desktop): route Bot Mode group-chat Markdown through the plugin SDK
Follow-up to the cherry-picked fix for #88391: the bare `streamdown` import
broke the plugin's side-load contract (vm-harness tests and the legacy-sdk
tmpdir loader can't resolve bare specifiers, and the runtime plugin door only
maps @hermes/plugin-sdk and react). Export Streamdown from the SDK instead,
feature-detect it in plugin.js like SkillsView/McpTab (plain-text fallback on
older desktops), and fix the indentation at the render site.
2026-08-17 11:29:47 -07:00
negroni334 cf2d28a2ff fix(desktop): render Markdown in Bot Mode group chat messages (fixes #88391) 2026-08-17 11:29:47 -07:00
David Dudok de Wit ed20a6f01a fix(desktop): preserve Bot Mode source routing 2026-08-17 10:58:48 -07:00
Teknium ce1f5dd30d fix(desktop): park the bot face clock when idle and tear it down on plugin disable
Follow-up to the salvaged #88219 visibility/15fps work:

- Dormancy: the rAF loop stops scheduling frames when no faces are mounted
  or none are visible, instead of running the 1Hz whole-document shadow-root
  scan forever. A mounting BotFace or a face scrolling into view wakes it.
- Teardown: register() now hooks ctx.onDispose so disabling the Bots plugin
  (or a hot reload) cancels the animation frame, disconnects the
  IntersectionObserver, drops cached nodes, and clears window.__hbFaceClock.
  Previously the loop ran until app restart even with the plugin disabled.
- Behavioral tests for park/wake/stop via a vm-extracted clock harness,
  verified to fail against the pre-fix source.
2026-08-17 10:58:45 -07:00
digitalbase ba2fb191c6 fix(desktop): bound bot face animation work 2026-08-17 10:58:45 -07:00
Teknium ece487ceee fix(bot-mode): live active-connection id for roster classification + source-qualified row keys
Layers on the salvaged #88489 (@29206394) and #88341 (@frizikk):

- sdk: host.activeConnectionId() — registry id of the LIVE active gateway.
  The salvaged fix classifies against the registry primary; after the user
  activates a non-primary source's agent, profiles.list answers from THAT
  source and primary-based matching would duplicate the active source's
  agents again. Live id wins, primaryConnectionId is the fallback, the
  legacy kind==='local' rule covers older desktops.
- plugin: roster/chip/picker list keys are botRowKey(bot) — source-qualified
  (connectionId, name) — so same-named agents on two genuine sources can
  never collide as duplicate React keys (the render half of the dupe-bots
  smear: name-keyed rows + duplicate names = repeated blocks every poll).
  Annotated active-source rows keep the plain-name key, so nothing remounts
  when a desktop gains the union roster.
- tests: live-id-beats-primary regression, botRowKey stability, source-shape
  anchor refresh.
2026-08-17 10:02:32 -07:00
Jakub Wolniewicz 7dd945b920 fix(desktop): deduplicate Bots roster and reuse canonical chats 2026-08-17 10:02:32 -07:00
netease 185209242e fix(desktop): stop duplicating active-gateway bots in the multi-source roster
The union agent roster (host.agents) enumerates EVERY registered connection,
including the active gateway that already answered profiles.list. The plugin
merger treated the active gateway's own agents as rows from other sources
because a remote-primary desktop reports them with connectionKind 'remote',
so every bot appeared twice (baseline) and kept growing with each refetch.

Match union agents to the active gateway via the new primaryConnectionId
field on the roster RPC response and annotate the local rows in place
instead of appending phantom copies. Same-named profiles on genuinely
separate sources (This device, other remotes) still get their own tagged
rows, preserving the @name-device disambiguation rule.

Fall back to the legacy connectionKind==='local' rule when
primaryConnectionId is absent (older Electron builds), so single-source
behavior is byte-identical.

Fixes #88344
2026-08-17 10:02:32 -07:00
Teknium 1a74e7fb43 feat(desktop): offer Bot Mode agent handles in the composer @ autocomplete
The @ popover only completed filesystem references; bot handles worked
when fully typed (mention middleware parses at submit) but were never
offered, so users had to know the exact handle — worse with multi-source
@name-device handles. Fixes #88060 (ported from Hermes-Bot-Mode#43).

- composer contrib: new 'composer.atCompletions' data area
  (ComposerAtCompletionSource) — contributed rows merge AHEAD of path
  results; a throwing source drops its rows, never the popover
- use-at-completions: merge contributed entries in all three fetch paths
  (gateway results, gateway-less, fetch error)
- SDK: export the new area + types for plugins
- bundled Bot Mode plugin: registers 'mention-completions' — roster
  handles from the query cache (\u22645s stale), active profile excluded,
  'default' offered as @hermes, multi-source @name-device handles via
  botHandle, display name + connection label in the row meta, capped at 8
- registered early in register(ctx) so vm harnesses reach it before the
  pane/UI registrations that stubs can't fully model
2026-08-17 01:43:04 -07:00
Teknium 72785b2657 feat(desktop): Discord-style group chat creation in Bot Mode
The Bots pane header + is now a dropdown (New Agent / New Group Chat).
New Group Chat opens a checkbox-picker modal: searchable roster list,
member cap at GROUP_CHAT_MAX_MEMBERS, group-name input that defaults to
the selected members' names, and a Create button that assigns the
existing per-bot group meta field - so the room rides the ui_meta sync
path unchanged and the user lands directly in the new room.
2026-08-17 01:36:03 -07:00
Teknium 9adc900ab2 fix(bot-mode): route bot-to-bot sends through --create-if-missing
The teammate-messaging protocol told bots to send with
`chat -c "Bot Chat" -Q -q` and, on 'No session found', fall back to a
manual two-step (send without -c, then sessions rename) — a dance the
CLI already made unnecessary when --create-if-missing landed (#86794).
Profiles that never went through the Bots-panel birth flow (CLI-created,
pre-Bot-Mode, remote-source) hit that miss on every first contact, and
background sends swallowed the error entirely (the original silent-drop
in Hermes-Bot-Mode#48 / #88059).

- tools/bot_mode_probe.py: protocol command gains --create-if-missing
- bundled plugin: Bot Chat prompt section + @mention handoff note use
  the flag; rename-dance instructions deleted

The capability epoch hashes the protocol section, so existing eternal
Bot Chat sessions pick the new instructions up on their next message via
the established once-per-change rebuild — no per-turn cache drift.

Live-verified: fresh profile with zero sessions, protocol command
created 'Bot Chat' and delivered (PONG round-trip); second send resolved
the same session by title (no duplicate); missing-title send WITHOUT the
flag still errors loudly on stderr.
2026-08-16 23:28:13 -07:00
Teknium 0b13cafffa feat(cron): continuity toggle across dashboard, Bot Mode routines, and TUI cron RPC
Wire the continuity flag through every cron-creation surface, not just the
model tool:

- dashboard (web/): checkbox in the cron job editor; form state round-trips
  the stored reserved 'self' entry into the toggle and strips it from the
  context_from textarea; web_server dashboard validator skips 'self'
  (create precedes the job's existence)
- Bot Mode Routines tab (hermes-bots plugin): Continuity checkbox in the
  New Cronjob dialog, forwarded through cron.manage
- tui_gateway cron.manage RPC: optional continuity param on action=add

vitest cron-job suite 10/10 (4 new), tsc app project clean, py_compile clean.
2026-08-16 22:09:28 -07:00
Teknium a36583e311 fix(desktop): keep cloud bot avatar eye catchlights inside the eyes
The white catchlight dots in BotFace were static circles pinned at the
circle-face eye line (cy 16.5), while the animation clock moves the
pupils to the shape-aware eye line (cy 22 for the cloud). On the cloud
avatar the highlights floated above the eyes instead of inside them.

- Tag the catchlights (data-hb-hl-l/r) and move them with the pupils in
  paintMathFace, offset upper-left of each pupil center.
- Render the initial eyes/catchlights/shut-lids at the shape-aware eye
  line so the first frame matches the animated frames.
2026-08-16 20:51:58 -07:00
Teknium 8236b41771 feat: sync bundled Bot Mode with multi-source roster (Hermes-Bot-Mode#68)
Pulls the multi-source roster into the bundled plugin: profiles.list rows
from the active gateway are merged with the host.agents() union roster
(hermes-agent #86875), so the Bots panel shows agents from every registered
Desktop connection with @name-device handles for duplicates. Feature-detected
and best-effort — an older Desktop build or roster failure leaves the
single-source list untouched.

Adapted for the bundle:
- useRoster queryFn combines the bot_mode_protocol capability read (which
  landed after #68 was cut) with the multi-source merge
- multi-source-roster tests updated for the namespace SDK import harness
- soul-protocol-backfill anchor widened for the new botHandle(name, bot)
  signature

Plugin suite: 143/143.
2026-08-16 18:30:53 -07:00
Teknium 400400e1d9 fix(hermes-bots): composeSoul honors the bot_mode_protocol capability
Found in live desktop E2E: the generated-identity path of composeSoul
still appended the protocol section even when the backend injects it
into the system prompt. New agents now get a clean identity-only SOUL
against capable backends; older gateways keep the append. Covered in
the capability-suppression test.
2026-08-16 18:30:53 -07:00
Teknium d516496cef fix: track bundled plugin.js sources past the tsc-artifact gitignore
apps/desktop/src/**/*.js is gitignored (stale tsc output shadows .tsx),
which silently dropped the hermes-bots plugin.js from the adoption
commit — tests shipped, source didn't, CI ENOENT'd. Negate the pattern
for src/plugins/*/plugin.js: adopted plain-ESM plugins have no .tsx
sibling, so the shadow hazard cannot apply.
2026-08-16 18:30:53 -07:00
Teknium 366d8814b8 feat(desktop): bundle Bot Mode (hermes-bots) as a built-in, default-on plugin
Adopts the Hermes-Bot-Mode desktop plugin (NousResearch/Hermes-Bot-Mode)
into apps/desktop/src/plugins/hermes-bots/, registered by the bundled
vite glob and ON by default. It stays a pure @hermes/plugin-sdk consumer
in plain-ESM plugin.js form; users disable it live in Settings > Plugins.

- contrib/plugins.ts: bundled glob accepts plugin.js entries
- contrib/runtime-loader.ts: a disk/runtime copy of an id that ships
  bundled is skipped (standalone installs predating adoption cannot
  double-register)
- package.json: check:test:plugins runs the plugin's node:test suite in
  CI (138 tests)
- source: Hermes-Bot-Mode @ c19baba, incl. today's #107/#103/#99 merges
2026-08-16 18:30:53 -07:00
Adolanium 0a9337d2bb feat(desktop): expose busy turn flags on plugin SDK
Plugins can now read host.state.busy and host.state.awaitingResponse
for the focused chat. These follow the same session slice the chat pane
uses, so a draft falls back to the global flags and a background turn
does not leak.
2026-08-16 02:44:54 -07:00
273-B_L0 34f484542e fix(desktop): show platform Kanban modifier 2026-08-15 01:04:06 -07:00
Brooklyn Nicholson 44790bc9c8 feat(desktop): plugin descriptions + open the agent plugins folder
HermesPlugin/PluginRecord gain a description one-liner (kanban gets
one) shown in the inventory instead of the raw file path, and the
agent plugins section can open the backend's plugins dir — path from
config.get profile so it's profile-aware, local backends only since
openDir mkdir-creates.
2026-08-08 17:34:20 -05:00
Brooklyn Nicholson 6f1072c83c fix(desktop): drop the gateway-pill dogfood plugin
It was a 1:1 rebuild of the core statusbar gateway item and shipped
enabled by default, so the pill showed up twice. Core chrome stays in
shell; demos that clone it belong in hermes-example-plugins.
2026-08-06 22:29:33 -05:00
hermes-seaeye[bot] 777512b760 fmt(js): npm run fix on merge (#76498)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-02 01:23:45 +00:00
brooklyn! 97971643ab Merge pull request #76417 from NousResearch/bb/kanban-model-picker
Pick a kanban task's model and thinking depth from the board
2026-08-01 16:55:01 -05:00
Brooklyn Nicholson 602fc5f9f5 feat(kanban): pick a task's model and thinking depth from the board
The desktop board had no model control at all: a task ran whatever the profile
you assigned it to happened to be configured with, and the only way to point
one task elsewhere was `hermes kanban set-model` or the browser dashboard's
flat provider:model select.

Adds a Model row to New Task and to the task drawer, rendering the composer's
own picker via the SDK — same search, same provider groups, same submenu — so
the board and the chat bar cannot drift. Unset reads "Profile default" and
changes nothing; a pin reads "provider: model · High" with an inline clear.

Presets are read-only here: picking a model seeds the depth from what you last
used for it, but a per-task choice never rewrites what the composer opens at.
Fast mode is omitted rather than shown-and-ignored — it's a live-session
request parameter with no worker-spawn equivalent.

The New Task dialog opts out of DialogContent's clip: the dialog publishes
itself as the portal container for popovers opened inside it, so its
overflow-y-auto cropped the menu at the dialog's edge. The general fix is in
flight as #75600; this override is scoped to one dialog to avoid conflicting
with it and disappears when that lands.
2026-08-01 16:10:25 -05:00
Brooklyn Nicholson 9175b05b40 fix(desktop): ship example plugin off by default
Match kanban — inventoriable in Settings ▸ Plugins, no statusbar chrome until opted in.
2026-08-01 15:46:36 -05:00
Brooklyn Nicholson 15b0b95413 refactor(desktop): one edge-faded scroller for the whole app
The kanban drawer had grown the only edge-aware masked scroller in the
tree, and the next surface that wants one would have copied it. Lift it
to components/ui as FadeScroll, export it on the plugin SDK, and leave
kanban's ScrollFade as a name its call sites already pass `max` to.

The mask math comes out as two pure functions. jsdom's CSS parser drops
any gradient containing calc(), so a rendered mask-image can't be read
back off the style attribute -- edgeMask/scrollEdges are testable for
real where the component's inline style is not.
2026-08-01 01:23:59 -05:00
hermes-seaeye[bot] 17e5f7244a fmt(js): npm run fix on merge (#75582)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-31 18:54:27 +00:00
Brooklyn Nicholson 1238efce13 fix(desktop): track kanban lane phase in state, not a mirrored ref
main landed a lint rule banning refs mirrored from reactive values in an
effect — they lag a render and cause stale reads. The lane-collapse
override tracker did exactly that with a counts ref.

Hold the empty/non-empty signature in state instead. React bails out
when it's unchanged, so a poll where no lane's emptiness moved costs no
extra render, and the comparison always sees the current value.
2026-07-30 07:18:45 -05:00
Brooklyn Nicholson 81e196f236 feat(desktop): kanban new-task hotkey — the plugin command pattern
Creating a task was mouse-only: the header button, the empty state, or a
per-column hover +. Kanban now ships a real command, wired the way any
plugin should wire one.

One action id (kanban.newTask) registered into two areas: KEYBINDS_AREA
gives it dispatch plus a rebindable row in the shortcuts panel, and a
palette row whose `action` field points back at the same id so the live
combo renders as its hotkey hint. The handler is route-independent — it
parks the lane in $newTaskLane and navigates, so the page picks the
request up whether it was already mounted or is mounting for the first
time, then clears it so a remount can't reopen a dismissed dialog.

Default is mod+alt+n (⌘⌥N). mod+n and mod+shift+n are core built-ins a
plugin can't shadow; core uses alt only for the mod+alt+1…9 profile
slots, never with a letter, which leaves ⌘⌥<letter> free as the natural
namespace for plugin commands.

Label ships in the plugin's own locale bundles (en/ja/zh/zh-hant) via
ctx.i18n, so it localizes without a core en.ts edit.
2026-07-30 07:18:45 -05:00
Brooklyn Nicholson eaa61d2aa6 i18n(desktop): move kanban to plugin-scoped ctx.i18n (per #67303)
Now that #67303 shipped the plugin-scoped i18n door, the kanban plugin ships
its OWN locale bundles via ctx.i18n.register instead of a core t.kanban
namespace — nothing added to core en.ts/ja/zh/zh-hant/types.ts. useKanban()
binds usePluginI18n('kanban') to the message SHAPE (one tiny generic) so
components keep their typed k.newTask / k.moveTo(label) access unchanged.
2026-07-30 07:18:08 -05:00
Brooklyn Nicholson 77f1e84a34 feat(desktop): plugin ctx.onDispose + self-disposing kanban bindApi
The plugin context only tracked contribution/socket disposers, so a plugin's
other side effects (store subscriptions) leaked across disable/re-enable. Add
ctx.onDispose(fn) — an arbitrary cleanup collected alongside the rest and run
on deactivate. bindApi now returns a disposer (unsubscribes its persisted-atom
listeners, closes the socket, drops the rest handle) and the kanban plugin
registers it via ctx.onDispose, so a toggle leaves nothing behind and never
duplicates listeners. Also DRYs the atom-persistence into one `persist` helper.
2026-07-30 07:18:08 -05:00
Brooklyn Nicholson f6c8f35a4a i18n(desktop): localize the kanban plugin across all four locales
Every user-facing string in the kanban plugin now routes through useI18n
(new t.kanban namespace) instead of hardcoded English — en, ja, zh, zh-hant
in lockstep (typecheck enforces parity). Column labels/help move out of the
COLUMN_META const (visual-only now) into i18n via columnLabel/columnHelp;
LOCKED_COLUMNS/ARC_TITLES/complexity copy likewise. Matches the rest of the
desktop app, which is fully localized.
2026-07-30 07:18:08 -05:00
Brooklyn Nicholson 901205420f feat(kanban): talk to a running worker without a restart
A running worker now polls its comment thread and folds new operator notes
into the live turn via the OUT-OF-BAND steer channel (list_comments_after +
a heartbeat-driven bridge, watermarked so history isn't re-injected and the
worker's own notes are skipped). No block→comment→unblock dance. Desktop's
composer sends notes live ("delivered within a few seconds") with "Requeue
with note" as the restart option and a help tooltip.
2026-07-30 07:18:08 -05:00
Brooklyn Nicholson 346149c4f8 feat(kanban): task effort estimate via the auxiliary model
An "Estimate" action asks the auto-routed auxiliary model for a rough token
count + complexity band (S/M/L) with a one-line rationale — tokens, not
dollars, since providers don't report cost reliably. POST /estimate (typed
title/body, for the create dialog) and POST /tasks/{id}/estimate (existing
cards) share one core. Desktop renders it inline ("~15k tok · Medium") with a
"makes a model call" disclaimer; SDK exports compactNumber.
2026-07-30 07:18:08 -05:00
Brooklyn Nicholson 027ef381a4 feat(kanban): scope boards to a project
Boards gain an optional project_id. When set, the board's default_workdir
mirrors the project's primary repo and every new task inherits the project —
a deterministic worktree + branch per task — unless it names its own. New
GET /projects; board create/patch/list carry project_id + resolved name; the
create dialog defaults its workspace to the board's and allows a per-task
path override. Desktop: "Board settings…" gains a project picker.
2026-07-30 07:18:08 -05:00
Brooklyn Nicholson 9be67b7762 fix(desktop): roomier kanban create-task modal
The create form was cramped at max-w-md with a 60vh scroll cap. Widen to a
responsive w-[min(42rem,94vw)] and raise the scroll cap so the fields breathe.
2026-07-30 07:18:08 -05:00
Brooklyn Nicholson 5c4d1e1ea2 feat(desktop): SDK — useGrabScroll export + dogfood plugin touch-ups 2026-07-30 07:18:08 -05:00
Brooklyn Nicholson 79e7adae2d feat(desktop): Kanban — dashboard-parity board plugin on the SDK
The founding opt-in plugin (defaultEnabled: false): /kanban board + drawer,
live task_events via ctx.socket, ⌘-click bulk ops, auto-nudge dispatch,
collapsible lanes, board switcher, and prose activity — all pure SDK-consumer
work against plugins/kanban/dashboard/plugin_api.py. Backend: /boards totals
count live cards only.
2026-07-30 07:18:08 -05:00
Brooklyn Nicholson 7ed7170960 feat(desktop): plugin manager, runtime loader, and plugins settings 2026-07-13 17:57:53 -04:00