Commit graph

364 commits

Author SHA1 Message Date
Charles Bine
dd66640fae
feat(#484): configurable default view for JSON/YAML code blocks
The JSON/YAML tree viewer (#484) hardcoded its default: valid json/yaml
fenced blocks opened in Tree view at 10+ lines (`const showTree=lineCount>=10;`).
Make that default user-configurable while preserving current behavior.

- New settings (persisted via /api/settings -> settings.json):
  - structured_code_default_view: auto | on | off  (default auto)
  - structured_code_auto_tree_lines: int 1..1000    (default 10)
- ui.js: replace the hardcode with a sanitized, unit-testable decision
  helper _structuredCodeShowTree(mode, threshold, lineCount); Tree/Raw
  toggle, Raw <pre><code>, parse-failure fallback, and YAML lazy-load
  are unchanged.
- Appearance settings: 'JSON/YAML code blocks' select + 'Auto threshold
  lines' input (disabled outside auto mode); wired through the existing
  appearance-autosave and explicit-save paths; live re-render on change.
- boot.js seeds window globals; i18n labels added to all 13 locales.
- Default (auto + 10) reproduces the original >=10 behavior exactly.
2026-06-23 13:51:45 -04:00
nesquena-hermes
2f250a4198 review(#4741): address greptile P2s — drop redundant lazy import, deep-copy raw config cache
Two greptile findings on #4741:
- api/routes.py: removed the redundant 'from api.config import load_settings'
  inside _session_list_payload_to_response — load_settings is already imported at
  module scope, and this function only runs at request time (after module load).
- api/config.py: _load_yaml_config_file_raw now deep-copies the cached raw dict by
  default so a caller can never mutate the shared _yaml_file_cache entry. The
  internal _load_yaml_config_file passes _copy=False (it immediately runs
  _expand_env_vars, which returns a fresh structure and never mutates its input)
  so the /api/reasoning hot path keeps its cheap no-copy fast path; reload_config
  uses the safe default copy.
2026-06-23 05:43:17 +00:00
nesquena-hermes
001ee0a986 fix(#4662): stamp _cfg_mtime for empty-dict config (Opus gate finding)
The 'and loaded' guard skipped the _cfg_mtime stamp for a config that parses to
an empty {} dict, so get_config()'s current_mtime != _cfg_mtime stale check fired
on every call and re-entered reload_config() under _cfg_lock forever — a hot-path
spin for a freshly created/reset profile's {} config. Split the guard: the no-op
_cfg_cache.update is still skipped for {}, but _cfg_mtime is always stamped when
the file parsed to a dict, matching master's pre-#4662 behavior. Incidentally
fixes the pre-existing empty/None-config spin. Adds a regression test.
2026-06-23 05:43:17 +00:00
nesquena-hermes
8006db22fc perf(#4662): route reload_config parse through the mtime config cache
Phase 2 of #4662. reload_config() did a raw yaml.safe_load of config.yaml on
every call (~125ms+ on a large file), including on the profile-switch hot path
(~446ms load_settings stage in /api/sessions diagnostics). It now parses through
the mtime+size-keyed cache added in #4652, via a shared _load_yaml_config_file_raw
core (DRY: _load_yaml_config_file = raw + env-expand). reload_config still runs
its OWN env expansion pinned to the unscoped process-env view (block_process_env_fallback
reset + _thread_ctx.env={}) so the #798 TLS profile-isolation invariant is byte-for-byte
preserved; only the disk parse is now cached. An unchanged config.yaml is no longer
reparsed; an mtime change still busts the cache.
2026-06-23 05:42:32 +00:00
nesquena-hermes
d2ee837796 fix(#3961): resolve Opus ship-gate findings (startup NameError + reload cache poisoning)
Opus final gate reproduced two issues in the config-expansion hardening:

MUST-FIX — import-order forward reference. _expand_env_vars (config.py top) calls
_thread_local_env_value, but that fn + _thread_ctx were defined ~6900 lines later,
AFTER the import-time reload_config(). A ${VAR} in a startup config.yaml raised a
(swallowed) NameError, leaving import-time config consumers with empty config.
Fix: relocate _thread_ctx + _thread_local_env_value above the config-file section
(they depend only on os + threading). Verified: startup ${VAR} now expands, no
NameError.

SHOULD-FIX — reload_config baked block-scoped expansion into the process-global
_cfg_cache. If a reload fired while a readonly/worker scope was active (profile
alternation resolves _get_config_path to the named profile), under-expanded
literal ${VAR}s were written to the shared cache, starving concurrent readers of
the `cfg` alias. Fix: reload_config now expands the global cache with the
block flag forced off (process-env view), since the cache is process-global;
per-request scoped reads still go through _thread_local_env_value at their own
sites. Verified: global cache holds the process-expanded value, not the literal.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-22 20:18:01 +00:00
nesquena-hermes
51dd69b4f2 fix(#3961): route config ${VAR} expansion through the profile-aware env lookup
Codex re-gate found a config-template leak vector distinct from the env-name
scrub: _expand_env_vars() expanded ${VAR} references in config values via raw
os.environ, so a profile config.yaml of e.g. `api_key: ${ANTHROPIC_TOKEN}` let
_get_provider_api_key reconstruct the server-process token and pass it through as
the API key argument — even when the child-env scrub stripped the env var. This
bypassed the thread-local channel entirely.

_expand_env_vars now substitutes via _thread_local_env_value (the same
thread-local-first accessor with the block_process_env_fallback guard the PR
introduced): a ${VAR} resolves to the profile's own value, and refuses the
process-env fallback when a profile-scoped readonly/background scope set the block
flag (leaving the reference unexpanded). Cache integrity preserved — only the RAW
unexpanded dict is cached; expansion re-runs per read against the current thread's
scope. Normal (no-scope) behavior unchanged. + regression that fails without the fix.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-22 19:49:44 +00:00
nesquena-hermes
239529f9bd stage #4544 (rodboev): route provider/model credential reads through thread-local profile channel (fixes #3961) 2026-06-22 18:47:59 +00:00
nesquena-hermes
1f4e70a5d4
stage #4652 (nesquena-hermes): stop /api/reasoning storm + memoize YAML config parse (fixes #4650) (#4659)
Two-layer perf fix for the /api/reasoning storm dropping streaming to ~2 tok/s:
- ui.js: syncReasoningChip() serves the cached chip on routine topbar syncs, only
  re-fetching when the model/provider key (_lastReasoningFetchKey) changes — restores
  pre-a9ce2889 behavior while keeping its model-switch-refresh intent.
- config.py: memoize _load_yaml_config_file keyed on (path, st_mtime_ns, st_size) under
  a lock; cache the RAW parsed dict and re-run cheap _expand_env_vars per call (cache stays
  correct vs os.environ and read-modify-save callers get a fresh structure; on-disk edits
  picked up via mtime/size). Code byte-faithful from PR head f868f10a1ad7. 9 own tests pass.

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-06-21 21:45:13 -07:00
nesquena-hermes
b575aaad1e
absorb #3177 (RobinAngele): global profile switcher in titlebar — gated behind opt-in setting (#4648)
Rebased RobinAngele's titlebar profile-switcher onto current master (resolved
index.html + panels.js conflicts from ~1300 commits of drift). Added a Settings ->
Appearance opt-in toggle 'show_titlebar_profile' (default OFF) per Nathan: the
titlebar button is hidden unless explicitly enabled, so default behavior is
unchanged (composer footer keeps its profile switcher). Wired config bool key +
boot.js apply (_applyTitlebarProfileVisibility) + panels.js load/save + index.html
checkbox + 2 i18n keys across all 13 locales (zh-Hant real translation).
Live-verified: default OFF = titlebar empty; opt-in ON = button appears + persists.

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: RobinAngele <RobinAngele@users.noreply.github.com>
2026-06-21 18:56:31 -07:00
nesquena-hermes
b549c004cb
stage #4598 (Paladin173): composer footer control visibility toggles + rebase + contract test (#4630)
Per-control show/hide for composer footer controls (primary vs situational groups),
per-profile persist + live-apply. Visibility-only — footer layout unchanged.
nesquena-hermes deep-reviewed safe (Codex+Opus+suite 9930). Rebased onto master:
resolved api/config.py boolean-keys conflict (kept both auth_disabled_acknowledged +
the 15 hide_composer_* keys) and panels.js settings-load conflict (kept both
render_user_markdown wiring + composer-control chip render). Contributor closed the
en-only-i18n CI blocker (19 keys x 13 locales verified). Added the missing static-
contract test (sibling pattern test_sidebar_tab_visibility.py).

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: Paladin173 <Paladin173@users.noreply.github.com>
2026-06-21 12:42:42 -07:00
nesquena-hermes
8ea7ad582e rebase #3581 onto master (v0.51.546) for review+ship-gate — require current password before disabling auth
starship-s: sudo-mode re-auth on POST /api/settings before any change/clear/passwordless transition
of password auth, + cosmetic auth-disabled-acknowledged nav badge. Operator-hardening (rubric-named).
Security-boundary + visible UI -> Nathan visual sign-off. Co-authored-by: starship-s <starship-s@users.noreply.github.com>
2026-06-21 02:26:20 +00:00
nesquena-hermes
6b2ac55b8d stage #4566 + stamp v0.51.546 — fix flaky gateway_sync test (#4557)
rodboev: settings-write-version counter widens the session-list cache key + flush CLI-sessions
cache on visibility toggle, killing the mtime_ns-bucket collision flake. Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-21 02:04:09 +00:00
nesquena-hermes
2471e277cb rebase #3933 + stamp v0.51.544 — opt-in render markdown in user messages
rodboev. Default-off; OFF path byte-identical to master (cache key 'u' + _renderUserFencedBlocks
unchanged when window._renderUserMarkdown falsy). ON routes user text through the same renderMd
SAFE_TAGS/_isSafeUrl sanitizer as assistant msgs. Nathan override-approved (likes the idea, opt-in).
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-21 00:03:02 +00:00
Rod Boev
c237946ea3 fix(#3961): keep mirrored worker env updates and pool helpers compatible 2026-06-20 20:01:58 -04:00
Rod Boev
62ff54d5da fix(#3961): preserve live pool entry state outside readonly scopes 2026-06-20 19:58:04 -04:00
Rod Boev
44437f7a26 fix(#3961): fail closed for profile-scoped credential pool reads 2026-06-20 19:52:52 -04:00
nesquena-hermes
acd57d847d rebase #4413 re-pushed head (553e9584) for re-gate
Converged both bounce items: enrich-existing-only (continue on webui_list-is-None,
no new providers -> routing side-effect gone, routes.py no longer touched) + respects
per-provider ID prefix convention (@nous:). Co-authored-by: kaishi00 <kaishi00@users.noreply.github.com>
2026-06-20 21:22:53 +00:00
nesquena-hermes
85e3be6a42 fix(#4555): preserve /api/models aliases contract on disk-cache fallback
Codex ship-gate (SILENT): both disk loaders dropped `aliases` (the save path
never persisted them), so a disk-cache hit — strict cold-path OR the new stale
fallback — served /api/models with empty aliases, silently breaking `/model
<alias>` slash-command resolution (static/commands.js resolves slash aliases
only from /api/models.aliases). Add _model_aliases_from_config() helper and
reconstruct aliases from current config in BOTH loaders (fixes the pre-existing
strict-loader gap too). Adds a regression test.

Co-authored-by: starship-s <starship-s@users.noreply.github.com>
2026-06-20 19:25:51 +00:00
nesquena-hermes
fbafe1e623 harden(#4555): isolate stale-cache path in test fixture + enforce schema-version equality on stale fallback
Applies Opus ship-gate findings: (1) the autouse fixture now points
_get_models_cache_path at an isolated temp file so the over-budget static-fallback
test can't read a real ~/.../models_cache.json and become an order-dependent flake;
(2) _load_stale_models_cache_from_disk rejects a cross-schema cache (its groups/badge
shape may be incompatible with the current picker). Adds a cross-schema-rejection test.

Co-authored-by: starship-s <starship-s@users.noreply.github.com>
2026-06-20 19:19:52 +00:00
Hermes Agent
3d8f3a1f7b fix(models): use stale disk cache on catalog timeout 2026-06-20 19:11:17 +00:00
MinhoJJang
630fd26eec fix configured provider slash model routing 2026-06-20 17:42:18 +00:00
Rod Boev
7a0d868635 Keep profile-scoped reads from falling back to server credentials 2026-06-20 03:37:35 -04:00
Rod Boev
b59f58601e fix(#3961): block process-env fallback in readonly profile scopes 2026-06-20 02:48:24 -04:00
Rod Boev
321090efcc fix(#3961): preserve mirrored live-model profile reads 2026-06-20 02:33:54 -04:00
Rod Boev
846951304d fix(#3961): route provider and model credential reads through the thread-local profile channel 2026-06-20 02:24:07 -04:00
nesquena-hermes
e1e2bbc7ef fix(config): order ghost-home guard after ambient short-circuit (#4516 Codex gate)
Codex SILENT finding: the new 'if not target.exists(): return {}' guard ran
BEFORE the ambient-resolver short-circuit, so a matching ambient profile home
whose directory does not physically exist (fresh install / monkeypatched cfg)
returned {} instead of deferring to get_config() — breaking in-memory overrides
and changing the default-profile missing-home path. Move the guard below the
ambient short-circuit; add a regression test (matching-but-nonexistent ambient
home still defers to get_config()).
2026-06-20 03:57:56 +00:00
nesquena-hermes
acd2c186ef stage #4516: apply config defaults to non-default profile reads (#4465 / #4513) + v0.51.532 CHANGELOG 2026-06-20 03:51:55 +00:00
Rod Boev
f086158c10 fix(#2698): keep isolated deployments on their own state and profile roots 2026-06-20 00:48:19 +00:00
nesquena-hermes
9ade727e63 stage #4494: expose built-in personalities in WebUI config (#4465) + v0.51.525 CHANGELOG 2026-06-19 22:26:30 +00:00
nesquena-hermes
5a8d24c936 fix(#4343): default transcript virtualization OFF (experimental/opt-in) + force-off migration
Virtualization (#500/#4214) caused a scroll-up flicker on long sessions with
tall tool-call rows (variable-height anchor oscillation), distinct from the
stream-completion jump fixed in #4325. Per maintainer decision, flip the
'Virtualize long transcripts' preference to experimental/opt-IN (default OFF)
for everyone until the scroll behavior is reliable.

- api/config.py: virtualize_transcript default True->False; add
  virtualize_transcript_optin marker (both bool-allowlisted); load_settings
  force-off migration — a stored true WITHOUT the post-flip opt-in marker is
  reset to off, so 100% of existing users land on off; an explicit opt-in
  (marker present) is honored.
- boot.js / panels.js: default-off semantics (===true), persist the opt-in
  marker on enable, checkbox not pre-checked.
- index.html / i18n.js (en): relabel experimental + flicker warning.
- Tests: rewrite the #4325 toggle assertions for the opt-in contract + 4 new
  migration tests (unset/stale-true/explicit-optin/marker-without-true).

Phase B (root-cause scroll-stability) tracked separately. Thanks @b3nw.
2026-06-17 03:41:47 +00:00
nesquena-hermes
810414b438 feat(#4325): opt-out toggle to disable transcript virtualization
Adds a Preferences setting 'Virtualize long transcripts' (default ON, opt-out).
When disabled, _currentMessageVirtualWindow returns a full non-virtualized window
so the entire transcript renders at once (also short-circuits the virtual
re-measure loop). Server-persisted (virtualize_transcript bool, default True),
hot-applies via renderMessages on toggle. i18n in all 13 locales.

Pairs with @rodboev's #4328 stream-end anchor scroll fix (cherry-picked into
this branch) to fully resolve #4325.
2026-06-17 01:52:06 +00:00
nesquena-hermes
62a65a4ff1 review polish: surface plugin-provider check failures at warning (greptile P2) + fix #4253 comment mislabel
Independent review (APPROVED clean) + greptile both flagged one non-blocking nit:
_is_known_model_provider swallowed a transient _is_plugin_model_provider failure
at debug level, so a real plugin-backed provider could briefly vanish from the
picker with no visible log. Promoted to logger.warning(exc_info=True).

Also corrected the endpoint-error-hint comments in ui.js: they cited #4253, which
is actually a cron release (mislabel carried from #4279's body). Comment-only —
no logic change to the browser-verified collapsible-groups code.
2026-06-16 22:09:57 +00:00
nesquena-hermes
ea9cd16850 fix(providers): gate credential_pool detection to known model providers (#4324)
Fixes the Photon phantom-providers regression from #4247 (v0.51.461): the
credential-pool detection loop added every auth.json credential_pool key to
detected_providers without checking it names a model provider, so the Photon
iMessage plugin's photon/photon_project/photon_user messaging credentials each
rendered as a phantom provider carrying the full auto-detected model catalog
(251 models on the reporter's machine).

Gated on a new _is_known_model_provider() check (custom:* slug, static
_PROVIDER_DISPLAY/_PROVIDER_MODELS entry, or registered model-provider plugin)
on both the load_pool and raw-dict fallback paths; the group-builder's generic
fallback no longer paints an unrecognized id with the global catalog. Real
pool-keyed providers (copilot, aliased google->gemini) still surface.

Integrates the stronger of two independent same-fix PRs — #4330's implementation
(thorough docstring, try/except-hardened plugin check, preserved #1881 comment,
defensive else branch) + its 8-case test that mocks at the WebUI boundary
(stubs hermes_cli, never imports the bundled agent package that's absent in CI,
exercises both the load_pool and ImportError raw-dict paths) — over #4329's
2-case test. Both rodboev (#4329) and the maintainer agent (#4330) independently
found and fixed the same regression.

Reported by Nic.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-16 21:45:43 +00:00
nesquena-hermes
29d15d7360 fix review: profile-scope _CREDENTIAL_POOL_CACHE + getattr-safe runtime_api_key (#4247)
Codex gate caught a cross-profile leak: _CREDENTIAL_POOL_CACHE was keyed
by provider id alone, so a custom provider configured under profile A made
profile B report has_key=True from a stale cache hit (then 401 at request).
Key the cache by (active-profile auth-store path, provider id) at all
get/set/evict sites via _credential_pool_profile_tag(). Also make the two
new bare _entry.runtime_api_key reads getattr-safe (OAuth-only entries lack
the attr), matching the guard the author already applied at providers.py.
Adds a cross-profile isolation regression test.

Co-authored-by: akrhin <akrhin@users.noreply.github.com>
2026-06-16 18:22:07 +00:00
nesquena-hermes
b81a92ecd5 fix: credential pool fallback for custom providers (#4247)
Custom/aliased providers with keys in the credential pool (added via
hermes auth add) were reported unconfigured in the WebUI because provider
detection only checked env vars. Add _has_explicit_pool_credentials() that
checks the pool while filtering the ambient gh_cli entry (so Copilot doesn't
falsely show configured on any machine with gh installed), cache-routed to
avoid the cold per-provider load_pool latency, and resolve provider aliases
before load_pool() at every retrieval site so detection and retrieval read
the same pool (glm->zai, github->copilot, etc.). /api/models/live wrapped in
profile scope; getattr-safe runtime_api_key for OAuth-only entries.

Co-authored-by: akrhin <akrhin@users.noreply.github.com>
2026-06-16 17:44:10 +00:00
nesquena-hermes
ecf637c66d
Release PN (v0.51.453): bridge approval controls to the gateway runs API (#4229, #4203) (#4299)
Approve/deny on a remote-gateway backend now forwards to the gateway runs API
(run_id server-derived, existing auth+CSRF preserved, capability-probed + 60s
cached). Non-gateway/local approval path unchanged — relay only fires when a
session has an active gateway run (_STREAM_RUN_IDS). Old gateways get a clear
upgrade message instead of a silent no-op.

Co-authored-by: rodboev

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-06-15 22:25:24 -07:00
nesquena-hermes
0b0e2a3213
Release PG (v0.51.446): model picker show-all overflow for large catalogs (#3691) (#4282)
* stage-3712: model picker show-all overflow + fix per-row count leak (#3691)

Self-rebased rodboev's #3712 onto master. FIX (Nathan-caught): the per-row
provider chip stripped the '(N of M)' overflow count — that count belongs on the
group heading only, and was reading as nonsense post-expand (row 30 showing
'15 of 30'). Updated test to assert heading-carries-count + rows-do-not.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>

* stage-3712: + dedupe overflow expand (Codex SILENT) — reuse existing injected option

Codex caught: select hidden overflow model from search → reopen → Show all could
duplicate the row (_appendOverflowOptionsToGroup blindly appended every extra
model without checking the select for an already-injected option). Now moves/reuses
an existing same-value option instead of re-creating it.

* stage-3712: + fix expanded-heading double-count (Opus) — strip decoration, count rendered rows

Opus caught: post-expand the heading showed 'Nous (2 of 4) (4)' (appended its own
count onto the already-decorated backend label) and used modelCount (incl hoisted-
configured) vs rendered rows. Now strips the '(N of M)' suffix when expanded +
counts groupRows.length. Test asserts no double count on out[expanded].

* stage-3712: + include extra_models in Settings/Cron/Profile/Aux native selectors (Codex CORE)

Codex caught: the server overflow split moves models>15 into extra_models for ALL
providers, but the 4 native <select> builders in panels.js (settingsModel:6690,
cronFormModel:1112, profileFormModel:5784, _auxProviders:8287) read g.models only
— silently dropping the overflow tail for large providers. Now concat
models+extra_models at all 4 sites. Test guard added.

* Release PG: model picker show-all overflow for large provider catalogs (#3691)

Self-rebased rodboev's #3712 onto v0.51.445; full browser-tested (desktop+mobile,
before/after) + Nathan screenshot-approved. 4 gate-caught issues fixed inline:
per-row count leak (Nathan), overflow-expand dedup (Codex), heading double-count
(Opus), and native Settings/Cron/Profile/Aux selectors dropping the overflow tail
(Codex CORE). Codex SAFE + Opus SHIP + 41 picker tests green.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-15 17:16:36 -07:00
nesquena-hermes
e47f685b89
Release PA (v0.51.440): advanced per-model options + auxiliary model routing (#3097) (#4264)
* stage-3097: advanced model options (re-push fixes both no-ops), rebased onto master

* Release PA: advanced per-model options + auxiliary model routing (#3097)

ai-ag2026's #3097, re-pushed to fix both prior silent no-ops:
- base_url no longer clobbered (advanced applied after resolver write)
- extra_body now reaches the agent via request_overrides (+ cache-key); main
  modal hides unsupported timing fields (aux-only). Functionally verified live
  (gear→modal→save→persists). Codex SAFE + Opus SHIP + suite green.
Note: also adds 3 aux task slots (kanban_decomposer/profile_describer/triage_specifier).

Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-06-15 13:16:34 -07:00
nesquena-hermes
ed5afd2ae3
Release ON: show_cli_sessions default-on for new installs (#3988) + symlink delete/rename guard (#4217) (#4230)
* fix(#3988): default show_cli_sessions to true for new installs, grandfather existing OFF

Combines the best of #3997 (rodboev) and #4222 (Sanjays2402):
- config.py default flipped True so NEW installs surface CLI/TUI/Telegram/
  Discord/Hermes One sessions in the sidebar without hunting for the toggle.
- load_settings() backfill grandfathers ESTABLISHED installs OFF: when the saved
  settings predate the flip (onboarding_completed True + key absent), pin False so
  an existing user's sidebar doesn't silently change. Fresh installs get True.
- Default-true hydration at every read site (config.py / boot.js / panels.js
  checkbox) via the !==false idiom — no config-ON-but-UI-OFF mismatch (#4006 class).
- 10 tests: config default, bool-key round-trip, grandfather matrix
  (fresh/mid-onboarding/established-absent/explicit-true/explicit-false/
  save-preserves-grandfather), and both frontend hydration sites.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: Sanjays2402 <Sanjays2402@users.noreply.github.com>

* fix(#4217): guard /api/file/delete and /api/file/rename against symlink targets

* chore(release): stamp v0.51.427 (Release ON) for #3988 + #4217

* chore: drop unused imports (ruff F401) in absorbed test files

* fix(#4217,#3988): address review — dangling-symlink ordering + boot.js catch fallback

Greptile review on #4230 + the related-PR reviews surfaced two fixes on the
shipped code:
1. (#4217) _handle_file_delete/_handle_file_rename checked target.exists()
   BEFORE is_symlink(), so a DANGLING workspace symlink resolved to a missing
   target -> misclassified as 404 'File not found' and left permanently
   undeletable/unrenameable. Moved the no-follow is_symlink() guard before the
   follow-based exists() check in both handlers. +2 dangling-symlink tests.
2. (#3988) boot.js settings-load-FAILURE catch block hardcoded
   _showCliSessions=false, inconsistent with the new True default (the #4006
   catch-block-fallback class). Now defaults true. +1 test pinning the fallback.

Both confirmed by the new tests (20/20 in the two files).

* fix(#3988,#4217): address Opus review — broaden grandfather signal + move-handler parity

Opus #4230 ship-review surfaced 2 LOW findings, both now fixed in-line:
1. (#3988) The grandfather keyed only on onboarding_completed, so a CLI-configured
   user who tweaked a WebUI setting but never ran the wizard would get flipped ON
   (and onboarding.py later auto-persisting onboarding_completed=True would make it
   permanent). Now: established = show_cli_sessions absent AND (onboarding_completed
   OR any persisted setting other than a falsy onboarding flag). A genuinely new /
   still-mid-onboarding file still falls through to True. +1 test.
2. (#4217) _handle_file_move still checked exists() before is_symlink() — the same
   dangling-symlink-404 bug just fixed in delete/rename. Reordered for parity so a
   dangling symlinked source is rejected 400, not 404.

22 tests across the two files pass; ruff clean.

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: Sanjays2402 <Sanjays2402@users.noreply.github.com>
2026-06-14 23:06:22 -07:00
nesquena-hermes
e7e856cd79
fix(providers): keep custom provider when model id has known-provider prefix (#4210) (#4220)
* fix: keep custom provider when model id has known-provider prefix (#4210)

Siblings of #3872 (v0.51.349, custom provider with a configured base_url
preserves intrinsic routing prefixes). The base_url unset variant of
the same bug was missed: with provider=custom or custom:<slug> and no
base_url set, a model id like 'x-ai/grok-2' or 'google/gemma-2' was
hijacked to provider='openrouter', base_url=None. The backend then
failed to initialize with 'RuntimeError: No LLM provider configured'
because the user had no OpenRouter credentials configured.

resolve_model_provider() now applies the same _is_custom exemption the
config_base_url branch already carries to the cross-provider redirect
at the end of the slash branch. The custom provider keeps the request
and the full model id; real first-party providers pointed at an
OpenAI-compatible proxy (e.g. provider=openai + base_url=litellm) are
unaffected because their strip path is in the base_url branch and the
'prefix == config_provider' branch above the change.

Verified:
- test_model_resolver.py 32 passed (30 existing + 2 new RED tests)
- test_custom_provider_bare_model_reasoning.py 34 passed
- Pre-existing F401 (api/config.py:23,24,28) are not introduced by
  this change; same 3 errors exist on upstream/master.

Refs #4210

* chore(release): reposition #4210 CHANGELOG entry under v0.51.426 (OM)

---------

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-06-14 19:56:12 -07:00
nesquena-hermes
87fa0a2ace
feat(workspace): optional Todos tab in workspace panel (#3564) (#4209)
* feat(workspace): add Todos tab to workspace panel behind a setting (#2942)

* fix(workspace): use DOM attribute to check active tab across script scope

* Prefer the live todos snapshot in the workspace tab

* Fill the missing workspace Todos locale keys

* feat(workspace): add optional Todos tab to workspace panel (#3564)

Rebased rodboev's #3564 onto current master + CHANGELOG entry.

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* fix(#3564): apply workspace Todos tab visibility on cold boot

Codex gate: boot.js set window._workspaceTodosTab from saved settings but
never called _applyWorkspaceTodosTabVisibility(), so a previously-enabled
Todos tab stayed hidden on a normal page reload (only re-appeared after
opening Settings). Call the visibility helper after assigning the flag in
both the settings-apply and the fallback/default paths.

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* chore(release): stamp v0.51.422 (Release OI) for #3564

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-06-14 17:31:13 -07:00
nesquena-hermes
92ecf8edb4
fix(models): preserve colon-suffixed model IDs in normalization (#3959, ships #4196) (#4204)
* fix(models): preserve colon-suffixed model IDs in normalization (#3959)

* fix(models): align configured-model normalizer parity (#3959)

* chore(release): stamp v0.51.421 (Release OH) CHANGELOG for #3959/#4196

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-06-14 16:27:39 -07:00
nesquena-hermes
6c6444396d
fix(#3750): preserve LM Studio auth on reasoning probe (self-rebased #3837 + credential hardening) (#4202)
* fix(#3750): preserve LM Studio auth on reasoning probe

Rebased rodboev's #3837 onto current master (master reworked
resolve_model_reasoning_efforts via #3431 with a combined hermes_cli
import + _nested_route_reasoning_denied guard). Threaded the api_key
resolution + the _lmstudio_model_reasoning_options wrapper + the
HTTP-fallback probe into master's current structure; intent preserved
byte-identically (+164/-24, same as the original PR diff).

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* fix(#3837): gate LM Studio probe credential to configured endpoint + no-redirect opener

Codex security review (CORE) on the rebased #3837: GET /api/reasoning accepts
a caller-supplied base_url, and the new probe attached the configured LM Studio
API key as a Bearer token; urllib also re-sends Authorization across redirects.
Together that could exfiltrate the stored credential to an attacker-controlled
host. (master only escalates here because master probed WITHOUT a key.)

Fixes:
1. Only forward the LM Studio credential when the probe target normalizes to
   the configured LM Studio base URL (reuses _normalize_base_url_for_match);
   a caller-supplied non-matching base_url is probed keyless.
2. _lmstudio_reasoning_probe_options_fallback uses a no-redirect opener
   (_NoRedirectHandler) so a 3xx can't forward the Authorization header onward.
3. Two regression tests: caller-supplied base_url drops the key; redirect is
   not followed (credential never reaches the redirect target).

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* fix(#3837): route ALL credentialed LM Studio probes through the no-redirect probe

Codex re-gate caught that the first fix only hardened the built-in fallback;
in production hermes_cli IS importable, so the preferred path called
hermes_cli.models.lmstudio_model_reasoning_options(..., api_key=...) which uses
plain urlopen and still follows redirects with the credential.

Fix: _lmstudio_model_reasoning_options now short-circuits to the built-in
no-redirect probe whenever api_key is set; the hermes_cli path is used ONLY for
keyless probes (no credential to leak). Reworked tests accordingly:
- test_credentialed_probe_never_calls_hermes_cli: credentialed probe bypasses CLI
- test_keyless_probe_logs_signature_mismatch_before_fallback: keyless CLI degrade
- test_credentialed_probe_does_not_follow_redirects_end_to_end: E2E through
  resolve_model_reasoning_efforts with real hermes_cli; redirect target never hit

Verified both security tests fail without the guard and pass with it.

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* chore(release): stamp v0.51.420 (Release OG) in CHANGELOG

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-14 15:46:35 -07:00
b3nw
db3d22912e fix(config): reasoning toggle for nested Gemini custom provider routes (#3431)
Version-gated the nested-gateway allow to 2.5-series/3-era (reviewer fix:
gemini-1.5/1.0 have no thinking controls -> selector would fail on send) +
pre-2.5 exclusion regression tests. Stamped v0.51.408 (Release NU).

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-06-14 04:44:47 +00:00
nesquena-hermes
30cc15975e fix(#4130): coerce stale 'max' in get_reasoning_status read path too (Codex catch)
get_reasoning_status() returned the raw config reasoning_effort, so a stale saved
'max' surfaced on boot/status/chip even though streaming coerces it to xhigh. Now
routes the status value through coerce_reasoning_effort_for_model() with the resolved
model/provider/base_url, so all read paths agree. Added a stale-max->xhigh status
regression test.
2026-06-13 18:50:00 +00:00
Rod Boev
95bb9ebe66 Keep stale max configs on the supported path 2026-06-13 14:33:26 -04:00
Rod Boev
c7c313672c fix(reasoning): hide unsupported max effort (#4119) 2026-06-13 14:33:26 -04:00
nesquena-hermes
c02c31c837 Merge #4006 (scroll auto-follow setting + pin guards) onto master
# Conflicts:
#	api/config.py
#	static/panels.js
2026-06-13 03:54:11 +00:00
nesquena-hermes
92f7a24448 Merge #4024 Transparent Stream onto master (rebase)
# Conflicts:
#	CHANGELOG.md
2026-06-13 00:40:17 +00:00
nesquena-hermes
82fcf665eb harden(#4052): tolerate providers.*.models list-of-dicts keyed by model/name (Opus SHOULD-FIX)
The richer static-catalog builder's group-building loop used a strict item["id"]
extraction that would KeyError (caught → degrade to the minimal one-model catalog)
for legal config shapes where providers.<id>.models is a list of dicts keyed by
"model"/"name" rather than "id". Mirror the tolerant id-or-model-or-name resolution
the detection loop already uses, skip entries with nothing usable, and add a
regression test. No behavior change for the common list-of-strings / list-of-id-dicts
shapes.

docs(changelog): stamp #4052 model-picker budget fallback as v0.51.370 (Release MI)
2026-06-12 20:05:12 +00:00