fix: truncate long conversation title on mobile to keep tab toggle visible (#867)

* 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>

* fix: truncate long conversation title on mobile to keep tab toggle visible

When a conversation title was long enough it would overflow and hide the
RightPanelToggle button (the tab toggle) in the top-right of the mobile
header. Fixed by:

- Adding `min-w-0` to the left-side flex container in
  ConversationNameWithStatus so it can shrink and the toggle stays in view
- Adding `shrink-0` to the status dot wrapper so it never collapses
- Adding `min-w-0` to ConversationName's root div (flex item) to allow
  it to shrink within the parent
- Removing `w-fit max-w-fit` from the title div (both prevented
  `truncate` from ever activating) so the title now properly truncates
  with an ellipsis when too long
- Adding `shrink-0` to the ellipsis-button wrapper so it is always
  accessible regardless of title length

Fixes #848

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Tim O'Farrell
2026-05-28 07:41:05 -06:00
committed by GitHub
co-authored by openhands
parent a00ebe44f3
commit 082cbc2ddb
2 changed files with 8 additions and 5 deletions
@@ -59,8 +59,8 @@ export function ConversationNameWithStatus() {
return (
<div className="flex items-center justify-between w-full">
<div className="flex items-center">
<div className="group relative">
<div className="flex items-center min-w-0">
<div className="group relative shrink-0">
<DebugStackframeDot
className="ml-[3.5px] w-6 h-6 cursor-pointer"
color={statusColor}
@@ -134,7 +134,7 @@ export function ConversationName() {
return (
<>
<div
className="flex items-center gap-2 h-[22px] text-base font-normal text-left pl-0 lg:pl-1"
className="flex items-center gap-2 h-[22px] text-base font-normal text-left pl-0 lg:pl-1 min-w-0"
data-testid="conversation-name"
>
{titleMode === "edit" ? (
@@ -150,7 +150,7 @@ export function ConversationName() {
/>
) : (
<div
className="text-white leading-5 w-fit max-w-fit truncate"
className="text-white leading-5 truncate"
data-testid="conversation-name-title"
onDoubleClick={handleDoubleClick}
title={conversation.title || ""}
@@ -160,7 +160,10 @@ export function ConversationName() {
)}
{titleMode !== "edit" && (
<div ref={ellipsisAnchorRef} className="relative flex items-center">
<div
ref={ellipsisAnchorRef}
className="relative flex items-center shrink-0"
>
<EllipsisButton onClick={handleEllipsisClick} />
{contextMenuOpen && (
<ConversationNameContextMenu