Commit Graph

24051 Commits

Author SHA1 Message Date
Brooklyn Nicholson 5df9cd27ea feat(desktop): let ConfirmDialog carry a secondary action
The worktree removal prompt offers a third way out — hide the lane but leave
the worktree on disk — which is why it was still hand-rolled. One optional
slot between Cancel and Confirm covers it, and it keeps Confirm as the
focused button so Enter still means the destructive action.
2026-08-19 23:52:23 -05:00
Brooklyn Nicholson 1e26c02de6 refactor(desktop): route cron delete and review revert through ConfirmDialog
Both were hand-rolled copies of the shared confirm — same two-button shape,
same busy/close beat — and neither answered Enter. Folding them in drops the
duplication and picks up the focus fix.
2026-08-19 23:52:23 -05:00
Brooklyn Nicholson bb0e9ee95a fix(desktop): confirm dialogs take focus so Enter confirms
The delete-session dialog opted out of Radix's autofocus, which left focus
on the sidebar row that opened it — Enter re-activated the row instead of
confirming, and ConfirmDialog's Enter handler never saw the key.

ConfirmDialog now focuses its own Confirm button on open. The existing Enter
test fired the key at the dialog node, so it passed over the bug; it now
fires at whatever actually holds focus.
2026-08-19 23:52:23 -05:00
Brooklyn Nicholson bfcfdb30d1 test(desktop): prove the status bar keeps its own right-click menu
The unit test covers the primitive contract — the marker survives Radix's
asChild Slot merge. This adds the end-to-end half: mount the real coordinator
next to the real status bar, right-click it, and assert the customize menu
opens while the app fallback stays shut. That is the assertion that fails on
a build where the two halves drift apart, and it holds regardless of how the
ownership marker is spelled.

Drops the hand-stamped DOM fixture that asserted the coordinator honors an
attribute the test itself wrote.

Co-authored-by: huklaa <huklaa@users.noreply.github.com>
2026-08-19 23:51:10 -05:00
aydnOktay 2d6d7c550f fix(desktop): keep Radix context menus when asChild overwrites data-slot
The app-wide context-menu coordinator recognizes surfaces that own a Radix
menu by `[data-slot="context-menu-trigger"]`. Radix `asChild` merges as
mergeProps(slotProps, childProps), so a child that sets its own `data-slot`
wins and the marker never reaches the DOM. The status bar footer is
`data-slot="statusbar"`, so the coordinator swallowed its right-click and
showed the window-verbs fallback instead — leaving every default-hidden
status bar item, the context meter included, unreachable from the UI.

Stamp a dedicated `data-hermes-context-menu-trigger` after `{...props}` on
ContextMenuTrigger and bail on that marker. Any asChild surface with its own
`data-slot` is covered, not just the status bar.
2026-08-19 23:51:10 -05:00
Brooklyn Nicholson b4f978d983 fix(nous): treat "takes no reasoning parameter" as a definitive no
Both the wire path and the picker only consulted the catalog's
`mandatory` flag, so a route the Portal lists as accepting no reasoning
parameter at all still got sent a disable, and still offered a Thinking
toggle in the model picker.

For a route it serves, the aggregator's own catalog outranks the
models.dev inference: `supports_reasoning: false` now suppresses the
disable on the wire and drops reasoning controls from the picker
entirely, so there is no disable left to describe.
2026-08-19 23:28:14 -05:00
Brooklyn Nicholson 9c0cd1add2 fix(nous): make "thinking off" stick on a cold start
Portal reasoning capabilities were held only in memory, so a process that
had not yet fetched them answered "unknown" — and on that answer the Nous
profile drops the disable rather than risk a 400. A short-lived process
(`hermes -p`, a cron job, a freshly booted gateway) is always in that
state, so every one of those runs silently ignored "thinking off" and
billed the user for reasoning they had turned off.

The parsed catalog is now mirrored to `cache/reasoning_caps.json`, keyed
by the URL it came from, and hydrated on a cold lookup without touching
the network. Every picker and pricing fetch already pulls that same
document, so they seed the mirror for free.

The catalog URL itself now resolves through the same ladder as the rest
of the Nous catalog reads (`NOUS_INFERENCE_BASE_URL` → credential base →
production) instead of being pinned to production, which had a staging
profile deciding the reasoning-mandatory question from prod's answers.
Keying the mirror by URL keeps those deployments apart.
2026-08-19 23:28:14 -05:00
Teknium 6851841112 fix(bot-mode): group chat opens as one room pane, not two (#89788)
Opening a Bot Mode group chat painted the room twice — once as a main-window
workspace tab (host.openWorkspace) and once as the in-panel fallback, because
the Bots pane rendered off $groupChatWorkspace alone. Two live panes with
independent drafts drove one shared engine, and the roster disappeared behind
the duplicate.

The in-panel room is the fallback surface, not a second copy: it now renders
only while no main tab owns the group. The selection atom stays set either way
so the roster row still highlights, and desktops without the door — or whose
door throws — keep the in-pane room.

Consolidates #89881, #90274 and #90398, which fixed the same bug.

Closes #89788

Co-authored-by: helix4u <helix4u@users.noreply.github.com>
2026-08-19 23:12:25 -05:00
Brooklyn Nicholson 145cd763ca feat(desktop): drag markdown table columns to resize them
A colgroup of percentages is the only state, so widths never touch the
cells: one <col> per column, table-layout fixed, and the browser does the
rest. A drag moves one seam and the pair either side trade width, so the
table box never changes size mid-drag — no reflow of the message around
it, no scrollbar appearing under the pointer.

Handles are markup inside each <th>; the table listens once and resolves
the grabbed seam from the DOM, so there is no context, no per-column
component, and no index threading. Tables stay in auto layout until they
are resized, and double-clicking a seam hands them back to it — the same
reset gesture the pane sashes use.

On a 43-row table a 40-step drag mutates 78 col[style] attributes and
touches no cell.
2026-08-19 23:10:25 -05:00
Brooklyn Nicholson e361e70b3b feat(desktop): keep markdown table column widths across turns and sessions
A markdown table has no id — it is re-parsed from text on every render, so
any resize state hung off the transcript dies on the next turn. Key the
record by a hash of the header row instead: the same table resolves to the
same key after a re-render, a session switch, or a reload, without the
transcript carrying anything.

Widths are percentages of the table box, never pixels, so a restored table
stays fluid in a narrow pane. The namespace is deliberately disposable —
one key, 64 entries, 7-day expiry, swept on first access. Losing it costs
one drag.
2026-08-19 23:10:25 -05:00
hermes-seaeye[bot] a72c9ca248 fmt(js): npm run fix on merge (#90461)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-20 03:21:47 +00:00
Brooklyn Nicholson d15cd18fa1 feat(desktop): hide the Thinking toggle where a disable is rejected
The picker offered an off switch for every reasoning model, including routes
whose upstream answers a disable with HTTP 400 — so "thinking off" was a
control that could not work. Carry the catalog's mandatory verdict through
model.options as can_disable_reasoning and hide the toggle when it is false.

Effort levels are left alone. The catalog's supported_efforts under-reports
what the Portal serves (z-ai/glm-5.3 publishes max, high, low yet honors
minimal at its lowest thinking), so filtering the scale by it would hide
levels that work.
2026-08-19 22:14:56 -05:00
Brooklyn Nicholson d39a031329 fix(nous): stop dropping "thinking off" on Portal models that can honor it
reasoning: {enabled: false} is the only shape the Portal honors, and the
profile refused to send it for every model. Sending nothing means the
upstream default instead, which on a thinking-first route like
deepseek/deepseek-v4-pro (catalog: default_effort high) is thinking ON — so
turning thinking off kept billing reasoning tokens on every turn.

The blanket omission was over-broad. The Portal only rejects a disable on
reasoning-mandatory routes ("Reasoning is mandatory for this model"), which
its catalog flags per model, so that flag now gates the omission. Models the
catalog can't speak to keep the old behavior rather than risk the 400.

extra_body.thinking, DeepSeek's own disable shape, is not forwarded upstream
by the Portal and is not an option here.
2026-08-19 22:14:56 -05:00
Brooklyn Nicholson 608fa9c7af feat(models): read Nous Portal reasoning capabilities from its catalog
The Portal serves OpenRouter's catalog schema, so the existing parser and
cache-only tri-state contract carry over unchanged. Only the HTTP fetch is
generalized across the two catalogs; each keeps its own cache because they
list different models.

The Portal 403s a catalog read with no User-Agent, so the shared fetch now
sends one.
2026-08-19 22:14:56 -05:00
Teknium aebab05f9e Merge pull request #90313 from NousResearch/feat/keyless-web-search-fallback
feat: web search works keyless on fresh installs (Parallel + Exa free tiers)
2026-08-19 19:47:12 -07:00
Teknium 258410a184 fix(cli): first launch banner shows the seeded skill catalog, not "No skills installed" 2026-08-19 19:44:42 -07:00
Teknium 76bf6c7c40 fix(cli): bare /hatch no longer freezes input with an invisible raw input() prompt 2026-08-19 19:42:31 -07:00
Teknium f4a866b484 fix(cli): /config displays the live agent credential, not the env-var constructor seed 2026-08-19 19:42:07 -07:00
Teknium b035036582 fix(cli): /yolo reports locked-ON under process-frozen YOLO instead of a false OFF 2026-08-19 19:41:59 -07:00
Teknium e73b8519f6 test: pin -z/--oneshot --skills forwarding and partial-success contract 2026-08-19 19:41:52 -07:00
Teknium e7091d5fb7 chore: map contributor email for GarlicGo 2026-08-19 19:41:52 -07:00
GarlicGo 466665282b fix(cli): Fix oneshot skills preload 2026-08-19 19:41:52 -07:00
Jeff Mettel 22f66de638 fix(skills): publish and read the (map, platform) pair under one lock
Review feedback: publishing the map and its platform tag as two separate
global assignments is not atomic. A reader landing between them sees the
NEW map still carrying the OLD tag, and if that stale tag matches its own
platform it accepts the map without rescanning — serving another
platform's disabled-skill view, the leak #14536 closed.

Guard the pair with a module lock. scan_skill_commands publishes both
under it; get_skill_commands resolves its platform first, then reads the
map and tag together under the same lock to make the freshness decision.
Scanning stays outside the lock — it does file I/O and deferred imports,
and concurrent scans are already independent after the local-map change.

get_skill_commands now returns the scan's own completed map rather than
re-reading the global, so a concurrent publish cannot swap the result
between the decision and the return.

Adds a regression test that holds the publish lock and asserts a reader
cannot complete its lookup until it is released.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:41:42 -07:00
kronexoi 4f12cfd98d fix(cli): wire /whoami slash command in classic CLI 2026-08-19 19:41:32 -07:00
Teknium 1fa66f2577 Merge remote-tracking branch 'origin/main' into feat/keyless-web-search-fallback
# Conflicts:
#	website/docs/user-guide/configuration.md
2026-08-19 19:36:12 -07:00
Axmr1 4511ba49dd fix(image_gen/openai-codex): do not save progressive partial frames as finals
Codex Responses streams can emit partial_image_b64 previews without a final
image_generation_call.result. The provider treated any b64 as success and could
let a partial overwrite a coexisting final in the same payload, delivering
smeared intermediates as finished GPT Image 2 outputs.

Request partial_images=0, prefer final over partial in extraction, fail closed
(with one content-agnostic retry) unless source=final, and surface image_source
plus pixel_size for QA.
2026-08-19 19:35:07 -07:00
Teknium d0132b5821 fix(cli): give every wizard free-text prompt arrow-key and Ctrl+A/E editing
Widens #90327's line_input() to the whole bug class: all 46 bare input()
free-text prompt sites across the setup wizards (model_setup_flows, setup,
config, gateway, auth, auth_commands, plugins_cmd, skills_hub, bundles,
setup_whatsapp_cloud, main) now route through line_input(), and the shared
cli_output.prompt() / setup.prompt() helpers do too — so every CLI wizard
gets cursor editing, not just the custom model prompt.

Redirected stdin and missing prompt_toolkit keep builtin input() behavior.
E2E: real PTY with raw escape bytes through line_input, cli_output.prompt,
and setup.prompt (arrows + Ctrl+A/E edit correctly); redirected-stdin
fallback verified.
2026-08-19 19:34:36 -07:00
Gille fa62b22ee7 fix(cli): enable editing in custom model prompt 2026-08-19 19:34:36 -07:00
Teknium 938f41e1cd Merge remote-tracking branch 'origin/main' into feat/keyless-web-search-fallback 2026-08-19 19:32:38 -07:00
Owenz-creator 8408edcfb5 fix(bot-mode): protect ordinary sessions from hide sweep 2026-08-19 19:30:28 -07:00
Teknium d604ba6585 fix(bot-mode): running kanban/tool workers now light the Bots roster (#90268)
Worker sessions are deny-listed out of every conversation list, so a
profile grinding through a 30-minute kanban task read idle ('3 hr ago')
with no ACTIVE NOW entry the entire run.

- tui_gateway/methods_profiles.py: profiles.list rows gain worker_session
  — the newest kanban/tool row (id, source, title, last_active). Workers
  heartbeat last_activity_at every <=60s while running (#72016), so the
  field stays fresh exactly while work is happening. last_session keeps
  its deny-list contract; include_sessions:false omits the field; older
  clients ignore it.
- hermes-bots plugin: workerActiveAt() (150s window, one missed heartbeat
  of slack) feeds ACTIVE NOW, the row pulse dot ('Working on a task right
  now'), and the row age label while a worker runs. Chat semantics are
  untouched when no worker is live.
- Tests: 4 new pytest (real SessionDB on temp HERMES_HOME), 2 new node
  behavior tests; sabotage-verified.

Session-list visibility of workers (the issue's first half) is left as-is
by design — auto-resume and shared lists must keep excluding workers; the
roster signal was the actionable gap.
2026-08-19 19:30:12 -07:00
Teknium 6d1284a073 test(update): deflake the Windows progress self-test (both race directions seen in CI)
test_progress_advances_while_the_orchestrator_blocks raced its subject on
both edges within one hour of PR CI (#90358):

- Run 1: sampled right after the shim URL printed, before the orchestrator
  published its stage — caught the page boot default
  ('Hermes will open once done.' != 'Testing quiet update').
- Run 2 (rerun): with HOLD=4s on a slow runner, the second sample slid past
  the hold and caught the cleared terminal state ('' != 'Testing quiet
  update').

Fix: wait (<=10s) for the published stage to actually land before starting
the 1.5s stability window, and raise the hold to 10s so both samples land
inside it. Same assertions, same contract — just anchored to the event the
test is about instead of wall-clock luck.
2026-08-19 19:29:58 -07:00
Teknium f7d90c9410 refactor: single canonical reasoning-effort vocabulary ends the per-vendor clamp drift
The #89503/#70058/#74295/#87279 bug class kept regenerating because every
transport and provider profile hand-rolled its own effort translation map
(9 sites, 4 distinct policies). New agent/reasoning_effort.py is the single
source of truth:

- EFFORT_LADDER: canonical low->high ordering (superset check against
  VALID_REASONING_EFFORTS pinned by test)
- clamp_effort(): one policy — supported passes verbatim, otherwise nearest
  WEAKER supported level (never escalate, never invert the ladder), floor
  when nothing weaker, 'none' never a degradation target, declared
  vendor-documented overrides win, bespoke names pass through
- declared wire vocabularies as data: OpenAI-compat, Codex Responses,
  xAI (4.6/legacy), Actual relays, Kimi K3/K2, TokenHub, GLM-5.2,
  DeepSeek V4, Ollama Cloud, Meta, Solar

Converted sites (all behavior-preserving except noted):
- chat_completions chokepoint, Kimi + TokenHub paths
- codex transport (backend branches now pick a declared set)
- auxiliary_client Responses path
- hermes_cli.models clamp_reasoning_effort_to_supported -> thin wrapper
- plugins: kimi-coding, zai, opencode-zen, deepseek, ollama-cloud,
  meta-ai, upstage, custom (copilot already routes via the wrapper)

Behavior fixes the shared policy surfaces:
- ollama-cloud/opencode-go 'minimal' now degrades to 'low' instead of
  being dropped (drop left the server default = MORE thinking than asked)

New tests: ladder contract (every configurable level is clamped by every
declared wire set; monotonicity across the full ladder for every set).
2026-08-19 19:29:10 -07:00
Teknium 3d62508240 style: sort sidebar-archive import per perfectionist/sort-imports 2026-08-19 19:25:55 -07:00
Teknium 3e05033275 fix(desktop): deleting an archived session no longer leaves a ghost row that spins forever
Archived rows render from $archivedSessions (their own capped store —
they're excluded from $sessions by design), but removeSession only pruned
$sessions. Deleting from the Archived filter left the row in place; a
click on it resumed a hard-deleted id: resume 404 -> goneSessionVerdict
saw the row still listed -> 'retry' -> unrecoverable spinner.

removeSession now resolves the row from either store, evicts both
optimistically, restores the archived row on RPC failure, and forwards
the archived row's owning profile to deleteSession.
2026-08-19 19:25:55 -07:00
Teknium 26da56fd53 docs: tool provider selection follows the hermes tools pick (post #90317) 2026-08-19 19:25:50 -07:00
Teknium b2ea0f3810 fix(desktop): gateway restart no longer clickable-by-mistake next to reconnect
Community report (X @Cobalt_Peak): Reconnect and Restart gateway in the
statusbar gateway popover rendered the same RefreshCw icon side by side,
so users triggered full gateway restarts when they meant to reconnect.

- Restart now uses a Power icon with a destructive hover tint
- Moved restart to the end of the row, after the system-panel button,
  behind a visual divider separating it from the benign actions
2026-08-19 19:25:37 -07:00
fangliquanflq cfc55d3487 fix(config): remove obsolete cwd warning parameter 2026-08-19 19:24:41 -07:00
fangliquanflq 2cc83543bb test(config): cover unreadable dotenv warning path 2026-08-19 19:24:41 -07:00
fangliquanflq a93f1b2bec fix(config): warn for all deprecated dotenv cwd entries 2026-08-19 19:24:41 -07:00
fangliquanflq 31561e37ed fix(config): read deprecated cwd settings from dotenv 2026-08-19 19:24:41 -07:00
Teknium 5ead089775 fix(desktop): cron panel empty states stop suggesting a broader search when no search is active
Both cron empty states used search-flavored copy unconditionally; a fresh
panel with zero jobs and no query told users 'Try a broader search
query'. Copy now follows the query state, reusing existing i18n keys.
2026-08-19 19:24:32 -07:00
Teknium 20059cbc69 fix(desktop): sidebar search results no longer show raw >>>term<<< FTS markers
The backend's session search wraps matched terms in sqlite snippet()
delimiters '>>>'/'<<<' (hermes_state_search.py). The sidebar rendered the
snippet as plain text via searchResultToSession(), so searching 'foo'
painted rows literally titled '>>>foo<<<'. Strip the markers before the
snippet becomes the row preview.
2026-08-19 19:24:22 -07:00
Teknium 98b6f8676d fix(desktop): Models/Providers settings no longer hang 20s when gh CLI is signed out
/api/model/options probed Copilot auth via `gh auth token` four separate
times per payload build. When gh has no credential store for the backend's
HOME (fresh profile, desktop-spawned backend, CI), each probe blocks its
full 5s subprocess timeout on keyring/D-Bus, so every open of the Desktop
Models or Providers settings page took 20s — past the renderer's 15s IPC
budget, painting 'Error invoking remote method hermes:api: Timed out'.

Fix: cache the gh-CLI probe result (hit or miss) for 5 minutes with an
invalidation hook, feed gh stdin=DEVNULL, and disable gh interactive
prompts/update notifier in the probe env.

Measured on the failing profile: 20.5s -> 5.3s cold (one bounded probe),
0.03s warm.
2026-08-19 19:22:48 -07:00
Jeffrey Quesnelle 612b3633d2 Merge pull request #77915 from bbednarski9/feat/relay-native-plugin-init
feat(relay)!: initialize static/dynamic plugins via native integration, remove opt-in plugin
2026-08-19 22:11:13 -04:00
hermes-seaeye[bot] fab8479aa0 fmt(js): npm run fix on merge (#90408)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-20 01:14:22 +00:00
Brooklyn Nicholson 22b81836d2 style(desktop): prettier 2026-08-19 20:01:40 -05:00
Brooklyn Nicholson 12d438ceb3 fix(desktop): keep the two-argument call shape for session RPCs without a deadline
Threading timeoutMs/signal through requestForSessionProfile and
requestGatewayForProfile handed every session-scoped RPC a trailing
`undefined, undefined`. Only the plugin host bridge actually supplies those,
so the rest of the app's calls changed observed arity for no reason — and the
resume/activate paths assert on the exact call shape.

Forward the deadline args only when the caller set them; the plugin bridge
keeps the full four-argument route it needs.
2026-08-19 20:01:40 -05:00
EndeavorYen 0734cfd319 fix(desktop): keep chrome API home when opening a Bot Chat
Opening a plugin/Bot Mode session is navigation, not a workspace switch.
keepAllProfilesScope (default true) now dials the named backend without
moving $activeGatewayProfile or setApiRequestProfile. Session-owned RPCs
still route to the session owner. Pass false to switch chrome and collapse
the Sessions sidebar.
2026-08-19 20:01:40 -05:00
EndeavorYen 2367b90b9f fix(desktop): keep Sessions workspace when opening a Bot Chat
Bot Mode passed keepAllProfilesScope:false, which re-homed the sidebar
onto the bot profile. That profile forever-chat is hidden, so Sessions
and the roster looked empty. Opening a bot is navigation, not a workspace
switch. Also restore all-profiles when the bot backend is already live.

Related: #89789
2026-08-19 20:01:40 -05:00