Commit graph

8767 commits

Author SHA1 Message Date
Teknium
378bca1d2f chore(release): add AUTHOR_MAP entry for falasi 2026-05-18 14:31:37 -07:00
falasi
43802ef3e3 fix: add default base_url_override for ollama-cloud provider 2026-05-18 14:31:37 -07:00
Austin Pickett
6fa1701bd3
feat(web): mobile dashboard UX polish (#28127)
* feat(web): mobile dashboard UX polish

Bottom sheets for sidebar theme/language pickers on narrow viewports with
enter/exit animation and drag-to-close; inline header badges beside titles;
bottom padding on the route outlet for scroll clearance; profiles loading uses a
unicode braille spinner; align profile/cron card actions to the top; viewport-fit
cover and supporting layout tweaks across dashboard pages.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix Nix web npm hash and mobile sheet accessibility.

Align fetchNpmDeps in nix/web.nix with web/package-lock.json for CI. Improve BottomPickSheet backdrop labeling, avoid aria-hidden on the dialog during exit animation, and wire theme/language sheets with listbox semantics and localized dismiss labels.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 15:20:31 -04:00
HenkDz
52e3bfc2f4 feat(acp): enrich permission request cards 2026-05-18 11:47:27 -07:00
teknium1
2057977102 fix(acp): use refresh moment as updated_at on session info push
Follow-up to #26543. The sessions table does not have an updated_at
column (see hermes_state.py — only started_at/ended_at), so
row.get('updated_at') always returned None and the str() coercion was
dead code. Use datetime.now(UTC).isoformat() instead, which reflects
exactly what the field means here: 'the title was refreshed at this
moment'. Drop the dead coercion.
2026-05-18 11:46:04 -07:00
HenkDz
741a349458 fix(acp): refresh session info after auto-title 2026-05-18 11:46:04 -07:00
teknium1
eda1c97a1e fix(acp): also mark raised-exception tool results as failed
Extends #26573 to also catch the case the original PR deliberately left
out: when a tool raises an exception, the agent's tool executor wraps it
in a canonical 'Error executing tool '<name>': ...' string prefix (see
agent/tool_executor.py around the try/except). That prefix is unique to
the wrapper and cannot legitimately appear in well-behaved tool output,
so it is a safe signal that the tool blew up.

Without this, the canonical 'tool raised' case still rendered as a green
'completed' row in Zed despite being a runtime failure — exactly the
class of bug #26573 set out to fix.

Adds a positive test (raised-exception prefix -> failed) and a negative
test (bare 'Error:' word in legit tool output stays completed) so a
future contributor doesn't accidentally widen the rule to false-positive
on compiler/linter diagnostics.
2026-05-18 11:43:45 -07:00
HenkDz
9cf1140caa fix(acp): treat polished tool error payloads as failed 2026-05-18 11:43:45 -07:00
HenkDz
b38d2d133b fix(acp): mark failed tool completions 2026-05-18 11:43:45 -07:00
HenkDz
375c7f9cc3 fix(acp): render structured JSON tool output 2026-05-18 11:41:57 -07:00
墨綠BG
50e93f23f2 🐛 fix(memory): require newline after context tag 2026-05-18 10:53:08 -07:00
墨綠BG
341c8d3030 🐛 fix(memory): keep inline memory-context mentions visible 2026-05-18 10:53:08 -07:00
teknium1
956dd44625 chore(release): add AUTHOR_MAP entry for dskwe 2026-05-18 10:51:15 -07:00
Ryan Lee
6143ce1546 fix(url_safety): block IPv4-mapped IPv6 addresses to prevent SSRF bypass 2026-05-18 10:51:15 -07:00
alt-glitch
e3f391c1ac test(cron): cover profile + workdir combined scenario 2026-05-18 17:39:50 +00:00
alt-glitch
ef5fe8dfaf fix(cron): gracefully degrade when runtime profile is deleted
Instead of raising FileNotFoundError (which silently bricks the job),
log a warning and fall back to the scheduler default home. Validates
at create/update time still catches typos. Idea from PR #19958.
2026-05-18 17:39:50 +00:00
alt-glitch
1d74d7f73a fix(cron): use delta-based env restore instead of clear+update
Avoids a brief window where other threads see an empty os.environ
during profile job teardown. Idea from PR #19958.
2026-05-18 17:39:50 +00:00
alt-glitch
1f9b2e4d0b chore: add gianfrancopiana to AUTHOR_MAP 2026-05-18 17:39:50 +00:00
Gianfranco Piana
9c48d47aaf fix(cron): isolate profile job env 2026-05-18 17:39:50 +00:00
Gianfranco Piana
544406ef23 fix: avoid process-wide cron profile home mutation 2026-05-18 17:39:50 +00:00
Gianfranco Piana
bb9ecb2178 feat: add cron job profile support 2026-05-18 17:39:50 +00:00
teknium1
47bc8e080d chore(release): AUTHOR_MAP noreply entry for Slimydog21 2026-05-18 10:37:35 -07:00
Slimydog21
aae1615977 fix(xai-responses): strip enum values containing '/' from tool schemas
xAI's /v1/responses and /v1/chat/completions endpoints reject tool schemas
whose enum values contain a forward slash with a generic HTTP 400 'Invalid
arguments passed to the model.' before any token is emitted — the schema
compiler trips on the '/' character regardless of where it appears.

Most commonly hit by MCP-derived tools whose enum lists HuggingFace model
IDs ('Qwen/Qwen3.5-0.8B', 'openai/gpt-oss-20b') or owner/name environment
identifiers.

Mirrors the existing strip_pattern_and_format sanitizer (PR for #27197).
The new strip_slash_enum walks tool parameters and drops the entire enum
keyword when any value contains '/' — keeping it partial would still 400
since xAI's failure is all-or-nothing on the enum. The field description
still reaches the model so the prompting hint is preserved.

Wired in at both code paths for parity:
  - agent/chat_completion_helpers.py (main agent xAI Responses path)
  - agent/auxiliary_client.py (aux client xAI Responses path, matching
    the same parity guarantee 2fae8fba9 established for pattern/format)

Salvaged from #28021 by @Slimydog21 — contributor's branch was severely
stale (would have reverted ~5000 LOC across azure/kanban/i18n); fix
re-applied surgically on current main with their sanitizer + 9 tests
preserved verbatim. Author noreply email used (original was a Mac
hostname leak).
2026-05-18 10:37:35 -07:00
EloquentBrush0x
d9331eecee fix(minimax-oauth): quarantine dead tokens on terminal refresh failure
resolve_minimax_oauth_runtime_credentials called _refresh_minimax_oauth_state
without a try/except, so a terminal failure (invalid_grant,
refresh_token_reused, invalid_refresh_token) raised AuthError but left
the dead refresh_token in auth.json. Every subsequent API call retried
the same token via a network round-trip, failing identically each time.

Fix: wrap the refresh call and, when exc.relogin_required is True and a
refresh_token is present, clear the dead OAuth fields (access_token,
refresh_token, expires_*) and write a last_auth_error quarantine marker
to auth.json before re-raising. The next call sees no access_token and
fails fast with 'not_logged_in' — no network retry — and the user is
prompted to re-authenticate.

Mirrors the existing quarantine pattern for Nous (_quarantine_nous_oauth_state),
xAI-OAuth (#28116), and Codex-OAuth (#28118). Persist failure is
best-effort (logged at DEBUG, error still re-raised).

Salvaged from #28003 by @EloquentBrush0x — contributor's branch was
severely stale (would have reverted ~5000 LOC across azure/kanban/i18n
subsystems); fix re-applied surgically with their pattern preserved and
added two regression tests (terminal-quarantines + transient-does-not-quarantine).
2026-05-18 10:34:03 -07:00
EloquentBrush0x
b570e0fdd0 fix(codex-oauth): quarantine terminal refresh errors so dead tokens are not replayed across sessions
When a Codex OAuth refresh token is permanently invalidated (HTTP 400/401/403,
token revoked or reused), _mark_exhausted was called but auth.json was left with
the dead credentials. On the next session, _seed_from_singletons re-read
auth.json and re-seeded the pool with the same revoked token, triggering the
same terminal failure in a loop.

Add _is_terminal_codex_oauth_refresh_error to auth.py and a matching quarantine
block in _refresh_entry: when a terminal error is detected and auth.json holds
no newer tokens, clear access_token/refresh_token from auth.json and remove all
device_code-sourced pool entries from memory. Mirrors the Nous quarantine added
in c90556262 and the xAI quarantine in #28116.

Also add a pre-refresh sync from auth.json before calling refresh_codex_oauth_pure,
matching the xAI and Nous patterns, to avoid refresh_token_reused races when
multiple Hermes processes share the same auth.json singleton.

Salvaged from #27911 by @EloquentBrush0x — contributor's branch was severely
stale (would have reverted ~5000 LOC across azure/kanban/i18n subsystems);
fix re-applied surgically on current main with their predicate and tests preserved.
2026-05-18 10:31:40 -07:00
Teknium
9aae59feab
fix(compress): make abort-on-summary-failure opt-in via config flag (#28117)
PR #28102 made the summary-failure abort path the unconditional default,
changing established behavior. Gate it behind config.yaml flag
`compression.abort_on_summary_failure` (default False = historical
fallback-placeholder behavior).

- hermes_cli/config.py: new `compression.abort_on_summary_failure` key,
  default False, documented inline.
- agent/agent_init.py: read the flag from compression config and pass to
  ContextCompressor.
- agent/context_compressor.py: `__init__` accepts `abort_on_summary_failure`
  (default False). `compress()` failure branch gates the abort on the
  flag; when False, falls through to the restored legacy fallback path
  (static "summary unavailable" placeholder + drop middle window).
- tests: restore original fallback expectations as default; add new
  TestAbortOnSummaryFailure class for the opt-in mode.

Gateway/CLI plumbing (force=True on /compress, hygiene/handler abort
detection, locale `gateway.compress.aborted` key) from PR #28102 stays
intact — those paths only fire when `_last_compress_aborted` is True,
which now only happens when the flag is enabled.
2026-05-18 10:28:20 -07:00
EloquentBrush0x
5e40f83cb7 fix(xai-oauth): quarantine terminal refresh errors so dead tokens are not replayed across sessions
When refresh_xai_oauth_pure raises a terminal error (HTTP 400/401/403,
i.e. revoked or reused refresh token), _refresh_entry's existing race-
recovery path re-syncs from auth.json and returns if another process has
already rotated the tokens.  If auth.json still holds the same stale
token pair, the function fell through to _mark_exhausted — leaving the
dead credentials in auth.json.  On the next Hermes startup _seed_from_singletons
re-seeded the pool from those stale tokens, causing the same failure loop
on every session.

Fix: after the auth.json re-sync check in the xAI-oauth error handler,
detect terminal errors with the new _is_terminal_xai_oauth_refresh_error
helper and apply a quarantine:
- Clear access_token and refresh_token from providers["xai-oauth"]["tokens"]
  in auth.json so they are not re-seeded.
- Write a last_auth_error entry for hermes doctor / auth status diagnostics.
- Remove all loopback_pkce entries from the in-memory pool so the current
  session stops retrying with the dead credentials.

Mirrors the identical quarantine already in place for Nous OAuth
(c90556262).

Closes the parity gap introduced when c90556262 added Nous-only terminal
error handling without a corresponding xAI-oauth path.
2026-05-18 10:28:09 -07:00
konsisumer
226680500d fix(auth): improve xAI OAuth SSH hint with visual header and auto-detected host 2026-05-18 10:26:55 -07:00
briandevans
bf6eeb3f93 fix(xai-oauth): show "not received" page when loopback callback has no code
When xAI's auth backend fails to redirect (e.g. the German "We couldn't reach
your app" fallback shown in #27385), users sometimes navigate manually to the
bare loopback callback URL — `http://127.0.0.1:<port>/callback` with no query
string. The handler used to return 200 "xAI authorization received" for any
GET that hit the expected path, because `parse_qs("")` yields no `code` and no
`error`, leaving `result` untouched while the success page was still served.

The CLI's wait loop, of course, still saw no code and timed out with
`AuthError: xAI authorization timed out waiting for the local callback.`
The user is left looking at a browser tab that claims success and a terminal
that says failure — exactly the contradiction in #27385.

This change makes the empty-callback case return 400 with an explicit
"not received" page and a hint to retry `hermes auth add xai-oauth`. The
wait-loop semantics are unchanged: `result["code"]` and `result["error"]`
both stay None, so the CLI still raises a real timeout rather than treating
the bare hit as a successful callback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 10:26:00 -07:00
EloquentBrush0x
1fabd6e100 fix(error_classifier): classify xAI Grok entitlement SSE errors as auth
When xAI returns a subscription/entitlement error through an SSE
``type=error`` frame, ``_StreamErrorEvent`` is raised with
``status_code=None``.  This caused ``_classify_by_status`` (step 2 of
``classify_api_error``) to be skipped entirely, and the Grok-specific
phrases ("do not have an active Grok subscription", "out of available
resources") appeared in none of the message-pattern lists.  The error
fell through to ``FailoverReason.unknown (retryable=True)``, burning
``max_retries`` on every affected X Premium+ / SuperGrok user before
the agent stopped — and ``_is_entitlement_failure`` was never called
because it only fires under ``FailoverReason.auth``.

The HTTP 403 path already handled this correctly (``_classify_by_status``
returns ``auth/non-retryable`` for 403).  Add an explicit pattern block
at step 1 (highest priority, before the ``status_code`` guard) so both
code paths route to ``FailoverReason.auth, retryable=False,
should_fallback=True`` — matching the 403 path exactly.

Add three regression tests in ``Fix D`` section of
``test_codex_xai_oauth_recovery.py``:
- primary "do not have an active Grok subscription" phrase
- "out of available resources" + "grok" variant
- unrelated ``_StreamErrorEvent`` must not be reclassified
2026-05-18 10:24:13 -07:00
teknium1
bc77f79798 chore(release): AUTHOR_MAP entries for Fewmanism + Slimydog21 2026-05-18 10:23:13 -07:00
Fewmanism
0d63661702 fix: latch xAI OAuth callback result 2026-05-18 10:23:13 -07:00
Fewmanism
eac198b6d5 fix: make xAI OAuth callback server threaded 2026-05-18 10:23:13 -07:00
flamiinngo
5613dfea93 fix(security): redact xAI (Grok) API keys in logs
xAI is a first-class provider in hermes-agent with its own credential
pool entry (XAI_API_KEY / xai-oauth). API keys follow the format
xai-<60+ alphanumeric chars> and were absent from _PREFIX_PATTERNS in
agent/redact.py.

When a key appears raw in log output, tool results, or error messages,
it passed through completely unmasked. The ENV-assignment and Bearer
header patterns catch the most common cases, but a raw token in a
stack trace or debug print had no protection.

Verified before fix:
  redact_sensitive_text("using key xai-ABCD...rstu to call xAI", force=True)
  # "using key xai-ABCD...rstu to call xAI"  <- exposed

After fix:
  # "using key xai-AB...rstu to call xAI"    <- masked

Five unit tests added to TestXaiToken covering bare token masking,
env assignment, short-prefix false positive, company name false
positive, and visible prefix in masked output.
2026-05-18 10:21:22 -07:00
Wesley Simplicio
fae0fa4325 fix(tirith): suppress .app lookalike_tld false positives in warn verdicts
Tirith flags .app domains with a lookalike_tld finding because the TLD
"can be confused with file extensions". This is a false positive for
legitimate production APIs (e.g. api.example.app, lark.app).

Add _is_app_tld_finding() and a post-parse suppression block in
check_command_security(): if the only finding(s) on a warn verdict are
lookalike_tld entries for .app, downgrade the action to allow.

Mixed findings (e.g. .app + shortened_url) and block verdicts are
unaffected. Non-.app lookalike_tld findings (.zip, .exe, etc.) are
preserved.

Add 15 regression tests covering: .app-only suppression, mixed-finding
preservation, non-.app TLD preservation, block-verdict invariance, and
the helper's field-name and case-insensitivity behaviour.

Closes #24461
2026-05-18 10:20:07 -07:00
Teknium
1634397ddb
fix(compress): abort instead of dropping messages when summary LLM fails (#28102)
When auxiliary compression's summary generation returns None (aux model
errored, returned non-JSON, timed out, etc.) the compressor previously
still dropped every middle message between compress_start..compress_end
and replaced them with a static 'Summary generation was unavailable'
placeholder. The session kept going but the user silently lost N turns
of context for nothing.

New behavior: on summary failure, compress() aborts entirely — returns
the input messages unchanged and sets _last_compress_aborted=True. The
existing _summary_failure_cooldown_until gate (30-60s) keeps the aux
model from being burned on every turn. Auto-compress callers detect
the no-op (len(after) == len(before)) and stop looping. The chat is
'frozen' at its current size until the next /compress or /new.

Manual /compress (CLI + gateway) now passes force=True which clears
the cooldown so users can retry immediately after an auto-abort. If
the manual retry also fails, the user gets a visible warning telling
them nothing was dropped and how to retry.

- agent/context_compressor.py: compress() gains force= kwarg; failure
  branch sets _last_compress_aborted and returns messages unchanged
  instead of inserting placeholder.
- run_agent.py: _compress_context() detects abort, surfaces warning,
  skips session-rotation entirely, returns messages unchanged.
- cli.py + gateway/run.py: manual /compress paths pass force=True.
- gateway/run.py: hygiene + /compress handlers detect _last_compress_aborted
  and emit the new 'Compression aborted' warning (gateway.compress.aborted)
  instead of the old 'N historical messages were removed' message.
- locales/*.yaml: new gateway.compress.aborted key in all 16 locales.
- tests: updated to assert the abort contract (messages preserved,
  compression_count not incremented, abort flag set, no placeholder
  leaked). New test_force_true_bypasses_failure_cooldown covers the
  manual-retry path.
2026-05-18 10:19:40 -07:00
teknium
65e0c49b77 chore(release): add AUTHOR_MAP entry for glennc 2026-05-18 10:14:38 -07:00
glennc
9df9816dab feat(azure-foundry): add Microsoft Entra ID auth
Use azure-identity DefaultAzureCredential for keyless Foundry auth.

Preserve refreshable callable credentials through OpenAI and Anthropic client paths.

Add setup, doctor, auth status, docs, and tests for Entra auth.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-18 10:14:38 -07:00
Teknium
457fa913b8
chore(deps): regen uv.lock to match pinned versions in pyproject (#28094)
uv.lock drifted from pyproject.toml after the CVE bumps (#26830) and
the 0.14.0 release. The installer's hash-verified tier was failing
`uv pip sync --locked` and falling back to unlocked PyPI resolve,
producing two warnings on every fresh install.

Regen aligns the lockfile:
- aiohttp 3.13.4 -> 3.13.3 (matches messaging/slack/homeassistant/sms pin)
- anthropic 0.87.0 -> 0.86.0 (matches anthropic extra pin)
- hermes-agent 0.13.0 -> 0.14.0 (matches project version)

No behavioral changes. `uv lock --check` now passes.
2026-05-18 09:49:15 -07:00
teknium1
9cae9c0166 fix(aux): log sanitizer failures instead of silently swallowing them
Match the warning behavior of the parent main-agent path in
chat_completion_helpers.py — sanitizer failures should be visible
in logs, not silent.
2026-05-18 09:43:44 -07:00
EloquentBrush0x
2fae8fba9c fix(aux): strip pattern/format keywords from tool schemas on xAI Responses path
xAI's /responses endpoint rejects tool schemas that contain pattern or
format JSON Schema keywords with HTTP 400. chat_completion_helpers.py
already strips these for the main-agent xAI/xai-oauth path (lines
294-302), but _CodexCompletionsAdapter.create() — used for every xAI
OAuth auxiliary call (kanban decomposer, profile describer, etc.) —
passed raw tool schemas without sanitization.

MCP tools that carry pattern/format keywords (common for string fields)
silently caused every auxiliary call over xAI OAuth to fail with an
HTTP 400, while the main agent worked fine. Parity fix: call
strip_pattern_and_format() on the tool list before converting to
Responses API format, matching the main-agent guarantee.
2026-05-18 09:43:44 -07:00
EloquentBrush0x
502d03d5a3 fix(kanban): detect cycles in decompose_triage_task sibling-link pre-validation
decompose_triage_task inlines SQL INSERTs for atomicity and intentionally
bypasses link_tasks() — which calls _would_cycle() per edge.  If the LLM
emits a cyclic parent graph (e.g. A.parents=[1], B.parents=[0]) the DB
write succeeds but every involved child deadlocks in 'todo' forever:
recompute_ready() requires all parents to be done, which is impossible
when A waits for B and B waits for A.

Add a Kahn topological sort over the sibling parent indices in the
pre-validation block, before any DB writes.  Mirrors the cycle-safety
guarantee that link_tasks() provides for manually linked tasks.
2026-05-18 09:40:44 -07:00
Teknium
a86d2ad557
fix(kanban-dashboard): wire onValueChange on OrchestrationPanel Selects (#27893)
The dashboard SDK's <Select> is a shadcn-style popup that fires
onValueChange(value), not native onChange({target:{value}}). The file
even has a selectChangeHandler() helper at L213 documenting this:
"Older plugin code calls onChange({target:{value}}) which silently
never fires."

#24547 already fixed the bulk-reassign, workspace-kind, and new-task
parent selects. This patch covers the two OrchestrationPanel selects
introduced later in #27572 that regressed onto the same broken pattern:

  - OrchestrationPanel orchestrator_profile picker
  - OrchestrationPanel default_assignee picker

Users opened the popup, picked an option, and the popup closed without
firing a PUT to /orchestration — so the orchestrator profile and
default assignee dropdowns appeared totally inert.

Uses the same selectChangeHandler helper as the other working Selects
in the file for consistency.

Reported by Exaario.
2026-05-18 09:31:08 -07:00
teknium
f0c6d59148 fix(anthropic): scope MiniMax beta-strip to MiniMax only
Cherry-pick of @sharziki's #27022 routed Azure Foundry through
_requires_bearer_auth, which also triggered the MiniMax-specific
beta-strip in _common_betas_for_base_url — dropping the 1M-context
beta from Azure even though Azure needs it for 1M context.

Split the strip predicate: introduce _is_minimax_anthropic_endpoint
so the fine-grained-tool-streaming and context-1m strips only fire
for MiniMax hosts, leaving Azure's bearer-auth header swap intact
without losing 1M context.

Also add a regression test that asserts Azure gets Bearer auth,
the api-version query param, and the context-1m-2025-08-07 beta.
2026-05-18 09:27:18 -07:00
sharziki
73407b1e30 fix(auth): send Bearer auth for Azure Foundry anthropic_messages endpoints
Azure AI Foundry's Anthropic-style endpoint requires
`Authorization: Bearer` instead of `x-api-key`.  Add `azure.com` to
`_requires_bearer_auth()` so the existing Bearer path at line 586 fires
before the generic third-party branch sets `api_key` (x-api-key).

Fixes #26970
2026-05-18 09:27:18 -07:00
Wesley Simplicio
16abb74eab
fix(kanban): use selectChangeHandler for workspace, parent, and bulk-reassign selects (#24547)
SDK Select fires onValueChange(value) not onChange({target:{value}}), so
all three bare onChange handlers silently received undefined from e.target.

Replace raw onChange with selectChangeHandler() — the existing helper that
wires both onValueChange and a guarded onChange — so selections register
regardless of which event the SDK Select dispatches.

Closes #24520

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 10:48:21 -04:00
LeonSGP
4414a99d8c
fix(kanban): stop forcing dashboard text to all caps (#26413) 2026-05-18 10:35:18 -04:00
Brian D. Evans
6a20ad6c0a
fix(dashboard): constrain theme picker dropdown height so themes are scrollable (#25213) (#25220)
The header theme picker (`ThemeSwitcher`) renders a `role="listbox"` popup
with no `max-height` or overflow. With 20+ community themes installed under
`~/.hermes/dashboard-themes/`, the list extends past the viewport and themes
at the top or bottom are unreachable — the user reports only 15 of 26 themes
visible, with no scrollbar to access the rest.

Sibling switchers (`LanguageSwitcher`, `SlashPopover`) already cap their
listboxes (`max-h-80 overflow-y-auto` / `max-h-64 overflow-y-auto`); this
just brings the theme picker into line. Scoped to the component instead of
a global `div[role="listbox"]` CSS rule so other dropdowns aren't affected.

`70dvh` matches the user's tested workaround and the `dvh` unit handles
mobile browser UI chrome correctly (unlike `vh`).

Fixes #25213.

Co-authored-by: briandevans <252620095+briandevans@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 10:23:03 -04:00
duyua9
ac1536b19f
fix(web): render object config values structurally (#10949) 2026-05-18 10:03:25 -04:00
Austin Pickett
609c485fc6
Merge pull request #27971 from NousResearch/austin/fix/goal-statusbar
fix(tui): keep /goal verdict out of compact status row
2026-05-18 08:42:33 -04:00