Files
OpenHands/public/mockServiceWorker.js
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

350 lines
8.9 KiB
JavaScript

/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker.
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
*/
const PACKAGE_VERSION = '2.14.2'
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
addEventListener('install', function () {
self.skipWaiting()
})
addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim())
})
addEventListener('message', async function (event) {
const clientId = Reflect.get(event.source || {}, 'id')
if (!clientId || !self.clients) {
return
}
const client = await self.clients.get(clientId)
if (!client) {
return
}
const allClients = await self.clients.matchAll({
type: 'window',
})
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
})
break
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: {
packageVersion: PACKAGE_VERSION,
checksum: INTEGRITY_CHECKSUM,
},
})
break
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId)
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: {
client: {
id: client.id,
frameType: client.frameType,
},
},
})
break
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId)
const remainingClients = allClients.filter((client) => {
return client.id !== clientId
})
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister()
}
break
}
}
})
addEventListener('fetch', function (event) {
const requestInterceptedAt = Date.now()
// Bypass navigation requests.
if (event.request.mode === 'navigate') {
return
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (
event.request.cache === 'only-if-cached' &&
event.request.mode !== 'same-origin'
) {
return
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been terminated (still remains active until the next reload).
if (activeClientIds.size === 0) {
return
}
const requestId = crypto.randomUUID()
event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
})
/**
* @param {FetchEvent} event
* @param {string} requestId
* @param {number} requestInterceptedAt
*/
async function handleRequest(event, requestId, requestInterceptedAt) {
const client = await resolveMainClient(event)
const requestCloneForEvents = event.request.clone()
const response = await getResponse(
event,
client,
requestId,
requestInterceptedAt,
)
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
const serializedRequest = await serializeRequest(requestCloneForEvents)
// Clone the response so both the client and the library could consume it.
const responseClone = response.clone()
sendToClient(
client,
{
type: 'RESPONSE',
payload: {
isMockedResponse: IS_MOCKED_RESPONSE in response,
request: {
id: requestId,
...serializedRequest,
},
response: {
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
headers: Object.fromEntries(responseClone.headers.entries()),
body: responseClone.body,
},
},
},
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
)
}
return response
}
/**
* Resolve the main client for the given event.
* Client that issues a request doesn't necessarily equal the client
* that registered the worker. It's with the latter the worker should
* communicate with during the response resolving phase.
* @param {FetchEvent} event
* @returns {Promise<Client | undefined>}
*/
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId)
if (activeClientIds.has(event.clientId)) {
return client
}
if (client?.frameType === 'top-level') {
return client
}
const allClients = await self.clients.matchAll({
type: 'window',
})
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible'
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id)
})
}
/**
* @param {FetchEvent} event
* @param {Client | undefined} client
* @param {string} requestId
* @param {number} requestInterceptedAt
* @returns {Promise<Response>}
*/
async function getResponse(event, client, requestId, requestInterceptedAt) {
// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const requestClone = event.request.clone()
function passthrough() {
// Cast the request headers to a new Headers instance
// so the headers can be manipulated with.
const headers = new Headers(requestClone.headers)
// Remove the "accept" header value that marked this request as passthrough.
// This prevents request alteration and also keeps it compliant with the
// user-defined CORS policies.
const acceptHeader = headers.get('accept')
if (acceptHeader) {
const values = acceptHeader.split(',').map((value) => value.trim())
const filteredValues = values.filter(
(value) => value !== 'msw/passthrough',
)
if (filteredValues.length > 0) {
headers.set('accept', filteredValues.join(', '))
} else {
headers.delete('accept')
}
}
return fetch(requestClone, { headers })
}
// Bypass mocking when the client is not active.
if (!client) {
return passthrough()
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return passthrough()
}
// Notify the client that a request has been intercepted.
const serializedRequest = await serializeRequest(event.request)
const clientMessage = await sendToClient(
client,
{
type: 'REQUEST',
payload: {
id: requestId,
interceptedAt: requestInterceptedAt,
...serializedRequest,
},
},
[serializedRequest.body],
)
switch (clientMessage.type) {
case 'MOCK_RESPONSE': {
return respondWithMock(clientMessage.data)
}
case 'PASSTHROUGH': {
return passthrough()
}
}
return passthrough()
}
/**
* @param {Client} client
* @param {any} message
* @param {Array<Transferable>} transferrables
* @returns {Promise<any>}
*/
function sendToClient(client, message, transferrables = []) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error)
}
resolve(event.data)
}
client.postMessage(message, [
channel.port2,
...transferrables.filter(Boolean),
])
})
}
/**
* @param {Response} response
* @returns {Response}
*/
function respondWithMock(response) {
// Setting response status code to 0 is a no-op.
// However, when responding with a "Response.error()", the produced Response
// instance will have status code set to 0. Since it's not possible to create
// a Response instance with status code 0, handle that use-case separately.
if (response.status === 0) {
return Response.error()
}
const mockedResponse = new Response(response.body, response)
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
value: true,
enumerable: true,
})
return mockedResponse
}
/**
* @param {Request} request
*/
async function serializeRequest(request) {
return {
url: request.url,
mode: request.mode,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: await request.arrayBuffer(),
keepalive: request.keepalive,
}
}