* fix(skills): save disabled_skills when settings has no prior disabled_skills field
The hydration effect in SkillsSettingsScreen only set hasHydratedInitialSettings=true
when settings?.disabled_skills was truthy. When no skills had ever been disabled,
the server omits the field entirely (undefined), so the condition was always false:
if (settings?.disabled_skills) { ... } // skipped when field is absent
Because settings?.disabled_skills is undefined both before *and* after settings
load, the dependency array [settings?.disabled_skills] never changed value on
load either, so the effect never ran a second time. hasHydratedInitialSettings
stayed false, and the save effect's early-return guard blocked every toggle.
Fix by gating on settingsLoading instead and defaulting the missing field with
?? []. Also add a localStorage stub for Node.js 25+ which ships a built-in
localStorage that is non-functional without --localstorage-file, breaking the
zustand persist middleware in the test environment.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix(tests): use mockResolvedValue(true) for saveSettings spy
saveSettings returns Promise<boolean>, not Promise<void>, so
mockResolvedValue(undefined) caused TS2345 type errors in CI.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix(skills): persist disabled_skills to localStorage for local backend
The local agent-server has no concept of disabled_skills and its
PATCH /api/settings strips the field before sending. As a result,
toggling a skill off appeared to work in the UI but was never
persisted -- the value was silently discarded on every save and
page refresh reverted the toggle.
Fix by mirroring the existing app-preferences pattern:
- saveSettings (local path): call writeStoredDisabledSkills before
stripping the field from the PATCH payload.
- transformApiResponse: call readStoredDisabledSkills and overlay it
onto the partial Settings returned from the API, so every getSettings
call (cached or fresh) surfaces the stored value.
Export DISABLED_SKILLS_STORAGE_KEY so tests can reference the key
without duplicating the string literal.
Co-authored-by: openhands <openhands@all-hands.dev>
---------
Co-authored-by: openhands <openhands@all-hands.dev>
* Gate browser tool requests on server support
Co-authored-by: openhands <openhands@all-hands.dev>
* Support usable_tools browser gating
Co-authored-by: openhands <openhands@all-hands.dev>
* Remove available_tools fallback
Co-authored-by: openhands <openhands@all-hands.dev>
* QA artifacts: live verification GIFs against agent-server PR #3028
Captured against two live agent-server containers (one built from
ghcr.io/openhands/agent-server:a3c0247-python with chromium, one with
chromium binaries removed):
- browser-tool-not-advertised: server's /server_info.usable_tools omits
browser_tool_set; frontend POST /api/conversations sends
agent.tools = ["terminal", "file_editor", "task_tracker"]
- browser-tool-advertised: server advertises browser_tool_set; frontend
POST /api/conversations sends
agent.tools = ["terminal", "file_editor", "task_tracker", "browser_tool_set"]
Co-authored-by: openhands <openhands@all-hands.dev>
* test: drain microtasks in vitest afterEach to avoid late-rejection flake
After main reverted HeroUI v3 to v2 (#124) and PR #64 added new tests
that change the file ordering, CI started flaking with unhandled
rejections like 'ReferenceError: window is not defined' originating
inside react-dom's resolveUpdatePriority. The root cause is that HeroUI
v2 components (e.g. Tooltip used by ConversationStatusDot) wrap content
in framer-motion's LazyMotion, which queues a setState in a microtask
that can resolve after the test file's jsdom environment is torn down.
Awaiting two microtask ticks at the end of every afterEach lets those
queued updates settle while window is still defined, eliminating the
spurious failures without affecting tests that use fake timers (we use
Promise.resolve() rather than setTimeout(0)).
Co-authored-by: openhands <openhands@all-hands.dev>
---------
Co-authored-by: openhands <openhands@all-hands.dev>