- New 'selfmedia' panel registered in MAIN_VIEW_PANELS with sidebar
rail/nav buttons, main view container and mobile sidebar panel
- Native in-origin rendering (no sandboxed iframe) so api() calls
carry session cookie + CSRF and actually load cron data
- Dashboard: stats cards, self-media cron job detection by keywords,
one-click job trigger, recent activity feed, 30s auto-refresh
- Quick actions open a brand-new conversation and send the command
there (发布公众号/选题/写文章/多平台发布/热门排行)
_scheduleMessageVirtualizedRender now takes (force, request), so the two
structural probes in test_issue4346_vscroll_footer_jitter.py matched nothing.
renderMessages() calls _resetMessageVirtualMeasurementBurst(), which node
harnesses that extract renderMessages() alone (test_compaction_current_summary_fallback.py)
don't define; guard it with typeof like the file's other optional helpers.
Review follow-up on #7720 (Greptile P2): the external Notes drawer reads the
same profile-bound MCP inventory as the MCP panel but dropped the
``runtime_scope`` label, so a profile whose runtime scope could not be
confirmed rendered its note sources as inactive/configured with no
explanation. ``/api/notes/sources`` now returns ``runtime_scope`` and the
drawer shows the existing ``mcp_runtime_scope_unavailable`` notice (same key,
no new locale strings) when it is ``unavailable``.
Tests: the skewed-routing unit test now also asserts the notes response
carries ``runtime_scope: "unavailable"`` with its sources inactive.
docs/troubleshooting.md mentions the drawer next to the panel.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two profiles configuring a same-named MCP server shared one connection and
the MCP settings panel showed another profile's runtime ("Active, N tools"
next to an empty inventory); /reload-mcp stopped every profile's servers and
rediscovered from the process-default config.
- api.profiles._set_hermes_home() is the single writer of the process-profile
home: it records get_process_profile_home() and hands it to Hermes Agent's
hermes_constants.pin_process_hermes_home when available, at startup and on
switch_profile(process_wide=True), so the per-turn HERMES_HOME mirror no
longer makes every turn look like the process profile (bare-name,
cross-profile MCP keys) and the anchor cannot drift from HERMES_HOME.
- Run get_mcp_status/registry reads and /reload-mcp inside
api.mcp_runtime.mcp_runtime_scope(): request profile's Hermes-home override
and secret scope (root included), no os.environ mutation, restored on exit.
- One ledger predicate, ledger_key_serves_view() (owned via
_server_scope_keys/key scope, or adopted via _server_tool_scopes), is shared
by status, inventory and the reload summary. The Agent's launch-profile view
is process-wide and name-indexed, so filter_runtime_status_to_view()
rebuilds connected/tools/status/error from the entries serving the profile:
the root profile never shows a routed profile's connection (nor lets its own
parked failed-spawn task borrow a routed profile's live same-named one).
- Inventory lists only tools in the profile's own registry slot for servers it
configures; responses carry runtime_scope.
- Fail closed when the agent's routing does not match the resolved profile:
hide runtime data (runtime_scope "unavailable", shown as a notice in the MCP
panel, 14 locales) and refuse /reload-mcp.
- /reload-mcp shuts down only the profile's own ledger names (parked
included), drops its own connect cooldown/error entries so a failed spawn is
retried (the wildcard cleared every cooldown; a scoped shutdown only clears
the live keys it tears down), rediscovers under the profile's home and
counts live connections only. The bare wildcard remains for agents without
profile-scoped MCP.
- Signature introspection is centralized (accepts_keywords) and falls back to
the bare call instead of blanking every status; ledger key helpers resolve
to the Agent's tools.mcp_tool_scope when present; tools.mcp_tool is
resolved by full name so stubbed agents still work.
- ARCHITECTURE.md §4.10 and docs/troubleshooting.md document the boundary.
- Tests: agent-free contract (models adoption, parked tasks, cooldowns and the
process-wide root view) and real-Agent integration (fake stdio MCP server,
skips without hermes-agent + mcp SDK).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A pending approval/clarify prompt re-surfaces on every 1.5s poll tick, and
with permission still 'default' (second browser/device) each tick issued a
non-gesture Notification.requestPermission() and toasted 'notifications
denied'. Auto requests are now made once per page; the explicit Send-test
path still always asks. Release-gate hardening on top of #7493.
Each approval/clarify fallback poller captures its own interval handle; a 409
(or finally) from a request issued by a replaced poller no longer stops or
clears the in-flight guard of the poller that superseded it.
- /api/git-info falls back to state.db metadata for sessions without a WebUI
sidecar (CLI, delegated subagents), mirroring /api/list, instead of 404ing.
- Approval and clarify fallback pollers stop on session_profile_mismatch 409
instead of re-requesting every 1.5s/3s; live cards stay standing.
- Drop <link rel=preload> for pwa-startup.js: the preload scanner resolves it
before the dynamic <base href> is written, so on /session/<id> it fetches
/session/static/... while the script loads /static/..., a wasted duplicate
request plus a 'preloaded but not used' warning on every load.
Forks and other child sessions keep their own project_id, so a fork moved to
No project stays Unassigned. A per-render memoized, cycle-safe resolver is
shared by partitioning, reference scoping and the Unassigned chip.
Editing a cron job with the model picker at "Default" sent provider: null
unconditionally, wiping provider-only pins (provider set, no model — the
pattern for self-hosted LLM endpoints). The combined picker cannot express
that state, so such jobs always rendered as "Default" and were silently
re-resolved to the bare global default on any save, breaking dispatch.
#4030 deliberately made clearing the picker mean "no overrides"; that
contract is preserved for explicit clears and model+provider jobs, while
provider-only pins survive saves where the picker was never touched.
- _cronProviderForClear(prevDetail, pickerTouched) decides the provider
submitted on clear: touched picker -> null; untouched + no model on the
job -> prev.provider.
- _cronModelPickerTouched is set by a change listener added when the picker
populates, and reset at openCronEdit/openCronCreate/duplicateCurrentCron.
- Tests: delegation + flag lifecycle locked in, and the helper driven
through node for provider-only / model-pinned / no-override / no-snapshot
inputs (6 passed).
Refs: #4030
The chip check now reuses the ancestor map built by _partitionSidebarSessionRows,
so a child whose project parent is only a hidden reference row is not counted
as unassigned. Document project inheritance for subagent sessions in README.
The replay-authority predicate accepted any non-empty assistant message,
but a new turn copies the full transcript into INFLIGHT.messages. In an
established conversation that content is the previous turn's reply, so
`canSeed` came back true with both stale cursor halves preserved and the
server kept skipping the range that rebuilds the body.
Message-based evidence now counts only from a current `_live` assistant
positioned after the latest user boundary; a historical assistant row
fails closed to floor 0 and an empty event id.
Refs #7640 (blocker on #7651)
Subagent child sessions come from state.db and never carry a project_id,
so the project filter dropped them. Resolve a child's project from its
nearest ancestor for the project filter, reference scoping and the
Unassigned chip.
#7708 re-gate (9/22 23:47), both findings:
**Finding 1 (SILENT)**: A bare path that contains a backtick
(MEDIA:/tmp/report`final.png) is truncated at the first backtick,
so the server stops authorizing it. The round-1 fix traded one bug
for another: adding the backtick to the bare-token exclusion set
dropped the inline-code closing backtick (good) but truncated any
bare path with a backtick in the filename (bad — POSIX and Windows
both allow backticks).
**Finding 2 (SILENT)**: TTS now reads filename fragments aloud
(MEDIA:/tmp/report(1).png -> 'a file).png') because the
parenthesis-exclusion was added to a site that should not need it.
Inline code is already stripped at the top of the TTS flattener.
**Fix shape** (maintainer-suggested two-pass scan):
1. Pre-pass: `MEDIA:([^`\s]+)` -> MEDIA:$1 — strips the
inline-code backticks off the wrapped form so the bare class
below sees a plain MEDIA:path.
2. Bare class: MEDIA:([^\s\)\]]) (original, backtick
removed from the exclusion set) — captures the full filename
even when the path itself contains a backtick.
3. TTS: restore the original MEDIA:[^\s]+ (no paren/bracket
exclusion either). Inline code is stripped at the top of the
flattener so the closing backtick of an inline-code wrapper has
already been eaten by the time the TTS regex scans the text.
Applied to all 7 capture sites:
- api/routes.py:20453 (_MEDIA_TOKEN_RE, findall site 20513)
- api/media_snapshots.py:507 (media_re, findall site 533)
- static/ui.js:7695 (linkifier, replace site)
- static/ui.js:9142 (TTS flattener — separate shape)
- static/messages.js:4848 (chunk finalize, exec)
- static/messages.js:4896 (combined walk, exec loop)
- static/messages.js:4922 (partial tail, exec)
**Tests** (24 total, all green):
- 11 source-shape guards: each JS/PY site must carry the bare-token
class literal AND the wrapped-form pre-pass literal
- 4 behaviour tests against the live source: wrapped form strips
closing backtick, bare form captures plain path, bare form with
backtick in filename captures the full path (the new regression),
trailing paren is still excluded
- 2 round-1 regression guards documenting the trade-off that drove
the two-pass compose
- 1 composed two-pass test (pre-pass + bare class on 4 inputs)
- 2 node --check syntax guards
- 1 site-count guard: 5 JS + 2 PY = 7 capture sites, all guarded
**Verification**:
- pytest tests/test_issue7359_media_backtick_strip.py -v -> 24/24 pass
- python3 scripts/ruff_lint.py --diff origin/master -> 'no new
violations on added/modified lines. OK.'
- node --check on the two modified JS files -> both OK
Closes#7622.
Round-3 follow-up to the maintainer review at
https://github.com/nesquena/hermes-webui/pull/7730#issuecomment-5786493511.
The round-2 'primary === "en"' heuristic overrode users who had
genuinely picked English: when the server-stored 'language' was 'en'
the resolver treated it as the schema default and skipped it, falling
through to localStorage / the browser hint. This round addresses all
three maintainer findings:
1. BRICK — server stops injecting the schema default. Drop
'"language": "en"' from api/config.py:_SETTINGS_DEFAULTS so a
fresh install returns settings['language'] == None. Existing
settings.json files that already carry 'language: "en"' still
report 'en' (the field is just absent from the defaults dict; the
per-file load path is unchanged). 'language' is added back to
_SETTINGS_ALLOWED_KEYS so the Settings modal save path still
validates and persists explicit user picks (the BCP-47 regex is
unchanged).
2. CORE — browser-language detection is now in one guarded helper.
static/i18n.js exposes _detectBrowserLanguageHint(), used by
loadLocale(), boot.js, and panels.js. A throwing navigator
accessor in some embedded webviews can no longer abort boot or
settings hydration before the model/provider/plugin/extension
sections are populated.
3. CI RED — restore the test shape. boot.js and panels.js call
resolvePreferredLocale(s.language, localStorage.getItem('hermes-lang'),
_detectBrowserLanguageHint()) inline so
tests/test_language_precedence.py::test_boot_and_settings_panel_use_shared_locale_precedence
matches the expected argument list. The TestComposedResolverPrecedence
tests in tests/test_issue7622_initial_locale_from_browser.py are
re-pinned to the round-3 contract (server primary is None for a
fresh install; an explicit 'en' primary from a user-saved file
beats a non-English browser hint).
Tests (22/22 i18n-related pass; 833/833 settings+i18n+locale pass; ruff
forward-gate --diff origin/master is clean):
- test_language_precedence.py: 8/8 (added guarded-helper, server-default,
composed-resolver, and cross-file first-visit tests)
- test_issue7622_initial_locale_from_browser.py: 14/14 (re-pinned to
round-3 contract; added explicit-saved-English regression test)
- test_login_locale.py and friends: unchanged behaviour for the
save/load round trip
Closes#7622.
The renderer's `loadLocale()` was strictly a localStorage-or-English
fallback — non-English speakers had to dig into Settings after every
fresh install. This change consults `navigator.languages[0]` (or
`navigator.language` when the ordered list is missing) ONLY when no
stored preference exists, so:
- first visit on a zh-CN browser → UI lands on `zh` immediately
- first visit on an en-US browser → still `en` (regression guard)
- first visit on a kl-GL browser (unsupported) → falls back to `en`
- any second visit (or after the user picked a language in
Settings) → the stored value wins, the browser hint is never
consulted again for that browser profile
Tests (8/8 pass): a node driver loads the real `static/i18n.js`,
mocks `localStorage` and `navigator`, and asserts the language
that `loadLocale()` ultimately passes to `setLocale(lang)`. The
test covers the four core happy/sad paths plus two pinning tests for
the 'stored wins' contract.
`resolveLocale()` already strips the region subtag (zh-CN → zh,
en-US → en, fr-FR → fr), so the `navigator.language` value is
reduced to a key in `LOCALES` without any extra work in this PR.
The fix is strictly additive: `loadLocale()`'s precedence chain
becomes (navigator hint) → (stored) → 'en' on first visit, and
remains (stored) → 'en' on every subsequent visit.
Gate follow-up for #7710. Three front-end handlers treated EVERY 409 as a
domain-specific stale/expired signal, so the new session_profile_mismatch
response would have:
- messages.js: hidden a LIVE clarify card and mislabelled it expired
- ui.js: retired a LIVE compression-recovery card with a false note
- commands.js: rendered a false 'Compression failed' error state
Each now routes through _sessionProfileMismatchFromError() first and leaves
the card standing. Also captures the status in the import_cli test mock so a
wrong status can no longer pass (the mock swallowed it into *_).