Files
OpenHands/__tests__/utils/path-utils.test.ts
T
54bf794471 Sidebar nav + conversation list layout overhaul (#170)
* Rework home workspace UX for agent-server backend

- Drop the Repositories/Workspaces tab treatment on the home page. The
  agent-canvas build only ever talks to an agent-server backend, so
  RepoConnector now renders WorkspaceSelectionForm directly. The
  LaunchTabs component is removed; RepositorySelectionForm is left in
  place for any future cloud-backend variant.
- Add a 'Manage Workspaces' modal accessible from the workspace
  dropdown footer. It lists each saved workspace with a per-row
  Remove button (wired to useWorkspacesStore.removeWorkspace) and
  hides itself when there are no workspaces yet. Removing the
  currently selected workspace clears the form selection.
- Folder browser 'Use this folder' now adds only the chosen
  directory as a single workspace (named by its basename), instead
  of importing every immediate subdirectory. The button is enabled
  whenever a path is selected.
- Add HOME$MANAGE_WORKSPACES, HOME$MANAGE_WORKSPACES_EMPTY,
  HOME$REMOVE_WORKSPACE, HOME$DONE i18n keys and rephrase
  HOME$ADD_WORKSPACES to the singular '+ Add Workspace'.
- Update tests: remove obsolete LaunchTabs cases, rewrite the
  folder-import test to assert single-folder behavior, and add
  coverage for Manage Workspaces remove + hidden-when-empty.

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

* Workspace parents: dynamically list subdirectories

Adds a 'workspace parent' concept alongside individual workspaces:

- 'Add Workspace' modal footer now has two action buttons:
    * 'Add this directory' (primary): saves the navigated folder as a
      single workspace, as before.
    * 'Add all subdirectories' (secondary): saves the folder as a
      LocalWorkspaceParent instead. The parent itself is not selectable;
      its immediate subdirectories are listed dynamically wherever
      workspaces are shown.
- New useResolvedWorkspaces() hook merges static workspaces with the
  live subdirectory listing of every saved parent (via FilesService
  .searchSubdirs and useQueries). Static workspaces win on duplicate
  paths.
- ManageWorkspacesModal grows a 'Workspace parents' section that lets
  users remove parents (their dynamic children disappear with them) and
  shows the current resolved children indented underneath.
- WorkspaceDropdown takes an optional showManage prop so the Manage
  entry stays visible when only parents (whose children may not have
  loaded yet) exist.

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

* chore(test): remove launch-tabs.test.tsx orphaned by LaunchTabs deletion

The LaunchTabs component was deleted in 'Rework home workspace UX
for agent-server backend', but the test file that imports it from
`#/components/features/home/launch-tabs` was missed in that change
(it was added on main concurrently and pulled in via merge), leaving
the suite unable to resolve the import.

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

* Refactor sidebar nav and conversation list layout

- Sidebar: text-only nav (Conversations, Automations, Skills, Integrations,
  Settings with collapsible sub-menu), wider 220px column, BackendSelector
  pinned to bottom with '+ Add backend' footer + modal.
- Routes: '/' redirects to /conversations; /conversations renders the
  workspace/repo picker formerly at root; conversation list pane is always
  shown when /conversations* is active and is horizontally collapsible.
- Conversation list: blended bg (#13151a), condensed card padding, square
  corners, '+ New' workspace/repo popover at the top.
- Chat layout: title+model header now sits above the chat pane and tabs
  sit above the right pane; right pane extends flush to the bottom of
  the viewport.
- Root layout: drop top/bottom app padding (chat UI carries its own).
- Drop UserActions/profile menu and AddBackendMenuItem; their roles are
  now covered by the BackendSelector footer.

* Add missing i18n keys for Skills, Integrations, and New buttons

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

* Move conversation list into sidebar nav

- Move conversation list (with + New button) into the left-hand sidebar
  as a new `SidebarConversationList` section that scrolls independently
  from the rest of the nav.
- Widen the sidebar from 220px to 300px to fit the conversation cards.
- Move the BackendSelector from the bottom of the sidebar to just below
  the logo at the top.
- Remove the now-redundant `ConversationListPane` and
  `conversations-layout` route wrapper; the conversations and individual
  conversation routes now live directly under the root layout.

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

* Sidebar new-conversation popover: flat workspace list

Replace the embedded WorkspaceDropdown + Launch button in the
'+ New Conversation' popover above the conversation list with a
flat list. Each entry (a leading 'No workspace' option plus one
row per stored workspace) immediately starts a conversation when
clicked. The sticky '+ Add Workspace' / 'Manage Workspaces'
footer is kept.

Also rename the trigger label from 'New' to 'New Conversation'
via SIDEBAR$NEW_CONVERSATION.

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

* Move LLM model name from conversation lists/title to chat input

Remove the model name display from the sidebar conversation cards, the
home page recent-conversations list, and the conversation title bar.
Render it instead inside the chat input footer, immediately to the right
of the Tools button, via a new ChatInputModel component.

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

* Conversation list: clearer agent-status indicator, indent, workspace fallback, and 10s polling

- Replace the single-color status dot in the conversation list with four
  distinct visuals driven by the agent's execution_status:
    * FINISHED                  -> green check
    * RUNNING                   -> blinking green dot (animate-pulse)
    * PAUSED / IDLE / WAITING   -> gray dot
    * ERROR / STUCK             -> red dot
  Add COMMON$FINISHED, COMMON$WORKING, COMMON$PAUSED i18n keys for
  the new tooltips.

- Indent ConversationCardFooter (pl-[18px] = dot 10px + gap-2 8px) so the
  repo / workspace icon row aligns directly under the title text.

- When a conversation has no selected repository, fall back to the
  workspace working_dir basename (top-level folder name) with a folder
  icon, instead of showing "No Repository". Plumb workspace.working_dir
  through ConversationCard -> ConversationCardFooter -> NoRepository.

- usePaginatedConversations now refetches every 10s so titles, status
  dots, and timestamps stay fresh in both the sidebar panel and the
  home page recent-conversations list. React Query keeps it as a
  silent background refresh and pauses when the tab is hidden.

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

* Invalidate (don't remove) conversations cache on create

Using removeQueries on "user, conversations" wiped the paginated cache
prefix, dropping all loaded pages and forcing the sidebar list back into
its initial loading/skeleton state after every successful create.
Switching to invalidateQueries marks the data stale and refetches in the
background while keeping the existing list rendered, so scroll position
and loaded pages persist.

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

* old convos

* chore(lint): prettier nit in manage-workspaces-modal

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

* delint a bit

* Address workspace UX review feedback

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

* Fix backend selector modals

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

* Address remaining sidebar PR feedback

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

* fix: address remaining sidebar review feedback

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

* fix: resolve remaining sidebar review feedback

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

* Handle latest PR review follow-ups

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

* fix: satisfy CI lint rules

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

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-05-09 08:47:23 -07:00

57 lines
1.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { getPathBasename, stripWorkspacePrefix } from "#/utils/path-utils";
describe("getPathBasename", () => {
it("returns an empty string for empty or whitespace-only input", () => {
expect(getPathBasename("")).toBe("");
expect(getPathBasename(" ")).toBe("");
});
it("handles POSIX paths with and without trailing slashes", () => {
expect(getPathBasename("/workspace/project/agent-canvas")).toBe(
"agent-canvas",
);
expect(getPathBasename("/workspace/project/agent-canvas/")).toBe(
"agent-canvas",
);
});
it("handles Windows-style paths", () => {
expect(getPathBasename("C:\\Users\\me\\repo")).toBe("repo");
expect(getPathBasename("C:\\Users\\me\\repo\\")).toBe("repo");
});
it("returns an empty string for root paths", () => {
expect(getPathBasename("/")).toBe("");
expect(getPathBasename("C:\\")).toBe("");
});
it("preserves relative basenames", () => {
expect(getPathBasename("repo")).toBe("repo");
expect(getPathBasename("./repo")).toBe("repo");
});
});
describe("stripWorkspacePrefix", () => {
it("removes the /workspace/<name>/ prefix when present", () => {
expect(stripWorkspacePrefix("/workspace/repo/src/file.py")).toBe(
"src/file.py",
);
expect(
stripWorkspacePrefix("/workspace/my-project/components/Button.tsx"),
).toBe("components/Button.tsx");
});
it("returns an empty string when only the workspace root has a trailing slash", () => {
expect(stripWorkspacePrefix("/workspace/repo/")).toBe("");
});
it("leaves non-workspace or incomplete paths unchanged", () => {
expect(stripWorkspacePrefix("/workspace")).toBe("/workspace");
expect(stripWorkspacePrefix("/workspace/repo")).toBe("/workspace/repo");
expect(stripWorkspacePrefix("relative/path.ts")).toBe("relative/path.ts");
expect(stripWorkspacePrefix("")).toBe("");
});
});