Commit Graph

5264 Commits

Author SHA1 Message Date
Teknium
a80ec24fb0 Merge pull request #116291 from NousResearch/fix/boa-partial-ultra-pill
fix(desktop,tui): reasoning pill says ultra sends max on this route instead of a distinct Ultra level (#61634)
2026-09-19 14:29:44 -07:00
Teknium
f22b7cedf9 Merge pull request #116288 from NousResearch/feat/boa-partial-429-retry-at-reset
feat(desktop): 429 usage-limit card can schedule one retry for when the limit resets (#98852)
2026-09-19 14:29:23 -07:00
teknium1
171a1777b5 fix(desktop,tui): reasoning pill and effort rows say ultra sends max on this route
The Desktop pill, the catalog row meta and the effort radio row rendered a
clamped pick as a plain "Ultra", and the TUI status bar as "ultra" — presenting
a Hermes-internal step as a wire level the route does not have, while the CLI's
`/reasoning` shows "ultra (sends max on this route)" (#115876).

Both surfaces now read `session.info.reasoning_effort_wire`:
- Desktop pill / catalog row meta: compact "Ultra→Max", tooltip and aria-label
  "Effort: Ultra (sends Max on this route)" (new `modelOptions.sendsOnRoute`
  string in every locale, mirroring the CLI wording).
- Effort radio row for the selected clamped level: "Ultra (sends Max on this
  route)".
- TUI status bar: "ultra→max".
- Unknown wire ('' — not stamped yet, or an optimistic pick) or a verbatim
  level makes no claim, so nothing changes on routes that send the level as
  is. `setCurrentReasoningEffort` and the tile optimistic write clear the wire
  so a stale clamp never pairs with a new pick until the gateway re-stamps.

Docs: one sentence in the Desktop guide. Part of #61634.
2026-09-19 11:22:44 -07:00
teknium1
e2a02106cf feat(tui_gateway): session.info reports the wire level the route sends for the effort
`ultra` is a Hermes-internal ladder step that every route clamps (to `max` on
OpenAI-compatible wires, per-model on Codex). The CLI already says so via
`agent/reasoning_effort.py::effort_display_label` ("ultra (sends max on this
route)"), but the renderers only ever received `reasoning_effort`, so the
Desktop and TUI had nothing to label a clamp with and showed Ultra as a level
of its own.

`_session_info` now also emits `reasoning_effort_wire`: the result of the same
`clamp_effort(route_supported_efforts(provider, model))` the request path uses
("" when unset/none, equal when verbatim). Clients label a clamp from this
value alone instead of duplicating the Codex per-model effort tables in TS.

Contract: `SessionLiveInfo.reasoning_effort_wire` + regenerated apps/shared
outputs. Part of #61634.
2026-09-19 11:22:44 -07:00
teknium1
a169438178 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	hermes_cli/config_defaults.py
2026-09-19 11:22:01 -07:00
teknium1
6e1f5c9713 feat(desktop): 429 usage-limit card can schedule one retry for when the limit resets
The 429 error card already tells the user when the limit lifts (#115924:
`Limit resets at HH:mm (in 1h 05m)`), but they still had to be at the
keyboard at that moment to press Retry (#98852). The card now also offers
ONE `Retry when the limit resets (HH:mm)` button, driven by the same
`resets_at` the backend stamps on the failed turn.

Clicking it arms a client-side timer for the reset moment and swaps the
button for a live countdown (`Retrying at HH:mm — in 12m 03s`) plus a
Cancel control. When it fires it calls `aui.message().reload()` — the exact
call behind the Retry button — exactly once. If that retry 429s again the
card (and the button) simply reappear; nothing repeats unattended.

Deliberately minimal (maintainer option B): no `retry in 1h/3h/6h` menu,
no custom time picker, no persisted scheduler, no backend auto-resume. The
timer lives in the mounted card only: cancel, unmount, session switch, a
manual Retry or any new message (thread running / message no longer the
tail) all clear it, and closing the app fires nothing. The button is shown
only while the reset is ahead and inside `setTimeout`'s 2^31-1 ms ceiling
(a larger delay would fire immediately in browsers).

- apps/desktop/.../assistant-message.tsx::ScheduledRetryAction
- lib/error-surface.ts: formatResetClock / scheduledRetryDelayMs /
  formatCountdown (formatLimitReset now reuses the clock helper)
- i18n keys errorRetryAtReset / errorRetryScheduled /
  errorRetryScheduledCancel in types + en/zh/zh-hant/ja/ar
- docs: website/docs/user-guide/desktop.md error-card section
- tests: two invariant vitest cases with fake timers (fires once at
  resets_at and not before; Cancel/unmount never fire; past reset hides
  the button) — red on the base component, green here.
2026-09-19 11:20:10 -07:00
Teknium
ea94d88e25 Merge pull request #115851 from NousResearch/fix/boa-desktop-openai-custom-api-mode
fix(desktop): Custom Endpoints pick an API mode and keep /v1/models alias metadata (#93622, salvage #69824)
2026-09-19 11:16:54 -07:00
teknium1
7c2b81d320 chore: merge origin/main (resolve apps/desktop/src/components/assistant-ui/thread/assistant-message.tsx, website/docs/user-guide/desktop.md) 2026-09-19 10:54:43 -07:00
teknium1
4ff5d91ac7 chore: merge origin/main (resolve apps/desktop/src/app/settings/custom-endpoints-settings.tsx, apps/desktop/src/types/hermes.ts, hermes_cli/web_routers/config_env.py, tests/hermes_cli/test_web_server.py) 2026-09-19 10:54:40 -07:00
teknium1
ec2c4eeb80 chore: merge origin/main (resolve apps/desktop/src/lib/voice-client-direct.ts, hermes_cli/config_defaults.py, tools/voice_client_config.py, website/docs/user-guide/features/tts.md) 2026-09-19 10:54:38 -07:00
teknium1
0d3fe24dce docs(repo): contributor and desktop dev docs stop suggesting /tmp
Throwaway HERMES_HOME, perf JSON/cpuprofile outputs and typing-lag profiles now land in the Hermes scratch dir; the CONTRIBUTING anti-pattern mention keeps its literal with a no-tmp marker.
2026-09-19 10:44:26 -07:00
teknium1
956732d4a8 chore(evals,desktop): drop literal /tmp from eval harnesses and desktop scripts
Eval probes wrote fixtures, receipts and evidence to hard-coded /tmp paths and
two live A/B tasks literally instructed the model to work in /tmp. They now
derive locations from tempfile.gettempdir() / os.tmpdir() (env overrides kept),
usage examples use relative output names, and the desktop e2e screenshot dirs,
perf scripts and the short-session repro fixture stop naming /tmp. Also adds
the explicit encoding= the windows-footgun check wants in the touched files.
2026-09-19 10:44:26 -07:00
teknium1
471ef5f4c3 fix(desktop): direct dictation requests time out per stt.openai.timeout instead of hanging
The client-direct STT path in the Desktop (voice-client-direct.ts) issued a
bare fetch to the provider with no AbortSignal, so a slow or wedged
transcription endpoint left the microphone stuck on "transcribing" forever;
the gateway's own transcription client already has a deadline.

- tools/voice_client_config.py::_resolve_stt_client_config adds `timeout_s`
  (from `stt.openai.timeout`, default 60; groq/deepinfra riders inherit) to
  the direct STT config the gateway hands the Desktop.
- voice-client-direct.ts routes all three direct STT fetches
  (openai-multipart, xai-stt, elevenlabs-stt) through sttFetch, which aborts
  at that deadline and surfaces "Transcription timed out after Ns".
- Docs: desktop.md dictation paragraph notes the shared budget.

Part of #112939
2026-09-19 10:40:04 -07:00
teknium1
f30fe581a8 fix(desktop): Model settings label the main-model context window and expose the compression model timeout
The Model page field "Context Window" read like a MoA/auxiliary setting,
so a reporter changed it expecting the main model to keep 1,048,576
tokens of context and could not tell which model it governed. Reword the
label and description so it is unambiguously the MAIN chat model's
context-window override (tokens; 0 = detected value; does not affect
auxiliary/MoA models) in en and the ja/ru/zh/zh-hant/ar copies.

Expose auxiliary.compression.timeout (default 120 s, see
hermes_cli/config_defaults.py::_aux) in the Memory & Context section next
to the other compression fields so the /compress timeout can be raised
from the Desktop instead of only via config.yaml. The backend schema
already flattens the nested key (web_server_config.py::
_build_schema_from_config), and field-copy lookup handles 3-segment keys
via defineFieldCopy/fieldCopyForSchemaKey, so only SECTIONS, FIELD_LABELS,
FIELD_DESCRIPTIONS and the locale copies change.

Invariant vitest in helpers.test.ts (red on base): the Memory & Context
section carries auxiliary.compression.timeout with label+description, and
the model_context_length label names the main model.

Part of #69912
2026-09-19 10:37:22 -07:00
teknium1
74c2439af2 fix(desktop): error card 'Switch provider' opens the live session model menu
The provider error card's "Switch provider" button navigated to
Settings → Models, which only changes the default provider/model for NEW
sessions — the failed chat kept its broken provider, so the user had to
find the composer pill on their own to actually recover the turn.

The button now calls requestModelMenuToggle(), the same bus request the
`composer.modelPicker` hotkey uses: it opens the composer pill's live
model menu (pane under the pointer, else the active composer), whose
picks go through model.switch on THIS session. When no chat surface is
on screen (requestModelMenuToggle returns false) it falls back to the
Settings → Models deep link as before. No new RPC.

Also refreshes the ErrorRecoveryPlan.switchProvider doc comment and the
Desktop user-guide bullet describing the button.

Tests: two invariant vitests on the error card (menu opened, no
navigation / menu unavailable → Settings deep link); both fail on base
where the click always navigates.

Part of #95066
2026-09-19 10:36:50 -07:00
teknium1
0830b7f741 fix(desktop): cap client-direct STT uploads at 60s
The three direct STT fetch() calls in voice-client-direct.ts carried no
AbortSignal, so a hung provider stalled dictation forever and the gateway
side stt timeout never applied to this surface. Each upload now carries
AbortSignal.timeout(60_000), mirroring the shared 60s STT default; the
configured stt timeout is not part of the voice client config today, so
the constant stands in for it (noted as follow-up in the PR body). One
existing vitest now asserts the signal is present (red without it).
2026-09-19 10:12:15 -07:00
teknium1
935eef50ef fix: pin consent_attestation on the Desktop client-direct TTS path
resolve_client_voice_config() now asserts tts.extra_body is {} when no
tts.openai extras are set and {consent_attestation: ...} when it is, and
the Desktop synthesizeSpeechClientDirect test asserts the spread field
reaches the openai speech request body. Removing either wiring
(extra_body kwarg in _resolve_tts_client_config, or the ...tts.extra_body
spread in voice-client-direct.ts) now turns the respective test red.
2026-09-19 10:10:29 -07:00
liuhao1024
f16083763a fix: forward tts.openai.consent_attestation to every OpenAI-compatible TTS request
Self-hosted OpenAI-compatible TTS servers reject cloned voices with
400 consent_required unless the JSON body carries `consent_attestation`;
Hermes never sent it, so the request failed and TTS silently fell back to
Edge. Add an optional `tts.openai.consent_attestation` key (default "",
nothing sent) and forward it via `extra_body` from the one place that
already knew about `lang_code`: a small `_openai_extra_body()` builder in
tools/tts_tool_openai.py, now shared by the whole-file path
(`_generate_openai_tts`), the chunked streamer (`OpenAIStreamer.stream`,
which previously forwarded neither field) and the desktop client-direct
voice config (`extra_body` on the openai-speech wire, spread into the
request body by voice-client-direct.ts).

Slim redo of PR #99782 by @liuhao1024 (pre-refactor tts_tool.py base,
no config default / docs), credited as author.

Fixes #99775
2026-09-19 10:10:29 -07:00
teknium1
fbcc605956 fix(desktop): require the provider slug in setOnboardingModel
The confirm card is the only caller and always forwards the picked model's
provider, so the fallback to the sign-in provider was dead code that would
silently reintroduce the mispairing if a caller ever omitted it.
2026-09-19 10:09:53 -07:00
teknium1
be81cf7d4d fix(desktop): pin the cross-provider onboarding pick test to the confirm card's picker wiring
The two store tests drove setOnboardingModel(model, 'nous', ...) directly, so
reverting only the ConfirmingModelPanel onSelect hunk (the actual root-cause
fix) left both green. Replace the happy-path store test with one that renders
FlowPanel in confirming_model, fires the picker's onSelect with a Nous model,
and asserts the /api/model/set body carries provider 'nous' plus the card
relabel. The failure-revert store test stays as the second invariant.
2026-09-19 10:09:53 -07:00
teknium1
b7ec75fc20 test(desktop): keep the two invariant cases for the onboarding model pick
Trim the salvaged suite to the two behaviours the fix guarantees: a
cross-provider pick persists against (and re-labels the card with) the
provider that serves the model, and a failed persist reverts model,
provider and label together. The same-provider and model-only call
shapes are exercised by the existing flow and add no invariant of their
own.
2026-09-19 10:09:53 -07:00
Derek Moore
c25892c8b8 fix(desktop): persist onboarding model change against the picked model's provider
The confirming-model step's Change picker lists models from every configured
provider, but the onSelect handler dropped the picked model's provider slug
and setOnboardingModel persisted the assignment against the just-signed-in
provider. Picking e.g. a deepseek model from Nous Portal after adding OpenAI
OAuth wrote provider=openai + model=deepseek/... to config, so new sessions
errored with "provider doesn't have the selected model" and the Model
settings showed a mismatched pair.

Thread the picker selection's provider (and display name) into
setOnboardingModel, persist against that provider, and keep the confirm
card's providerSlug/label in sync so the screen reflects the provider that
actually serves the model. Revert model/provider/label on failed persist.
2026-09-19 10:09:53 -07:00
teknium1
38029a5204 fix(desktop): trim voice picker tests to invariants; document ElevenLabs model ids
Follow-up to the salvaged #94013 hunk: keep two invariant cases in
voice-field-visible.test.ts (unset provider falls back to edge/local; an
explicit STT provider shows only its own fields) and one suggestion pin
(eleven_v3 is offered, mirroring tools/tts_tool_delivery.py's model table).
Drop the option-list change-detector and the gpt-transcribe pin — that
option was already on main, the bug was only that the row never rendered.

The regex only ever matches tts|stt, so the fallback ternary collapses to
edge/local (also clears the padding-line lint warning). Voice docs now say
the Desktop field accepts any ElevenLabs model id. Adds the contributor
email mapping for the cherry-picked commit's author.
2026-09-19 10:08:46 -07:00
Ramiro Rivera
71df3e5aee fix(desktop): expose ElevenLabs v3 and gpt-transcribe in Voice pickers
The ElevenLabs model field was a closed 3-item select that omitted
eleven_v3. Make it a suggestion combobox like other voice/model IDs.
Treat unset TTS/STT providers as the backend defaults (edge/local) so
nested model fields stay visible. gpt-transcribe was already listed
but hidden until STT provider was explicitly openai.

Closes #94012
2026-09-19 10:08:46 -07:00
teknium1
754d229bea fix(dashboard): a typed-root 404 no longer hides the /v1 key rejection; pin the Settings Test URL rewrite
_probe_openai_compatible_models kept the FIRST failure, so a server that lives at /v1 and
wants a key (root 404, /v1/models 401) reported 'HTTP 404' instead of 'rejected the API key'.
A later non-success now replaces a retained 404 and is reported against the candidate that
produced it. Also adds the Settings vitest: after Test the URL input shows the base that
actually served /models, so deleting the form rewrite goes red (#65488).
2026-09-19 10:04:52 -07:00
teknium1
588bc72bc8 test(desktop): onboarding persists the resolved local endpoint URL (#65488)
Adds a vitest case for saveOnboardingLocalEndpoint that mocks
/api/providers/validate answering with `resolved_base_url` set to the
`/v1` variant of a bare host root typed by the user, and asserts the
subsequent /api/model/set body carries that resolved URL rather than the
URL as typed. This guards the salvaged #65489 behaviour that Save stores
the base that actually served /models, since the runtime POSTs
{base_url}/chat/completions verbatim and a host root that only
"detected" via /v1/models would 404 every chat.
2026-09-19 10:04:52 -07:00
teknium1
9202bae546 fix(dashboard): custom endpoint validation persists the base URL that served /models (#65488)
A custom OpenAI-compatible endpoint typed without `/v1` in the Desktop
onboarding or Settings > Custom endpoints flow was probed only at
`{base}/models`, while the CLI's probe_api_models falls through to the
`/v1` alternate. Whatever the probe reported, the URL was saved verbatim
and the runtime POSTs `{base_url}/chat/completions` to it, so a server
that only serves `/v1/*` 404'd every chat request.

Both dashboard validators (`/api/providers/validate` OPENAI_BASE_URL
branch and `/api/providers/custom-endpoints/validate`) now share one
probe that tries the URL as entered and then its `/v1` variant (or the
stripped variant), and return `resolved_base_url` — the base that
actually served the model list. The Desktop onboarding persists that
URL, and the Settings "Test" rewrites the form's URL to it so the
following Save stores a URL chat can reach.

Co-authored-by: Jeongseok Kang <jskang@lablup.com>
2026-09-19 10:04:52 -07:00
teknium1
1fc8887c6e fix(desktop): the error card names a WAF block and the User-Agent fix instead of 'retry in a moment'
upstream_blocked had no ERROR_CODE_KEYS entry or errorCodes copy, so the Desktop card fell
back to the provider-layer 'retry' body — wrong for a block a retry can never heal. Same
guidance as the CLI/TUI copy: set a User-Agent via the provider's extra_headers, or switch
provider. Other locales carry no errorCodes table and fall back to en. The Python fallback
set agent/error_surface.py::_NON_RETRYABLE_REASONS learns the reason too.
2026-09-19 09:57:21 -07:00
teknium1
5b27b4a628 fix(desktop): admit the near-deadline 'still waiting on' wait notice in the status row
providerWaitText only matched frames starting with 'waiting on', so the
near-deadline update minted by agent/chat_completion_wait_notice.wait_notice_text
('⏳ still waiting on <model> — …') was rejected and Desktop kept the stale first
notice until the reconnect. Widen the matcher; pin it with the exact string the
helper produces.
2026-09-19 09:53:48 -07:00
teknium1
604d8803a1 fix(tui-gateway): session.usage RPC ships provider account limits (account_lines)
The CLI/TUI slash worker and gateway /usage now render Codex quota windows, but the
Desktop usage feed reads the `session.usage` RPC, which returned only Nous
`credits_lines`. Add `account_lines` (the same `render_account_usage_lines` block,
fetched against the session's live route or the configured `model.provider` when no
agent is built) and render it in the Desktop `renderRpcResult` ahead of credits.
Fail-open like the credits block.
2026-09-19 09:40:23 -07:00
teknium1
50638799b4 chore: stack on #115828 to resolve config_env.py conflict
validate_custom_endpoint resolves the base via #115828 _probe_openai_compatible_models first, then runs the
transport probe (_probe_transport_route / _auto_api_mode) against the resolved base and returns models,
model_details, transport_checked and resolved_base_url; TS keeps both resolvedBaseUrl update + transport notify.
2026-09-19 03:36:30 -07:00
teknium1
7845213b4d chore: stack on #115840 to resolve gateway/streaming_tts_consumer.py conflict
StreamingTTSConsumer.__init__ keeps SentenceChunker.from_config(tts_config) (min_len, #115846)
and #115840's _streamer_format() helper for the provisional audio format refreshed at the
first PCM chunk, instead of the inlined AudioFormat derivation.
2026-09-19 03:35:50 -07:00
teknium1
f97f8d5fd5 fix(desktop): remove a half-installed get-windows dir before the workspace npm install (#90829)
An interrupted extract leaves node_modules/get-windows without package.json
and npm never revisits an existing directory, so the tree stayed broken on
every later update until a manual `npm install get-windows`. Delete such a
dir (workspace hoist or app-local copy) in _install_desktop_workspace_deps
before npm runs so the same update re-extracts it; the staging repair hint
now points at `hermes desktop --force-build` instead of the manual install.

Also drops the unused `exists` injection on findHalfInstalledGetWindowsDir
and the stale "exported for tests" note on missingGetWindowsWarning.
2026-09-19 02:32:35 -07:00
teknium1
048e7b974f fix(desktop): degrade instead of failing the build when get-windows lacks its binding or helper
The unresolvable-package case already degraded, but a half-extracted install
(#90829) that keeps package.json while losing lib/binding (win32) or the
macOS helper (darwin) still threw from stageGetWindowsInto and before-pack
rethrew, so the Desktop rebuild died on the optional dep anyway. Stage the
fail-soft JS surface without a binding on win32 (the win32-arm64 precedent),
skip staging on darwin without the helper, and treat a failing native
installer the same way; the runtime reports read_window_below as unavailable
in every one of those states. The classify gate still refuses a present
binary compiled for the wrong platform.
2026-09-19 02:32:35 -07:00
teknium1
7ee8f5528d fix(desktop): name the half-installed get-windows dir and its repair when staging degrades
Follow-up to the cherry-picked #109251 (which stops a missing optional
get-windows from killing `npm run build` on win32-x64/darwin):

- The reporter's install (#90829) was not "npm skipped an optional dep" but a
  Windows in-place update interrupted by a running Desktop/gateway
  (TAR_ENTRY_ERROR): node_modules/get-windows exists with its binding but no
  package.json, so require.resolve fails while npm never revisits the
  directory. Degrading silently would leave read_window_below dead forever.
  `findHalfInstalledGetWindowsDir` walks the same node_modules ancestors
  require.resolve does; when the dir is there the warning names it and the
  repair (`npm install get-windows --save-exact` in apps/desktop, then
  `hermes desktop --force-build`).
- Warning text lives in `missingGetWindowsWarning` (pure, exported) and the
  locator is injectable, so tests assert behaviour instead of source text.
- Tests: the old per-platform "fails when absent" cases are replaced by one
  every-platform degrade invariant and one half-install → repair-hint case
  (both red on origin/main). Docs: desktop.md build-troubleshooting note.
2026-09-19 02:32:35 -07:00
salch-cred
2a9ba25994 fix(desktop): do not fail build if get-windows is missing on any platform 2026-09-19 02:32:35 -07:00
teknium1
7009611013 fix(desktop): document the afterExtract ordering, trim tests, refresh stale hook references
Follow-up to the salvaged #106846 commit (@JoaoMarcos44):

- after-extract.mjs: spell out WHY the stamp moved (electron-builder's
  beforeCopyExtraFiles rebuilds the PE with resedit for the ELECTRONASAR
  resource; rcedit then cannot commit to that exe, deterministically —
  #105629), and why disableAsarIntegrity was not taken.
- after-extract.test.mjs: two invariants — the hook wiring (afterExtract set,
  afterPack unset, ASAR integrity still on) and the stamp target
  (electron.exe on win32, nothing on other platforms). Red on origin/main.
- set-exe-identity.mjs / scripts/install.ps1: comments still named the
  afterPack hook / after-pack.mjs.
2026-09-19 02:31:27 -07:00
joaomarcos
168e0f78ad fix(desktop): order PE stamping before ASAR integrity 2026-09-19 02:31:27 -07:00
Siddharth Balyan
633dda6d7f refactor(desktop): the Capabilities page is a module at /capabilities, one folder per tab (#115054)
* refactor(desktop): Capabilities is a module at /capabilities

The Capabilities page lived in `app/skills/`, was exported as `SkillsView`
and was routed at `/skills`, although Skills is only one of its four tabs.
The name sent every reader to the wrong place.

- `app/skills/` becomes `app/capabilities/`, with one folder per topic:
  `skills/`, `plugins/`, `mcp/`, and `catalog/` for the browser that the
  skills and plugins tabs share.
- `SkillsView` becomes `CapabilitiesView`, in the plugin SDK too. The
  bundled bots plugin reads the new export name.
- The route, its id and its view become `/capabilities` and
  `capabilities`. The keybind action becomes `nav.capabilities` and the
  sidebar item id follows, with its i18n keys in every locale.
- The `hermes://open/...` link that the plugin notice fires follows.

No alias is kept for the old route. A remembered `/skills` route no longer
matches a page; the restore path already drops a route it cannot validate
and opens the last session.

No behaviour change otherwise.

* refactor(desktop): each Capabilities tab owns its list, detail and writes

`CapabilitiesView` was one 770-line function. It held the tab routing, the
whole Skills tab and the whole Tools tab, while the Plugins and MCP tabs
already lived in their own files.

- `capabilities/index.tsx` is the page shell: tab selection, the search
  header, the profile and connection scope, the refresh hotkey, and one
  table that maps a tab to its component (1190 lines down to 230).
- `skills/` and `toolsets/` each hold their tab, detail pane, data hooks and
  helpers. `scope-selector.tsx` holds the scope hook and its selector.
  `primitives.tsx` holds what two tabs share.
- The shell still fetches the two installed lists, because the tab pills
  count them for the tab the user is not on. Query keys are unchanged;
  `store/hub-actions.ts` imports the skills key instead of copying it.
- Every tab is keyed on the scope, so a profile or connection switch mounts
  a fresh tab. This replaces the epoch counters and manual resets. One small
  change follows: a return to a scope seen before selects the first row, not
  the row selected last time.
2026-09-19 08:35:58 +00:00
teknium1
645e9298b6 feat(error-surface): 429 error card shows when the usage limit resets
A "HTTP 429: The usage limit has been reached" turn offered only Retry and
never said when a retry would work, so users guessed or babysat the app
(#98852). The provider already tells us: Retry-After / resets_at /
retry_after are parsed into the turn's error context (extract_api_error_context)
and honoured by the backoff, but the datum died there.

- agent/turn_recovery.py::_stamp_limit_reset: both terminal paths
  (max_retries_exhausted_result, nonretryable_client_error_result) stamp
  failure_resets_at (epoch s) on the failed result and append one plain line
  ("Limit resets at 14:05 (in 1h 00m).") to final_response, which every text
  surface (CLI, Ink TUI, messaging gateway) renders.
- agent/error_surface.py: result path forwards failure_resets_at as
  surface.resets_at; the exception path derives it from the same context.
- tui_gateway/contracts/events.py::ErrorSurface.resets_at + regenerated
  apps/shared gateway-contract outputs.
- apps/desktop lib/error-surface.ts: parse resets_at -> resetsAt,
  formatLimitReset("HH:mm (in 1h 05m)", null once passed), diagnostics line;
  the error card renders "Limit resets at …" next to Retry (i18n copy in every
  full locale).
- Docs: website/docs/user-guide/desktop.md error-card section.

Informational only: no scheduled or automatic retry is added — firing a turn
unattended on a subscription is the maintainer's call (#98872, #103048).
2026-09-19 01:34:01 -07:00
teknium1
ab241af386 fix(desktop): custom endpoint Test probes the transport route, not just /v1/models
A Responses-only (or Anthropic-compatible) host lists models on GET /models and
then 404s every POST /chat/completions, so Test passed and the first chat
failed (#93622, item 2). validate_custom_endpoint now keeps the probe client
open and, after the models GET, POSTs a one-token request to the route of the
pinned api_mode — or of the mode the runtime's URL auto-detect resolves to
(_detect_api_mode_for_url, fallback chat_completions, the same order
runtime_provider_custom uses). 404/405/501 fails validation with a message that
names the transport and the route; 200/400/401/422/429 prove the route exists;
a network error or timeout stays inconclusive so a local server still loading
its model is not blocked. The response reports the mode it checked as
transport_checked; the Desktop success toast names it so an auto-detected mode
is visible before Save.

The Providers-pane OPENAI_BASE_URL probe returns model_details next to models
too, so an alias picked there keeps its canonical_model / reasoning_effort.

Co-authored-by: SacrEllfarch <2091538824thx@gmail.com>
Co-authored-by: JackLee992 <124239570+JackLee992@users.noreply.github.com>
Co-authored-by: fangliquanflq <fangliquan@qq.com>
2026-09-19 01:32:20 -07:00
teknium1
744945b3ba fix: honour tts.streaming.min_len on the Desktop client-direct TTS path
The Desktop's client-direct playback (first rung of the ladder, ahead of
the /api/audio/speak-stream relay) has its own sentence cutter with a
hard-coded 24-char floor, so a short CJK opener was still buffered there
although the PR promised the knob on every surface. GET
/api/audio/voice-config now carries the resolved tts.streaming.min_len
in the direct TTS block (tools/voice_client_config.py), and
openClientDirectSpeechSession passes it to cutSentences; older backends
without the key keep the historical 24.

Tests: test_elevenlabs_tts_direct_carries_voice_and_model asserts the
field; one vitest asserts a 7-char CJK opener is cut alone at min_len 6
and rides with sentence two without the key. Both red before wiring.
2026-09-19 01:31:22 -07:00
teknium1
088e8278d1 fix(dashboard): a typed-root 404 no longer hides the /v1 key rejection; pin the Settings Test URL rewrite
_probe_openai_compatible_models kept the FIRST failure, so a server that lives at /v1 and
wants a key (root 404, /v1/models 401) reported 'HTTP 404' instead of 'rejected the API key'.
A later non-success now replaces a retained 404 and is reported against the candidate that
produced it. Also adds the Settings vitest: after Test the URL input shows the base that
actually served /models, so deleting the form rewrite goes red (#65488).
2026-09-19 01:29:04 -07:00
teknium1
4b53209983 test(desktop): onboarding persists the resolved local endpoint URL (#65488)
Adds a vitest case for saveOnboardingLocalEndpoint that mocks
/api/providers/validate answering with `resolved_base_url` set to the
`/v1` variant of a bare host root typed by the user, and asserts the
subsequent /api/model/set body carries that resolved URL rather than the
URL as typed. This guards the salvaged #65489 behaviour that Save stores
the base that actually served /models, since the runtime POSTs
{base_url}/chat/completions verbatim and a host root that only
"detected" via /v1/models would 404 every chat.
2026-09-19 00:44:40 -07:00
teknium1
363b8a6fcb fix(desktop): custom endpoints pin an API mode and keep /v1/models alias metadata
Settings > Custom Endpoints assumed Chat Completions: the form, its types, the
update payload and _write_custom_endpoint carried no api_mode, so a
Responses-only (or Anthropic-compatible) host validated fine on /models and
then 404'd on every POST /chat/completions. Validation also flattened each
/v1/models row to a bare id, so a reasoning alias like gpt-5.6-sol-high
(canonical_model + reasoning_effort) was saved as a literal upstream model.

- Desktop form: API Mode segmented control (Auto-detect / Chat Completions /
  Responses API / Anthropic Messages — the same set `hermes model` offers);
  threaded through toPayload, hydrated from GET read-back.
- CustomEndpointUpdate.api_mode (Literal) persisted as providers.<id>.api_mode,
  the key the CLI writes and the runtime reads; None (older UI) leaves a
  hand-written mode alone, "" clears it. GET rows report api_mode.
- validate returns model_details (id / canonical_model / reasoning_effort)
  next to the unchanged string[] models; _parse_model_ids is now a projection
  of _parse_model_entries.
- Save keeps the alias metadata in providers.<id>.models and, when the picked
  default is an alias, persists the canonical model and pins its effort under
  agent.reasoning_overrides (the resolve_reasoning_config chokepoint).

Fixes #93622
Supersedes #69824 (@SacrEllfarch), #82148 (@JackLee992), #93693 (@fangliquanflq)
2026-09-19 00:17:19 -07:00
teknium1
086628ad8a Revert "feat(ui): support icon-only segmented controls"
This reverts commit 2e05fcf524.
2026-09-19 00:11:36 -07:00
teknium1
8bd0da2b8c Revert "feat(desktop): restore native skill and plugin catalogs"
This reverts commit cbd76e4ea3.
2026-09-19 00:11:36 -07:00
teknium1
f923398fee Revert "feat(desktop): browse catalogs as cards with a saved list option"
This reverts commit 4e9d3c713a.
2026-09-19 00:11:36 -07:00
teknium1
859c883ce5 fix(dashboard): custom endpoint validation persists the base URL that served /models (#65488)
A custom OpenAI-compatible endpoint typed without `/v1` in the Desktop
onboarding or Settings > Custom endpoints flow was probed only at
`{base}/models`, while the CLI's probe_api_models falls through to the
`/v1` alternate. Whatever the probe reported, the URL was saved verbatim
and the runtime POSTs `{base_url}/chat/completions` to it, so a server
that only serves `/v1/*` 404'd every chat request.

Both dashboard validators (`/api/providers/validate` OPENAI_BASE_URL
branch and `/api/providers/custom-endpoints/validate`) now share one
probe that tries the URL as entered and then its `/v1` variant (or the
stripped variant), and return `resolved_base_url` — the base that
actually served the model list. The Desktop onboarding persists that
URL, and the Settings "Test" rewrites the form's URL to it so the
following Save stores a URL chat can reach.

Co-authored-by: Jeongseok Kang <jskang@lablup.com>
2026-09-19 00:06:15 -07:00
kshitijk4poor
603007ead3 refactor(desktop): folder-derived project name lives in the input only
Drop the derived resolvedName fallback: pickFolder already writes the folder
name into the input, so the fallback only fired when the user cleared the
field after picking, and then Create stayed enabled with a name the dialog
never showed. Submit and the button gate go back to name.trim().

The dialog test asserts the fields this flow owns (folders, name) instead of
the exact createProject argument object, which would break on any harmless
extra kwarg (dropPlacement already rides along); beforeEach sits next to the
mocks it resets.
2026-09-19 12:19:30 +05:30