mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 16:08:23 +08:00
f4dbfcdf8f78e366c1cff33b9f6aa044d0488b7d
7279
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f4dbfcdf8f |
fix: correct Docker image tag format (remove v prefix) (#339)
The SDK build script strips the 'v' prefix from semver release tags when
publishing Docker images. The correct tag format is {version}-python
(e.g., 1.22.0-python), not v{version}-python.
This fixes the 'Unable to find image' error when running npm run dev:docker.
Changes:
- Update DEFAULT_AGENT_SERVER_TAG from v1.22.0-python to 1.22.0-python
- Update documentation in AGENTS.md to reflect correct tag format
Co-authored-by: openhands <openhands@all-hands.dev>
|
||
|
|
c0bc27c0ca |
fix: use versioned release tags for agent-server Docker images (#338)
Update DEFAULT_AGENT_SERVER_TAG in dev-docker.mjs from commit-based tag
(0924962-python) to versioned release tag (v1.22.0-python) for better
reproducibility and consistency with the PyPI version used in dev-safe.mjs.
Changes:
- Update DEFAULT_AGENT_SERVER_TAG to v1.22.0-python
- Add documentation in AGENTS.md explaining the versioning approach
- Document that Docker and non-Docker dev modes should use matching versions
The software-agent-sdk repository builds Docker images with versioned tags
in the format v{version}-python when release tags are pushed, which makes
them suitable for pinning to specific releases.
Closes #323
Co-authored-by: openhands <openhands@all-hands.dev>
|
||
|
|
773cc72324 |
feat: update SDK to 1.22.0 and add CI version sync check (#333)
* feat: update SDK to 1.22.0 and add CI version sync check
- Update DEFAULT_AGENT_SERVER_VERSION from 1.21.1 to 1.22.0 in dev-safe.mjs
- Update SDK version references in AGENTS.md
- Add scripts/check-sdk-version-sync.mjs to verify automation project uses
matching SDK versions for openhands-sdk, openhands-tools, openhands-workspace,
and openhands-agent-server
- Add .github/workflows/sdk-version-sync.yml CI workflow with:
- Path-filtered PR/push triggers for version-related file changes
- repository_dispatch triggers (sdk-version-check, sdk-release) for
external repos to notify when SDK deps change
- workflow_dispatch with optional version override
- Scheduled runs every 6 hours to catch upstream changes
- PyPI version checking support (--check-pypi flag)
The check script supports:
- EXPECTED_SDK_VERSION env var override for CI triggers
- --check-pypi flag to also display latest PyPI versions
- --help for usage documentation
To trigger from external repos (e.g., OpenHands/automation or SDK repo):
curl -X POST -H "Authorization: token \$GITHUB_TOKEN" \\
https://api.github.com/repos/OpenHands/agent-canvas/dispatches \\
-d '{"event_type": "sdk-version-check"}'
* fix: check released PyPI version instead of GitHub main branch
The SDK version sync check now fetches dependencies from the released
openhands-automation package on PyPI (version specified by
DEFAULT_AUTOMATION_VERSION in dev-with-automation.mjs) rather than
fetching pyproject.toml from the GitHub main branch.
This ensures we're checking the actual released version that users
would install, not the development version on main.
* fix: address review feedback for SDK version sync check
- Add env var overrides for automation package name and version
- Add retry logic with exponential backoff for PyPI API failures
- Add semantic version normalization for comparing versions
- Fix repository_dispatch to use client_payload.version
- Improve regex to handle parenthesized dependency formats
- Add comprehensive test coverage for helper functions
* fix: add type casts for dynamic module import in tests
* chore: update automation version to 1.0.0a2
- Update DEFAULT_AUTOMATION_VERSION in dev-with-automation.mjs
- Update AGENTS.md documentation
- Update test expectation
---------
Co-authored-by: openhands <openhands@all-hands.dev>
|
||
|
|
7a03d234e9 | fix: render full YAML metadata on the Skills page with search & filters (#336) | ||
|
|
9106bc30da |
fix: show folder name for local-only repos in git control bar (#322)
* fix: show folder name for local-only repos in git control bar * fix --------- Co-authored-by: hieptl <hieptl.developer@gmail.com> |
||
|
|
b6529577b8 | fix: preserve per_turn_token when combining metrics from a dormant condenser LLM (#332) | ||
|
|
18e51fa84d |
fix: move TMUX_TMPDIR to /tmp to avoid socket errors on mounted volumes (#325)
* fix: move TMUX_TMPDIR to /tmp to avoid socket errors on mounted volumes Some filesystems (NFS, CIFS, certain FUSE/overlay mounts used by Docker bind-mounts) do not support Unix domain sockets. When TMUX_TMPDIR pointed to ~/.openhands/agent-canvas/tmux/ inside a container, tmux failed with: error connecting to .../tmux-10001/openhands (Operation not supported) Move tmux socket directory to /tmp/openhands-agent-canvas-tmux which is always on a local/tmpfs filesystem that supports Unix sockets. Tmux sockets are ephemeral and don't need persistence across restarts. Co-authored-by: openhands <openhands@all-hands.dev> * refactor: drop explicit TMUX_TMPDIR from dev-docker.mjs, use system default Per review feedback — the container's default TMUX_TMPDIR (/tmp) already supports Unix domain sockets, so there's no need to set it explicitly. Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
7d9e6ab6ba |
fix: improve right panel UX when unpinning active tab (#328)
* fix: improve right panel UX when unpinning active tab - Add RightPanelToggle button in chat header for persistent panel visibility control - When unpinning the active tab, switch to another pinned tab instead of hiding the panel - Add i18n keys for show/hide panel tooltips - Update tests to reflect new behavior Fixes #326 Co-authored-by: openhands <openhands@all-hands.dev> * fix: address review feedback - Fix fragile test pattern using unmount/remount instead of double render - Show panel toggle on mobile devices as well (remove lg:block restriction) Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
9b2f0d6ff8 | Update README.md | ||
|
|
4e5acbca6c |
Change caution to warning in SELF_HOSTING.md
Updated caution note to a warning in the self-hosting guide. |
||
|
|
0740d20c87 | Update SELF_HOSTING.md | ||
|
|
2212906fd5 |
fix(frontend): make Add Workspace modal dynamic and host-aware (#306)
* fix: make Add Workspace modal dynamic and host-aware * fix: make OH_MOUNT_HOST_HOME opt-in and surface it from the modal |
||
|
|
ccefb6cb04 | fix: persist app-level settings from /settings/app save flow (#304) | ||
|
|
4acc38da4d | fix: wrap description in badge to match LLM Settings styling (#303) | ||
|
|
d3707e045e | fix: animate sidebar min-width so expand transition is visible (#301) | ||
|
|
173f08e42d | fix: cap modal height and scroll LLM settings internally (#299) | ||
|
|
b88c64307f | fix: show pointer cursor on NewConversationButton popover items (#297) | ||
|
|
93e6ef9114 | fix: hide No conversations found when sidebar is collapsed (#295) | ||
|
|
d968acdc3a | fix: keep outer container padding consistent across routes (#293) | ||
|
|
c8c08acd67 |
chore(dev:docker): bump default agent-server tag to 0924962-python (#289)
Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
0068b5f465 |
chore: address post-merge feedback on #284 + small cleanups (#288)
Three of the four post-merge review comments on #284 are still real: - `src/utils/file-priority.ts` — `pathDepth` used `split('/').length - 1` without filtering empty segments, so a leading slash, trailing slash or double slash silently inflated the computed depth and dropped a top-level file behind genuinely-nested ones in the Files tab pill row. Aligned with the `.filter(Boolean)` convention already used by `buildFileTree`, with regression tests for both the leading-slash and double-slash cases. - `src/utils/conversation-local-storage.ts` — `filesTabContentViewMode` is typed `ViewMode` ("rich" | "plain") but came from `JSON.parse(localStorage)`, so a corrupt / hand-edited value would leak past TypeScript into the UI. `sanitizeStoredState` now drops unknown values so the typed default re-applies, with a test that asserts `{ filesTabContentViewMode: 'fancy' }` falls back to 'rich'. - `src/routes/files-tab.tsx` — the toolbar refresh button used `aria-label={t(I18nKey.COMMON$FILES)}` which translates to just "Files". Screen-reader users would hear the button as "Files button" instead of "Refresh files". Added a dedicated `FILES$REFRESH` translation ("Refresh files" + the 14 sibling locales), wired the button's aria-label and title to it, and updated the existing test to assert the new label. The fourth comment (id-less events re-processed on every effect run in `use-auto-refresh-files-on-edit`) was already addressed in a follow-up commit via a `WeakSet<OHEvent>` keyed by object identity, so no change needed there. Also: `src/components/shared/buttons/refresh-button.tsx` and its companion `src/icons/refresh.svg` had zero references in the codebase (the only refresh button left in use is the inline one in `routes/files-tab.tsx` with `u-refresh.svg`). Deleted. Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
b2fa082e6e |
chore: bump default agent-server image tag to 1916bb4-python (#287)
Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
f2d787776a |
feat(onboarding): update default model and starter prompt (#286)
- Set default LLM to anthropic/claude-opus-4-7 in the onboarding LLM step - Change the starter prompt to 'Create a basic webpage explaining what OpenHands can do' Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
7ea79e0a39 |
feat(files-tab): Files tab with diff + rich/plain file viewer, safe-HTML markdown (#284)
* Add Files tab with file viewer and diff view modes Replaces the previous Changes and App (served-host) tabs with a unified Files tab that supports two top-level modes: - Diff View (default when working inside a git repo): renders the existing Changes UI in-place. - File Viewer: shows a quick-access row of the most important files (index.html, package.json, README.md, etc.) with an overflow dropdown, plus an on-demand expandable file tree. Selecting a file shows its content with a Rich/Plain toggle. Rich mode renders HTML, markdown, and images in a sandboxed iframe; Plain mode shows plaintext with a binary-fallback message. Includes a new useIsGitRepo hook and supporting workspace-files hooks plus utilities for sorting files by priority and building a tree from flat paths. Co-authored-by: openhands <openhands@all-hands.dev> * fix(files-tab): only default to diff view when a repo was explicitly attached The previous detection relied on whether 'git status' succeeded against the workspace, but the agent-server initialises every workspace as an internal git worktree for change tracking. As a result, a brand-new conversation with no user-attached repo was incorrectly treated as a git repo and the Files tab opened in diff view. Drop the filesystem probe and use the conversation's 'selected_repository' as the sole signal — that's the field populated by the repo picker for 'an existing git repo' from the user's point of view. Co-authored-by: openhands <openhands@all-hands.dev> * feat(files-tab): refresh button covers file list, auto-refresh on edits Two related fixes to the Files tab data lifecycle: 1. The toolbar refresh button used to only refetch git changes (the diff view). It now also invalidates the workspace file list and any cached file contents, so clicking it works as expected in both modes. 2. Add a useAutoRefreshFilesOnEdit hook mounted by FilesTab that watches the conversation event store and invalidates the workspace-files, workspace-file-content and file_changes queries whenever the agent produces a mutating file-editor observation (create / str_replace / insert / undo_edit). Read-only 'view' commands and non-file observations are ignored. The hook is array-position based so it processes each event exactly once. Tests: 4 new for the hook, all existing files-tab and conversation-tabs tests still pass. Co-authored-by: openhands <openhands@all-hands.dev> * feat(files-tab): tree toggle, full paths, real markdown rendering UI changes to the workspace file viewer: - Replace the trailing 'more files' overflow dropdown with a single caret button on the LEFT of the quick-access row that toggles the left-hand file tree. Tree is shown by default; users who want more horizontal space for the content pane can collapse it. There is no longer a dropdown listing extra files — anything that doesn't fit in the pills row is reachable by opening the tree. - Pills in the quick-access row now display the full relative file path (e.g. 'src/main.ts') instead of just the basename, so users can distinguish between same-named files in different folders at a glance. The full path also serves as the tooltip. - Markdown files are now rendered via the existing MarkdownRenderer (react-markdown + remark-gfm + remark-breaks) inside a styled prose container. The old approach piped raw text into a sandboxed <iframe> wrapped in <pre>, which displayed unrendered markdown source. The iframe path is removed for .md / .markdown / .mdx files. Tests: 3 new cases in files-tab.test.tsx (full-path pills, tree-toggle round-trip, markdown rendering with h1 + bold + no iframe). All 28 existing files-tab / conversation-tabs / auto-refresh tests still pass, plus markdown component tests (27/27). AGENTS.md now records the worktree policy: don't auto-switch the main workspace away from the worktree's branch unless the user explicitly asks. Co-authored-by: openhands <openhands@all-hands.dev> * feat(files-tab): static fileserver + tab/toolbar layout pass Switches the Files tab to the agent server's static workspace fileserver (software-agent-sdk PR #3192) and shuffles the layout in response to real-world usage feedback. == Static workspace fileserver == The agent server now exposes each conversation's workspace at GET /api/conversations/{conversation_id}/workspace/{file_path:path}. A new utility, buildWorkspaceFileUrl, composes that URL from the conversation_url (host + optional path-prefix for proxy deployments) and conversation id. useWorkspaceFileContent is refactored to: * always expose a 'staticUrl' field, so consumers can point an iframe or <img> at the same-origin fileserver and benefit from relative asset resolution. * skip fetching the bytes for image/PDF kinds entirely (the consumer renders staticUrl directly). * for text-classified files, fetch via the static URL with X-Session-API-Key auth instead of going through the typescript- client RemoteWorkspace.downloadAsBlob (which we no longer need for this view). * drop blobUrl and absolutePath from WorkspaceFileContent — they were only used by the iframe renderer, which now uses staticUrl. FileContentViewer in rich mode: * HTML/SVG and PDFs render with <iframe src={staticUrl}> (no sandbox) so relative asset references load against the same origin. * Images render with <img src={staticUrl}> (no blob URL plumbing). * Markdown still uses MarkdownRenderer; plain mode unchanged. Caveat (intentional, matches user spec): on agent servers with a configured session_api_keys list, iframe src cannot send the X-Session-API-Key header, so rich HTML/PDF previews won't load there. Unauthenticated (auto_error=False, empty key list) servers — the default for local dev — work. == Tab bar / toolbar layout == * Files tab moved to the leftmost slot in ConversationTabs (was second, after Planner). Task-list insertion adjusted from unshift() to splice(1, 0, ...) so Files stays leftmost even when present. * Refresh button removed from the top tab bar and re-homed inside the FilesTab toolbar; it now sits next to the diff/files and rich/plain toggles on the right edge of that row. * Rich/Plain toggle moved from the right-hand side of the toolbar to sit immediately next to the Diff/Files toggle on the left (justify-between → flex-start gap-3, with the refresh button using ml-auto). * Left-hand file tree collapsed by default (was expanded). The quick-row caret on the pill row is the toggle, as before. * 'Diff view' label shortened to just 'Diff' across all 15 locales in translation.json (key FILES retained to avoid a noisy rename in the generated declaration.ts). == Tests == * New: __tests__/utils/workspace-file-url.test.ts (7 tests) covers null guards, encoding of path segments, leading-slash stripping, omitted relativePath, and proxy-deployment path prefixes. * New: __tests__/i18n/files-diff-label.test.ts locks the renamed English label in translation.json (the test environment's i18next mock returns keys, so we assert against the source-of-truth file). * files-tab.test.tsx: - mock content shape updated to use staticUrl (no more blobUrl / absolutePath). - default-state expectations updated for the collapsed tree. - tree-toggle test inverted: hidden → expand → hide. - new test asserts HTML files render as <iframe src={staticUrl}> (no sandbox attribute). - new test asserts the refresh button lives in the files-tab toolbar and triggers refetchGitChanges. * conversation-tabs.test.tsx: - old 'refresh button in the top tab bar' test replaced with the inverse assertion (no standalone refresh <button> there now). - two new tests pin Files as the leftmost tab in both has-tasklist and no-tasklist cases. All 40 focused tests pass; typecheck clean; src/ lint + prettier clean. Co-authored-by: openhands <openhands@all-hands.dev> * feat(files-tab): GFM+safe HTML markdown; suppress diff default on empty repos == Markdown rendering == MarkdownRenderer was only running remark-gfm + remark-breaks. Raw inline HTML in markdown source (e.g. <details>, <kbd>, <mark>, GitHub-style badges, or anchor target=_blank tricks) was silently dropped by react-markdown's default behaviour. This commit: * Adds rehype-raw to parse raw HTML embedded in markdown into the rehype tree. * Adds rehype-sanitize to strip anything dangerous before render -- scripts, event handlers, javascript:/data: URLs in href, and any tags not in the allow-list. * Extends the default sanitize schema with markdown-friendly extras (className/id/style on all elements; target+rel on anchors; img with safe src schemes only; <details>/<summary>, <figure>/ <figcaption>, <mark>, <kbd>, <sub>, <sup>). * Restricts URL protocols on href to http/https/mailto/tel and on src to http/https/data (so data:image/... still works for inline base64 images, but data:text/html -- XSS vector -- does not). The new behaviour is opt-out via a new MarkdownRenderer prop, 'allowHtml', which defaults to true. The sanitize schema makes raw HTML safe by construction, so on-by-default is the right call -- and it's consistent with how GitHub and most markdown renderers behave. 11 new markdown-renderer tests cover GFM tables/strikethrough/task lists, inline HTML rendering (<mark>, <kbd>, <details>/<summary>), sanitisation of <script>/onclick/javascript:/<iframe>, safe URL schemes (https/mailto) passing through, and the allowHtml=false opt-out path. All 217 tests in the chat / diff-viewer / planner / conversation-panel suites still pass -- confirming no regression from making allowHtml default-on for the existing call sites. == Diff-view default for empty repos == The Files tab was defaulting to diff view whenever the conversation had a selected_repository, even on attached repos with zero commits (unborn HEAD -- e.g. a freshly-created empty GitHub repo). In that state the diff view has nothing to diff against and looks broken; the file viewer is a much better landing experience. * Adds src/hooks/query/use-has-git-commits.ts -- a thin useQuery- backed hook that shells out via the conversation's RemoteWorkspace to run 'git rev-parse --verify HEAD' in the working dir. Exit 0 -> hasCommits: true; non-zero -> false. The enabled flag is plumbed through so we don't probe when there's no attached repo to check (saves a workspace round trip on every plain conversation). * FilesTab now derives diffViewDefault as isGitRepo && hasCommits !== false -- i.e. only enables diff by default when both conditions hold, treating the in-flight 'null' state as optimistically true so we don't get a files->diff flash on the common path. 3 new files-tab tests cover empty-repo behaviour, the enabled-gating of the probe when no repo is attached, and the optimistic in-flight default. == Tests / Quality gates == * typecheck: clean (react-router typegen + tsc). * eslint + prettier on changed src files: clean. * Vitest focused run: 52/52 (markdown + files-tab). * Vitest chat + diff-viewer + planner + conversation-panel: 217/217 (no regressions from default-on allowHtml). Co-authored-by: openhands <openhands@all-hands.dev> * feat(files-tab): mint workspace cookie via startWorkspaceSession for iframe/img auth Bumps @openhands/typescript-client to feat/workspace-static-session (f062287d) which adds POST /api/auth/workspace-session. Calling it exchanges the X-Session-API-Key for an HttpOnly cookie scoped to /api/conversations -- which is the only auth mechanism the browser attaches to top-level <iframe src> / <img src> requests. == New plumbing == * src/api/typescript-client.ts: factory createRemoteConversation(id) that wraps RemoteConversation with a placeholder Agent (required by the constructor but unused for startWorkspaceSession). RemoteConv and Agent come from the package root -- they're not exposed under the package's subpath exports. * src/hooks/query/use-workspace-session.ts: useWorkspaceSession hook that fires startWorkspaceSession once per conversation via react- query, caches the result with staleTime: Infinity (the cookie sticks in the browser jar; re-issuing the POST is wasted work), and exposes { baseUrl }. retry: false because a 401 here is a fixed config issue, not transient. * Same file: joinWorkspaceUrl(baseUrl, relativePath) -- URL-encodes each path segment but preserves "/" separators. Replaces the standalone src/utils/workspace-file-url.ts which is removed. == Refactor == * use-workspace-file-content.ts now derives staticUrl from useWorkspaceSession's baseUrl via joinWorkspaceUrl, gates the query on !!baseUrl (so we don't fire fetches against an unauthenticated URL), and switches fetch() to credentials: "include" instead of the X-Session-API-Key header. This makes our JS fetch ride the same auth path as the iframe/img -- one behavior, one CORS story, no preflight for a custom header. == Tests == * __tests__/hooks/query/use-workspace-session.test.tsx -- 9 tests: happy path (POST fires, baseUrl flows through, createRemote Conversation gets the right args); runtime-not-ready and no- conversation-id both gate the POST; error surfaces as isError + error.message; joinWorkspaceUrl covers empty / single-segment / nested / leading-slash / unicode + space encoding. * Deleted __tests__/utils/workspace-file-url.test.ts -- the helper it pinned is gone, joinWorkspaceUrl is covered by the new tests. == Quality gates == * typecheck clean (react-router typegen + tsc). * Targeted vitest run: 251/251 across markdown / files-tab / chat / diff-viewer / planner / conversation-panel / workspace-session. * Lint has known issues in the new test file (display-name and function-component-definition warnings on the QueryClient wrapper factory; no-promise-executor-return on the `await new Promise(r => setTimeout(r, 10))` polling -- both patterns already in use in other __tests__/hooks/query/*.test.tsx files); will sweep separately. Co-authored-by: openhands <openhands@all-hands.dev> * chore(files-tab): clear remaining lint errors on the branch * __tests__/hooks/query/use-workspace-session.test.tsx: name the QueryClientProvider wrapper component (silences react/display-name + react/function-component-definition) and lift the setTimeout("yield to scheduler") trick into a flushScheduler() helper (silences no-promise-executor-return -- ESLint forbids returning a value from a Promise executor). * src/utils/conversation-local-storage.ts: replace the destructure- with-throwaway-name pattern (which tripped naming-convention on `_drop`) with a plain spread + delete -- one statement clearer, no rename gymnastics. Also re-wrap the signature so prettier is happy. * src/components/features/conversation/conversation-tabs/conversation- tab-content/conversation-tab-content.tsx: prettier reformat (one long line broken). Verified: full `npm run lint` passes (10 pre-existing warnings in files outside this branch's scope remain). Co-authored-by: openhands <openhands@all-hands.dev> * feat(files-tab): use RemoteWorkspace.startWorkspaceSession directly typescript-client PR #155 was reshaped to land startWorkspaceSession on RemoteWorkspace (taking conversationId as an argument) rather than on RemoteConversation. The new shape is strictly simpler for our use case: minting a workspace cookie no longer requires constructing a placeholder Agent + RemoteConversation just to call one method. Changes: * package.json / package-lock.json: bump @openhands/typescript-client pin to github:OpenHands/typescript-client#6b5a65c5 (head of feat/workspace-static-session, sole commit on PR #155). * src/api/typescript-client.ts: drop the createRemoteConversation factory along with the Agent + RemoteConversation imports it needed -- callers can now go through createRemoteWorkspace (which already existed for git-service) for everything we use. * src/hooks/query/use-workspace-session.ts: call createRemoteWorkspace({ conversationUrl, sessionApiKey }) and then workspace.startWorkspaceSession(conversationId). Same return value (baseUrl string), same caching semantics. * __tests__/hooks/query/use-workspace-session.test.tsx: rename the mocked factory + assertions to match. The hook's surface (data, isLoading, isError, error) is unchanged so the rest of the suite is untouched. Verified: typecheck clean, workspace-session + files-tab tests (23/23) pass, full `npm run lint` reports 0 errors (10 pre-existing warnings in unrelated files remain). Co-authored-by: openhands <openhands@all-hands.dev> * feat(files-tab): persist toggles + cache-bust iframe + depth-first sort + external-open link Five behavioural improvements to the Files tab, plus the corresponding typescript-client pin bump now that PR #155 has merged to main. 1. Persist diff-view + rich/plain choice per-conversation. Both Files tab toggles now live in conversation localStorage rather than transient component state, so switching to another conversation and back restores whatever view the user last selected. Implemented by adding `filesTabDiffView` (nullable -- null means 'fall back to repo-aware default') and `filesTabContentViewMode` to `ConversationState`, exposing matching setters from `useConversationLocalStorageState`, and wiring the toolbar `SegmentedToggle`s to them. When the hook is called with an empty or task-placeholder id it now mirrors updates into local React state instead of dropping them on the floor -- keeps the UI reactive in unit tests / pre-route renders. 2. Fix initial files→diff flash inside a real repo. While `useIsGitRepo` is still loading we now stay optimistic (`isGitRepo || isGitRepoLoading`), matching the existing optimism around `hasCommits`. The user-visible bug was: a brand new conversation attached to a git repo would show files-view for one frame before flipping to diff-view, and any persisted choice we made during that frame stuck. 3. Cache-bust iframes / images after every file-editor observation. New `useWorkspaceMutationCounter` zustand store: a monotonic counter `useAutoRefreshFilesOnEdit` bumps every time it sees a mutating `FileEditorObservation` / `StrReplaceEditorObservation` / `PlanningFileEditorObservation`. `FileContentViewer` and the toolbar 'open in new window' link append it to the workspace static URL as `?v=<n>`, so the browser refetches HTML/CSS/images the agent just rewrote on disk instead of showing the stale cached response. Read-only `view` observations and unrelated kinds (e.g. `ExecuteBashObservation`) don't bump. 4. Depth-first file ranking. `sortFilesByPriority` now sorts by path depth first (shallower wins unconditionally), then by high/secondary basename importance, then alphabetically. Concretely: top-level `README.md` outranks `foo/bar/index.html`, but `index.html` still beats `README.md` at the same depth. Updated docs + added tests for the new contract. 5. 'Open in new window' affordance. New external-link button in the Files tab toolbar, rendered next to the refresh button while in file-viewer mode whenever a file is selected and we've resolved its workspace static URL. Hidden in diff-view (no single meaningful URL to point at). Cache-bust query string applied so the popped-out tab also sees the latest bytes. Also bumps `@openhands/typescript-client` to `ef62e82fc3dfb03991a1c8025429caf354427263` -- the merge commit of PR #155 on main. No API change vs the previous `6b5a65c5...` pin (that was the only commit on the merged branch); this just gets us off the soon-to-be-deleted feature branch ref. Tests: * Full suite: 1805 passed / 12 skipped / 9 todo across 283 files. * `__tests__/utils/file-priority.test.ts`: added two new cases pinning the depth-first + same-depth rules. * `__tests__/hooks/use-auto-refresh-files-on-edit.test.tsx`: added two cases pinning the mutation-counter bump (yes on mutations, no on `view` / non-file observations). * `__tests__/routes/files-tab.test.tsx`: existing iframe-src test switched from strict equality to a `^staticUrl\?v=\d+$` regex to allow the cache-buster suffix. * `__tests__/hooks/use-draft-persistence.test.tsx` and `__tests__/hooks/use-handle-plan-click.test.tsx`: fixtures updated to include the new ConversationState fields and matching setter mocks. Lint clean (0 errors; 10 pre-existing warnings remain). Typecheck clean. New i18n key `FILES$OPEN_IN_NEW_WINDOW` translated across all 15 locales. Co-authored-by: openhands <openhands@all-hands.dev> * fix(files-tab): address PR #284 review comments - Markdown sanitizer: drop `style` attribute from allowlist (CSS-injection / data-exfiltration channel) and remove `data:` from `src` protocol allowlist (data:text/html bypass). - HTML / PDF preview iframes: add `sandbox="allow-same-origin"` so scripts and inline event handlers in previewed files cannot execute in the canvas context while relative asset refs still resolve. - Auto-refresh hook: track processed event ids in a Set instead of slicing the tail by length, so out-of-order events inserted into the (sorted) event store still trigger invalidation + cache-bust. - File tree builder: replace O(n) `children.find` with an O(1) side-table Map per parent; promote a leaf node to a directory if a deeper path arrives later. - Conversation localStorage: filter removed tab names ("editor", "served", "changes", "app") out of `selectedTab` and `unpinnedTabs` on read so ghost entries don't linger. - File-content viewer: distinguish load-error from binary-fallback with a new `FILES$LOAD_ERROR` i18n key. - package.json: exact-pin `rehype-raw` and `rehype-sanitize` (no caret). - Tests: add security regressions (style attr, data:text/html, inline event handlers), tree promotion + wide-dir smoke test, out-of-order mutation event test, unpinnedTabs migration test; update HTML-preview iframe sandbox assertion. Co-authored-by: openhands <openhands@all-hands.dev> * Update src/components/features/markdown/markdown-renderer.tsx Co-authored-by: OpenHands Bot <contact@all-hands.dev> * feat(files-tab): drop "read-only" from terminal title, theme markdown preview, prism-highlight source/plain views Three small UI changes that go together: 1. Terminal tab title — strip the "(read-only)" qualifier from COMMON$TERMINAL in every locale. The fact that the embedded xterm doesn't echo stdin is internal plumbing; users just want to see "Terminal" in the tab strip. 2. Rich-mode markdown preview — paint the wrapper in the right-pane chrome color (\#25272D) and force every text node to white. The old `bg-white text-[#222]` made markdown files look like a stark card floating on the dark canvas. Switched to `prose prose-invert` and layered arbitrary CSS-variable utilities (`[--tw-prose-body:#fff]` et al.) on top, because the typography plugin's prose-invert default is off-white (#e5e5e5) rather than pure white. Existing custom heading components already use text-white and continue to win. 3. Source-code views (rich AND plain) and plain views of markdown/HTML — feed everything through the existing PrismLight pipeline used by chat code-block rendering. New `HighlightedSourceView` component wraps SyntaxHighlighter with vscDarkPlus + a transparent background so the highlighter blends with the right-pane chrome instead of painting its own slab. New `getPrismLanguageForFile` util resolves extensions (.ts, .py, .yaml, ...) and well-known no-extension filenames (Dockerfile, Makefile, .bashrc, ...) to Prism grammars, falling back to a raw `<pre>` when nothing matches. Behavior matrix in the files tab now reads: Rich mode: HTML/SVG -> sandboxed iframe preview Markdown -> rendered (dark bg, white text) Image -> <img> PDF -> sandboxed iframe Source -> highlighted source (no other "rich" form for code) Plain mode: Source -> highlighted source Markdown -> highlighted markdown source (see the markup) HTML -> highlighted markup source Other -> raw <pre> fallback (rare) Binary -> binary fallback message Tests: - New unit tests for getPrismLanguageForFile (extensions, no-ext filenames, case-insensitive, mime-type fallback, null on unknown). - files-tab integration tests now assert the markdown wrapper paints bg-[#25272D]/text-white, and that toggling .md to plain renders highlighted markdown source rather than rich markup. Lint and full test suite intentionally not re-run on this commit; follow-up commits address upstream issues and PR review feedback. Co-authored-by: openhands <openhands@all-hands.dev> * fix(markdown): repair botched suggestion in fc208bc — collapse rel-attribute schema fc208bc applied PR review feedback via the GitHub web-UI "commit suggestion" button, but the suggested replacement ended up *inside* the existing array literal instead of replacing it: a: [ ...(defaultSchema.attributes?.a ?? []), "target", a: ["href", "title", "target", "rel"], // ← syntax error ], That's a labeled-statement-like token inside an array literal — it fails both `tsc` and `eslint` parsing, breaking the branch's build and typecheck for everyone pulling this PR. Fix-forward (preserves rbren's authorship of fc208bc in history) by applying the reviewer's actual intent: collapse the `a` allow-list to `["href", "title", "target", "rel"]`. This addresses the security concern the reviewer raised in thread 3215928329 — the old `["rel", "noopener", "noreferrer", "nofollow"]` form is rehype-sanitize's exact-value variant, which strips the standard space-separated `rel="noopener noreferrer"` and reintroduces a reverse-tabnabbing vector on raw HTML anchors with `target="_blank"`. Added a comment in the schema explaining the reasoning. A regression test for this is added in the next commit (PR review feedback round 2) alongside the rest of the round-2 fixes. Co-authored-by: openhands <openhands@all-hands.dev> * fix(files-tab): address PR #284 round-2 review comments Round 2 of review feedback on PR #284. The five threads addressed here all came in together at 01:39 UTC; the related rel-attribute schema fix is in the previous commit (7c099bf). Hook (use-auto-refresh-files-on-edit): * Guard against undefined event.id (3215928337). The event store accepts events with no id (`getEventId` returns `string | number | undefined`). The previous version of the hook blindly called `has(event.id)` / `add(event.id)`, which put the literal `undefined` into the Set on the first id-less arrival and then silently swallowed every subsequent id-less event because the Set already contained that key. Now we only consult/touch the set when the id is defined; id-less events are always treated as new. * Widen processedIdsRef from Set<string> to Set<string | number> (3215928342). The formal EventID type is string, but the event store itself uses Set<string | number> defensively and getEventId returns string | number | undefined — the hook mirrors that tolerance so a stray numeric id never sneaks past dedup. Tests added for the above (3215928347): * `does NOT deduplicate id-less events (every id-less arrival is a new event)` — three distinct id-less FileEditorObservations land in sequence and we expect three counter bumps; with the old implementation only the first would land. * `dedupes numeric event ids the same way as string ids` — same numeric-id event added twice produces exactly one counter bump. Markdown sanitizer (3215928336): * Export MARKDOWN_SANITIZE_SCHEMA so tests can target the schema directly. Wrote a docstring explaining why the through-component test the reviewer suggested wouldn't catch the bug: our custom `anchor` component hard-codes `target="_blank" rel="noopener noreferrer"`, so the final DOM is safe regardless of what the schema does to HAST. We have to test the schema in isolation. * Two new `describe("MARKDOWN_SANITIZE_SCHEMA")` tests that run hast-util-sanitize directly on hand-built HAST trees: - rel="noopener noreferrer" survives sanitization (regression for the fc208bc bug — the old exact-match schema would have stripped it) - rel="nofollow ugc" also survives (locks in the property that *any* rel-token combination is safe, since rel doesn't execute code or navigate) Conversation local storage (3215928349, 3215928351): * Existing unpinnedTabs filter test extended from `["editor", "changes", "served"]` to all four removed tabs (`+"app"`). The previous version of the test missed "app" and that gap is exactly what let the original whitelist-vs-denylist bug slip through. * New `describe("filesTabDiffView persistence")` block with four tests: default-null, round-trip true, round-trip false, isolation between conversations. The boolean is per-conversation and its default-null is load-bearing — the higher layer relies on null to apply the git-repo-aware default. * New `describe("filesTabContentViewMode persistence")` block with four tests: default-'rich', round-trip 'plain', round-trip explicit 'rich', isolation between conversations. The 'rich' default is locked in so a careless field-initializer rename doesn't quietly flip every user from rich to plain after deploy. Lint and full test suite intentionally not re-run on this commit per the user's instruction; will run on the next pass once any further review feedback is in. Co-authored-by: openhands <openhands@all-hands.dev> * fix(files-tab): make CI green (lint + correct id-less event dedup) CI was red on `4e9271b` for two reasons: 1. **Lint errors** (prettier + no-continue) that I'd skipped on the user's instruction during the round-2 commit: * `src/hooks/use-auto-refresh-files-on-edit.ts:75` — `no-continue` violation from the new id-guard. Rewrote to use an inverted `alreadyProcessed` predicate instead of `continue` so the control flow reads the same way without breaking the lint rule. * `src/utils/file-language.ts` — three prettier diffs (string-quote style + Math.max arg wrapping). Auto-fixed via `eslint --fix`. * `src/components/features/files-tab/file-content-viewer.tsx` — two prettier wraps on long className + JSX expression that showed up after the round-1 round of edits. Auto-fixed. 2. **A real bug** the lint-fix exposed in the round-2 test: the suggested test (and mine, which followed it) asserted `counter === 3` after adding three id-less events inside a single `act()` block. That assertion is wrong because the hook bumps the counter exactly once per effect flush (the bump is outside the event loop), and act() batches all three addEvent calls into one flush. But while investigating, I found the underlying bug the reviewer was probing toward: id-less events were not durably deduped. The events array is rebuilt on every store mutation but its element references are stable, so a single id-less event would re-trigger the bump on every subsequent render — spamming cache invalidations forever. Fix: added a second tracking store, `processedEventsRef`, a `WeakSet<OHEvent>` keyed by object reference. Id-less events dedup by reference; id-bearing events still dedup by id. The WeakSet doesn't pin the events in memory after the store clears them, so there's no leak. Long explanatory comment in the hook covers both halves of the contract (why we can't put `undefined` in the id Set, and why id-less events still need *some* form of dedup). Tests restructured accordingly: * `processes each id-less event distinctly` — three SEPARATE `act()` calls (one per event) so each gets its own effect flush; counter ends at 3. Comment explains why the single-act() version of this assertion is meaningless. * `does NOT re-bump on subsequent renders for the same id-less event` — NEW test that catches the spurious-rebump bug directly: add one id-less event, then `rerender()` three extra times, assert counter stays at 1. This would fail against the previous version of the fix (no WeakSet path). Verified locally: `npm run typecheck` clean, `npm run lint` 0 errors (10 pre-existing warnings in files I didn't touch), `npm test` all 1862 tests pass (286 files, 12 skipped + 9 todo — all pre-existing). Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: OpenHands Bot <contact@all-hands.dev> |
||
|
|
2d8f260c09 |
Update README with multi-VM backend support
Added information about running the backend on multiple VMs. |
||
|
|
068ee47300 | Update README.md | ||
|
|
3e57aa6a04 |
feat(chat): queue pending user messages with sending/error/retry states (#281)
* feat(chat): queue pending user messages with sending/error/retry states Replace the single optimisticUserMessage slot with a FIFO queue of pending user messages so the user gets immediate feedback when they hit send and multiple in-flight messages do not clobber each other. - Reshape optimistic-user-message-store around enqueuePendingMessage / consumeOldestSendingMessage / markPendingMessageError / markPendingMessageSending with a 'sending' | 'error' status per entry. - Render queued messages via a new PendingUserMessages component using ChatMessage's new pendingStatus + onRetry props, applying a faded treatment for 'sending' and an error banner + retry link for 'error'. - Wire the WebSocket context to consume the oldest 'sending' entry when the server echoes a UserMessageEvent back, so the queue drains FIFO. - Update ChatInterface, GitControlBar, TaskCard and useHandleBuildPlanClick to enqueue pending messages, and flip the matching entry to 'error' when the send call rejects. - Add i18n strings for Sending / Send failed / Retry. Co-authored-by: openhands <openhands@all-hands.dev> * fix(chat): scope pending message queue per conversation and stop double 'Sending' render Two follow-up fixes on top of the pending-message-queue refactor: 1. **Per-conversation scoping.** The optimistic queue is global but each entry is now tagged with the `conversationId` it was enqueued from, and `PendingUserMessages` filters to entries matching the active conversation via `useOptionalConversationId`. This means switching conversations no longer carries 'Sending…' bubbles over, and the WebSocket `UserMessageEvent` ack for one conversation can never pop a pending entry belonging to another. `consumeOldestSendingMessage` now takes the conversation id and only matches within it. Call sites updated: `chat-interface.tsx`, `git-control-bar.tsx`, `use-handle-build-plan-click.ts`. `task-card.tsx` moves the enqueue into the `createConversation` `onSuccess` callback so the message is tagged with the newly-created conversation id. 2. **Duplicate 'Sending…' bubble.** `<PendingUserMessages />` was rendered from two places after the previous rebase: once at the bottom of `<Messages>` (where the pending queue would never re-render anyway because `Messages` is `React.memo`'d on event ids) and once unconditionally from `<ChatInterface>`. Removed the render inside `<Messages>` so only the ChatInterface render remains. Also fixed a separate double-submit path in `custom-chat-input.tsx`: the `submittedMessage` effect listed `onSubmit` in its deps, but `onSubmit` (= `handleSendMessage`) is a fresh function on every parent render, and the parent re-renders synchronously when the new `enqueuePendingMessage` call mutates the store. That made the effect fire twice for the same `submittedMessage` value before `setSubmittedMessage(null)` flushed. Pinned `onSubmit` behind a ref and dropped it from the dep array. Tests updated to pass `conversationId` and a new test asserts the queue is not consumed across conversations. Co-authored-by: openhands <openhands@all-hands.dev> * fix(chat): address review feedback on pending-message queue - plan-preview.test.tsx: include `useOptionalConversationId` in the `#/hooks/use-conversation-id` mock so the test passes now that `useHandleBuildPlanClick` depends on it. This was failing all 21 PlanPreview tests in CI on the previous push. - optimistic-user-message-store: replace the module-level counter (which never reset between tests) with a `Date.now()` + base36 random suffix. Same uniqueness guarantee, no shared state. - optimistic-user-message-store: collapse `consumeOldestSendingMessage` into a single atomic `set()` so the find and the filter can't observe an interleaved update from another action. - git-control-bar: capture the `pendingId` from the `enqueuePendingMessage` return value and, if the `send` promise rejects, mark that entry as error so the user gets a retry link instead of a stuck 'Sending…' bubble. - chat-message: add `role="status" aria-live="polite"` to the 'Sending…' label and `role="alert"` to the error label so screen readers announce send-state transitions. Co-authored-by: openhands <openhands@all-hands.dev> * fix(chat): content-keyed echo matching + 60s watchdog timeout for pending messages Addresses the bot's reposted 'critical' review threads on PR #281. **Out-of-order echo matching.** - `PendingUserMessage` now stores both `text` (user-visible bubble) and `content` (the exact string sent to the server, which may include the appended 'Files uploaded: …' prompt). `enqueuePendingMessage` accepts an optional `content` and defaults it to `text` for call sites that don't transform the prompt. - New `consumeMatchingPendingMessage(conversationId, content)`. It does an exact content match first — so an echo of 'second' arriving before an echo of 'hello' correctly pops 'second' instead of the oldest entry — and falls back to the oldest "sending" entry in the same conversation if no exact match exists (lets us still drain the queue if the server slightly munges the body). - `conversation-websocket-context` extracts the echoed text by joining the `TextContent` parts of `event.llm_message.content` and passes it to the new matcher. Both consumption sites (main WS + planning agent WS) use the matcher with the main `conversationId`, so a planning sub-agent echo can never consume a main-conversation pending entry. - `chat-interface` passes `content: prompt` (text + file annotations) to `enqueuePendingMessage`. **60-second watchdog timeout.** - `enqueuePendingMessage` now schedules a `setTimeout` for `PENDING_MESSAGE_TIMEOUT_MS` (60s, exported). If the entry is still in 'sending' state when the timer fires, it's flipped to 'error' with message 'Send timed out' so the user gets a retry link instead of a permanently-stuck bubble. Timeout is a no-op if the echo already consumed the message or if it was already marked error explicitly (the original `errorMessage` is preserved). - 60s is long enough to cover legitimately slow uploads / agent-server latency but short enough to actually rescue stuck bubbles. **Tests.** - `optimistic-user-message-store.test.ts` adds coverage for: storing separate `text`/`content`, exact-match preference, FIFO fallback, skipping entries already in 'error', cross-conversation isolation with identical content, watchdog timeout firing, and the two no-op cases (echo already consumed, message already failed). Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
a7b35f7098 |
fix(changes): show placeholder instead of error toast when expanding a deleted file (#285)
The agent-server's /api/git/diff endpoint calls path.exists() before anything else and returns HTTP 400 (GitPathError) for files that no longer exist on disk. Expanding a deleted file in the Changes view therefore triggered the global QueryCache onError handler and surfaced an error toast. Disable the unified-git-diff query when the change type is 'D' and render a localized 'file deleted' placeholder in FileDiffViewer (no view-mode toolbar, no Monaco editor) so the expansion is a no-op visually instead of an error. Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
0eb0ca5dfa |
fix(chat): render InvokeSkill action/observation with skill name (#283)
Previously, an agent's invoke_skill tool call rendered as the literal 'INVOKESKILL' in the chat — the default fallback in getActionEventTitle was uppercasing the action kind because InvokeSkillAction wasn't a recognized kind. Add InvokeSkillAction / InvokeSkillObservation to the core type unions and route them through the same translation-key machinery as the other built-in tools, so the title now reads 'Invoking skill <name>' / 'Invoked skill <name>' and the expanded card shows the rendered skill content instead of a JSON blob. Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
400be5a3d4 |
feat(chat): add "upload as file" checkbox for attached images (#282)
When the user attaches an image to the message input, a checkbox labeled "upload as file" now appears alongside the previews. When checked, attached images are routed through the normal file-upload path (the same path that handles non-image attachments) instead of being converted to base64 and embedded in the message sent to the LLM. Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
31302f9282 |
fix(ingress): handle socket errors so ECONNRESET can't crash the proxy (#280)
A WebSocket flowing through scripts/ingress.mjs would take down the
whole ingress process whenever its underlying TCP socket reset:
Error: read ECONNRESET
at TCP.onStreamRead (node:internal/stream_base_commons:216:20)
Emitted 'error' event on Socket instance at:
at Socket.onerror (node:internal/streams/readable:1026:14)
proxyWebSocket() only attached an 'error' listener to the outbound
HTTP upgrade request, never to the raw client / upstream sockets that
the bidirectional pipe runs over. ECONNRESET on a long-lived
/sockets/events/... connection (browser tab close, NAT timeout,
mobile network handoff, …) therefore became an unhandled 'error' event
on a Socket and Node aborted the process.
Fix:
- Attach 'error' (and 'close') listeners to both the client socket
and the upstream socket in proxyWebSocket; on either side erroring,
tear the peer down gracefully.
- Mirror the same defensive handling for plain HTTP in proxyRequest:
add 'error' handlers on req, res, and proxyRes so a mid-stream
disconnect aborts the upstream call instead of crashing.
- Add server.on('clientError') for malformed client requests.
- Add a narrow uncaughtException guard that swallows benign socket
teardown errors (ECONNRESET / EPIPE / ECONNABORTED /
ERR_STREAM_PREMATURE_CLOSE) but rethrows everything else, so real
bugs stay visible.
Tests:
- New regression covering an upstream WebSocket that immediately RSTs
after upgrading; before the fix this took the proxy down (next
request fails with ECONNREFUSED), after the fix the process keeps
serving HTTP traffic and stderr never shows "Unhandled 'error' event".
- New regression covering a client that aborts an in-flight HTTP
request mid-flight.
Co-authored-by: openhands <openhands@all-hands.dev>
|
||
|
|
d9beb15250 |
feat(sidebar): collapsible left-hand nav with icons + hover tooltips (#279)
* feat(sidebar): make left-hand nav collapsible with icons + hover tooltips - Add collapse/expand toggle (block-drawer-left icon) to the sidebar - Top-level nav items (Conversations, Automations, Skills, Settings) now render with leading icons in expanded mode and icon-only in collapsed mode - Persist collapse state to localStorage via useSidebarCollapsedState - New SidebarCollapseContext lets descendant lists adapt their layout - Conversation list switches to a compact rail (status-dot rows) when collapsed, with a HeroUI Tooltip showing the full conversation card on hover; New Conversation button becomes a square + button - Hide the inline 'older conversations / start tasks' summary in collapsed mode (the popover preview already exposes that detail) - Switch message.svg to currentColor so it tracks active/muted nav state - Tests: cover toggle behavior, persistence, and that every top-level nav item ships with an icon Co-authored-by: openhands <openhands@all-hands.dev> * refactor(sidebar): caret toggle + suppress nested status-dot tooltip - Replace the block-drawer-left collapse glyph with a plain ChevronLeft / ChevronRight from lucide-react. The previous icon read as a small white 'pill' wedged next to the main panel shape; the caret is much clearer as a directional toggle. - Add an optional showTooltip prop to ConversationStatusDot (default true to preserve existing call sites). The compact conversation row in the collapsed sidebar now opts out, so hovering the status dot no longer races a tiny status tooltip against the full conversation preview tooltip. Co-authored-by: openhands <openhands@all-hands.dev> * fix(sidebar): hoist lucide-react import to satisfy import/order Co-authored-by: openhands <openhands@all-hands.dev> * refactor(sidebar): reposition caret, drop white tooltip, reorder nav, polish compact list Sidebar header: - Drop the StyledTooltip on the collapse toggle (the white pill on hover). The chevron direction already conveys what the button does; aria-label is kept for screen readers. - Expanded: chevron is pushed flush to the right edge of the rail via 'ml-auto md:-mr-2' (header gets pl-2/pr-0 so the negative right margin escapes most of the aside's pr-3). - Collapsed: chevron now sits in the same row as the logo (was stacked below). Collapsed rail widens from 64px → 80px with px-1.5 to fit a 46px logo + small w-5 chevron side-by-side. Nav: - Reorder Skills above Automations. Compact conversation list: - New-conversation '+' button was visually off-center because StyledTooltip wraps its children in inline-flex (sized to the trigger), which neutered the trigger's own mx-auto. Wrap the relative positioner in 'flex justify-center' when compact so the trigger is centered against the rail itself. - Hide 'older' conversations and the 'Load more' control in compact mode so the icon rail mirrors the expanded sidebar's default — recent (within the past hour) only, no archive cruft. Co-authored-by: openhands <openhands@all-hands.dev> * fix(sidebar): stack chevron below logo when collapsed Walks back the previous attempt to fit logo + chevron side-by-side in the collapsed rail (which required widening it to 80px). Going back to the original 64px rail with the chevron centered below the logo — cleaner visual, and consistent with the rail's icon-stack rhythm (logo, chevron, then the nav-link column). Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
491a3f6cdf |
Lazily load conversation history: 50 most recent first, paginate on s… (#252)
* Lazily load conversation history: 50 most recent first, paginate on scroll Previously every conversation page loaded its entire event history in a single REST call (`limit: 100` plus a WebSocket `resend_all` replay), which is slow and wasteful for long-running conversations. Now: - `EventService.searchEvents` takes an options bag (`limit`, `pageId`, `sortOrder`, `timestampGte`, `timestampLt`) and returns the raw `EventPage` so callers can paginate. Both local and cloud-proxy paths forward the params. - `useConversationHistory` fetches only the most recent `INITIAL_HISTORY_PAGE_SIZE` (50) events with `sort_order='TIMESTAMP_DESC'` and reverses them to chronological order. - `useLoadOlderEvents` is a new on-demand hook that paginates older events via `timestamp__lt = <oldest known timestamp>`. `ChatInterface` triggers it when the user scrolls within 80px of the top, shows a loading spinner, and preserves the visible scroll position by adjusting `scrollTop` by the inserted height delta. - `ConversationWebSocketProvider` waits for the REST query to settle before opening the main socket, then subscribes with `resend_mode='since'` and `after_timestamp = <latest preloaded event timestamp>`, falling back to `resend_mode='all'` if REST returned no events or errored. The legacy count-based history loading detection is dropped for the main connection. - Event store gained a bulk `addEvents` action (used for the initial REST seed and for "scroll-up" pagination) that re-sorts by timestamp once at the end so prepending an older page stays O(N log N) overall. Co-authored-by: openhands <openhands@all-hands.dev> * Render Messages when any renderable events exist The previous gate (`conversationUserEventsExist`) hid the chat whenever the loaded window contained no `source: 'user'` events. With the new lazy "50 most recent" REST fetch, long agent runs between user turns can push the original prompt out of the initial window, leaving Messages unmounted. Without rendered messages there is no scroll container content, so 'scroll up to load older' never fires — the user appears stuck on a blank chat. Switch the gate to `renderableEvents.length > 0`. The empty-state ChatSuggestions block above keeps its own `!userEventsExist && !hasSubstantiveAgentActions` gate, so brand-new conversations still show suggestions instead of an empty chat. Adds a regression test for the bug and a sibling test that the scroll handler issues `searchEvents({ timestamp__lt })` when the user scrolls near the top. Co-authored-by: openhands <openhands@all-hands.dev> * Handle paginated history edge cases Co-authored-by: openhands <openhands@all-hands.dev> * Trigger loadOlder when pinned at top or list doesn't overflow The browser only dispatches a 'scroll' event while `scrollTop` is actually changing. Two paginated-history cases never triggered: 1. The user is already at `scrollTop = 0` and tries to wheel further up (overscroll). No scroll event fires. 2. The initial 50-event window is short enough to fit on screen, so there is no scrollbar at all and nothing to scroll. In both cases the user had to scroll down a bit and back up to coax loadOlder into firing. Replace `handleScrollForPagination` with a single `maybeLoadOlder` predicate that fires when `scrollTop <= threshold` OR `scrollHeight <= clientHeight + threshold`, and wire it from three sources: - onScroll (existing path: user scrolls near the top) - onWheel (catches overscroll-at-top: scrollTop == 0 and deltaY < 0) - useEffect (catches no-overflow: re-runs after each rendered page so we keep filling until the viewport overflows or the server has no more older events) Tests cover all three trigger paths. Co-authored-by: openhands <openhands@all-hands.dev> * Fix paginated history follow-up issues Co-authored-by: openhands <openhands@all-hands.dev> * Harden paginated history responses Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
9c405c201a |
fix(conversations): key per-conversation queries by active backend (#275)
* fix(conversations): key per-conversation queries by active backend Switching workspace local→cloud→local caused the conversation route to fire "conversation does not exist or you do not have permission" until a hard refresh, because: * The `useUserConversation` query was keyed only by conversation id (`["user", "conversation", cid]`), not by the active backend. * While the cloud backend was active, the still-subscribed query (or its 30s `refetchInterval`) issued a `batchGetAppConversations([cid])` call that the cloud backend resolved to `null`, overwriting the cached entry under the shared key. * On switching back to local, the route remount looked up `["user", "conversation", cid]` and found a fresh (`staleTime: 5m`) `null`, so it short-circuited to the not-available toast and redirected to /conversations. This violated the documented invariant in active-backend-context.tsx that long-lived queries must include the active backend's `id` and `orgId` in their query keys so a backend/org switch becomes a brand-new query (as `usePaginatedConversations` already does). Apply the same convention to the three per-conversation hooks backed by the AgentServerConversationService: * `useUserConversation` * `useSubConversations` * `useBatchAppConversations` (currently unused but kept consistent) After this, the conversation route's cache identity changes on every backend/org switch, so stale cross-backend nulls cannot survive into the next switch and no full page refresh is required. Co-authored-by: openhands <openhands@all-hands.dev> * chore: prettier-format updated query keys Co-authored-by: openhands <openhands@all-hands.dev> * test(use-websocket): stub WebSocket in query-params test to avoid CI flake The MSW-backed "should support query parameters in WebSocket URL" test intermittently times out at the 5s waitForConnection step because `wsLink.broadcast()` from sibling tests leaks across the shared mock server (see the file header note and AGENTS.md). The assertion only needs to inspect the WebSocket URL, so switch to the same deterministic stubbed-WebSocket pattern already used by `onClose` immediately below. Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
6a54ff261b |
Add SELF_HOSTING.md: guide for hosting Agent Canvas on a VM (#273)
* Add SELF_HOSTING.md guide for VM deployments Documents how to self-host Agent Canvas on a virtual machine with defense-in-depth: cloud/host firewall + nginx HTTP basic auth + Let's Encrypt TLS + SESSION_API_KEY on the agent server. Walks through provisioning a VM, pointing a domain at it, and running 'npm run dev:dangerously-dockerless' behind nginx. Co-authored-by: openhands <openhands@all-hands.dev> * Refine SELF_HOSTING.md per review - Replace ASCII diagram with a mermaid flowchart. - Drop manual SESSION_API_KEY generation steps; the dev scripts auto- generate and persist the key at ~/.openhands/agent-canvas/session-api-key.txt. - Move host firewall / ufw guidance into an 'Advanced: defense in depth' section at the end so the main flow stays focused on the cloud firewall as the primary perimeter. Co-authored-by: openhands <openhands@all-hands.dev> * Correct architecture diagram: nginx -> ingress proxy -> backends The dev:dangerously-dockerless script runs a standalone ingress proxy on 127.0.0.1:8000 that routes by path: /api/automation/* -> automation backend (:18001) /api/*, /sockets -> agent server (:18000) /* -> Vite dev server (:3001) nginx only proxies to the ingress port, not to the three upstreams directly. Update the mermaid diagram, the operational tip about not exposing internal ports, and the 'ss -tlnp' check to reflect this. Co-authored-by: openhands <openhands@all-hands.dev> * Update SELF_HOSTING.md * Update SELF_HOSTING.md * Align detailed steps with the new Quickstart flow The Quickstart added by 30d1385 has 5 steps: provision -> secure -> run -> (optional) domain+nginx -> (optional) connect locally. Rework the detailed sections to match 1:1 instead of the previous flow that required a domain and nginx up front. - Section 1 'Provision a machine' no longer combines the domain step; it now mentions both cloud VMs and dedicated hardware (Mac Mini etc). - Section 2 'Secure the machine' reflects the default posture (only SSH inbound) instead of pre-opening 80/443. Ports 80/443 are opened later, in step 4. - Section 3 'Run the server' merges install + SESSION_API_KEY note + run command into one place, and adds an SSH-tunnel subsection so step 4 is genuinely optional. - Section 4 is now '(Optional) Get a domain and put nginx + Let's Encrypt + basic auth in front', combining the DNS/firewall opening with the nginx+certbot+basic-auth setup. - New section 5 '(Optional) Connect your local Agent Canvas to the remote machine' walks through Manage backends -> Add a backend using the persisted session-api-key.txt as the Session API key, including the SSH-tunnel and basic-auth caveats. - Updated the 'three lines of defense' summary to reflect that basic auth is now an optional layer that only applies if step 4 is done. Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
bae8e29f04 |
fix(agent-status): truncate status text instead of breaking per-character on narrow screens (#276)
When the chat input row was squeezed on small screens (Tools + model name consume most of the width), the AgentStatus container shrank far enough that 'whitespace-normal break-words' on the status label was forced to break on every character, producing a one-character-per-line 'Running task' waterfall next to the pause button. Drop flex-1 / whitespace-normal / break-words and use 'truncate' instead so the label collapses to a single ellipsized line; the existing title attribute still exposes the full text on hover. Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
b09f3a2275 |
add envrc to gitignore (#274)
Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
1c64a52d2c | fix: hide Code tab when active backend is local (#272) | ||
|
|
3f55a57ff8 |
fix(home): show git repo picker on home when active backend is cloud (#270)
* fix(home): show git repo picker on home when active backend is cloud Previously RepoConnector always rendered the local WorkspaceSelectionForm because agent-canvas only ever talked to a local agent_server. Now that a cloud backend can also be selected, branch on the active backend kind and render RepositorySelectionForm (provider / repo / branch dropdowns) when the active backend is cloud, leaving the workspace picker for local backends. Co-authored-by: openhands <openhands@all-hands.dev> * fix(sidebar): show repo picker on +New Conversation when active backend is cloud The sidebar 'New Conversation' popover previously always listed local workspaces. When the active backend is cloud, list git repositories from the connected provider instead. Clicking a repo launches a conversation against its default branch (falling back to 'main'); no branch picker is exposed — branches can still be switched once the conversation is running. Split the existing local-mode component out into new-conversation-button-local.tsx, add new-conversation-button-cloud.tsx, and make new-conversation-button.tsx a thin dispatcher that picks the right popover based on useActiveBackend().backend.kind. Co-authored-by: openhands <openhands@all-hands.dev> * refactor(home): make RepositorySelectionForm.onRepoSelection optional Address PR feedback on the no-op `onRepoSelection={() => {}}` callback in RepoConnector. The form is fully self-contained — it owns its own Launch button, calls createConversation, and navigates internally. The callback exists only so callers that want to mirror the selection in their own state can do so. Mark it optional, document the contract, and drop the no-op from RepoConnector. Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
3384aed454 |
Rearrange conversation right panel: tabs inside flush panel (#269)
* Rearrange conversation right panel: tabs inside flush panel - Wrap tabs row + content in a single bordered, lighter (#25272D) panel on the conversation page, so the tabs read as part of the panel chrome. - Remove the duplicate tab-name title bar inside each panel; refresh (changes) and Build (planner) actions move into the unified tabs row. - Invert tab colors: inactive tabs become transparent and blend into the panel; the active tab uses the darker page color. - Drop the conversation route's outer top/right padding and the ConversationMain top padding so the panel sits flush with the viewport edge; keep matching top padding on the chat panel for symmetry with the existing bottom padding. - Delete the now-unused ConversationTabTitle component and its tests; update conversation-tab-content / conversation-tabs tests for the new structure (no title text, action buttons inside the tabs row). Co-authored-by: openhands <openhands@all-hands.dev> * Fix right panel tab regression coverage Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
8482aeda26 |
Add multi-step onboarding modal (#257)
* Add multi-step onboarding modal with agent/backend/LLM/hello steps
Adds a four-step welcome flow that appears on first visit (gated by an
`openhands-onboarded` localStorage flag) on the home route:
0. Choose Agent — OpenHands selectable, Claude Code & Codex disabled
with a "coming soon" note
1. Check Backend — embeds the existing backend edit form, plus a
live connection banner driven by useBackendsHealth
2. Set up LLM — embeds the existing LlmSettingsScreen
3. Say hello — pre-filled greeting input that launches a fresh
conversation with no workspace
Each step lives in its own panel; advancing slides the rail
horizontally for the slide-in-from-the-right animation. A top progress
bar reflects the current step.
Also extracts a reusable BackendForm out of BackendFormModal so the
backend step can render the same form inline, and adds an
`onSaveSuccess` hook to LlmSettingsScreen so the onboarding flow can
auto-advance after saving.
Tests: 14 new tests cover the progress bar, completion hook, agent
chooser, and full modal navigation.
Co-authored-by: openhands <openhands@all-hands.dev>
* Fix onboarding slide rail layout & promote 'coming soon' note
Two related fixes for the onboarding modal:
1. Slide rail was using `width: 400%` + `translateX(-N * 100%)` on the
rail itself. CSS translate percentages are relative to the
*element's own* width, so on step 2 the rail shifted by its full
2240px width — pushing all step content off-screen and leaving an
empty modal sized to the tallest step. Reworked the rail so each
slide now translates by `(index - currentStep) * 100%` of its own
width, with inactive slides absolute-positioned (`inset-0`) inside
a relative parent. Active slide drives the modal height; inactive
slides no longer contribute vertical space, eliminating the
funky-overhang behavior.
2. Moved the 'Support for other agents coming soon!' note from a tiny
italic line at the bottom of the agent step to a prominent
primary-tinted callout pinned to the top, with a Sparkles icon and
bumped-up typography.
Tests assert per-slide transforms and active state instead of a
single rail transform; existing flow tests still cover the full
0 → 3 progression.
Co-authored-by: openhands <openhands@all-hands.dev>
* Polish onboarding LLM step: embed inline + default to Anthropic/Opus
The third onboarding step embedded `LlmSettingsScreen` as-is, which
brought along the screen's sticky `bg-base` save bar. In the modal
that rendered as a dark stripe overlapping the rest of the form, and
the existing per-step Next button created a confusing duplicate CTA
alongside Save Changes.
- Add an `embedded` prop to `SdkSectionPage` (and forward it through
`LlmSettingsScreen`) that drops the sticky positioning + contrast
background so the Save button just sits inline at the bottom of
the form, blending with the modal panel.
- Add an `initialValueOverrides` prop to `SdkSectionPage` that wins
over `useSettings`-derived defaults and pre-marks the overridden
keys dirty so the Save button is enabled on mount. Use it from the
onboarding LLM step to pin the model to
`anthropic/claude-opus-4-5-20251101` per the spec, instead of the
global OpenHands-prefixed default.
- Drop the redundant `onboarding-llm-next` button. Saving is now the
primary advance action via the existing `onSaveSuccess` hook; a
small "Skip for now" link remains for users who want to fill this
in later.
Onboarding modal test updated to drive step 2 to step 3 via the skip
link.
Co-authored-by: openhands <openhands@all-hands.dev>
* Drive LLM step's save from the onboarding Next button
The previous polish pass left the LLM settings form's "Save Changes"
button visible inside the onboarding modal, with a separate "Skip for
now" link doing the advancing. That's the wrong shape for an
onboarding flow — every other step has a single primary Next button at
the modal footer, and the user expects clicking Next to commit the
form and advance.
- Add `hideSaveButton` and `onSaveControlChange` props to
`SdkSectionPage` (forwarded through `LlmSettingsScreen`). The first
suppresses the built-in Save button + the `pb-20` placeholder it
reserved; the second hands the parent a stable `save()` callback
plus `{ isSaving, isDirty }` state so it can render its own button.
- Rebuild `SetupLlmStep`'s footer as a single primary "Next" button
matching the Backend step. Clicking Next:
* triggers `saveControl.save()` if there are dirty fields;
`onSaveSuccess` (wired to `onNext`) advances when the mutation
resolves successfully.
* falls through to `onNext()` when nothing is dirty, so users
with already-configured settings aren't blocked.
The Next button stays disabled while a save is in flight.
- Drop the previously-added "Skip for now" affordance — Next now
covers the no-dirty-fields case.
- Restore the test selector to `onboarding-llm-next` so the modal
test continues to drive step 2 → 3 the way users will.
Co-authored-by: openhands <openhands@all-hands.dev>
---------
Co-authored-by: openhands <openhands@all-hands.dev>
|
||
|
|
4b0f7d6b6e | fix: keep top padding constant between Conversations and Automations (#268) | ||
|
|
18f7f985e8 | fix: hide in-page settings navigation on desktop (#266) | ||
|
|
2d454b0405 |
Fix Vite build hygiene warnings (#264)
Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
4683fa90ae | fix: always load public skills on global Skills page (#263) | ||
|
|
fdaf3873fe | fix: align nav link default text color with settings page nav (#261) | ||
|
|
137fbae87f | fix: pass container workspaces path as VITE_WORKING_DIR (#259) | ||
|
|
8017bb5e1b |
Fix conversation right pane clipping when divider dragged far right (#256)
- Remove min-w-max from the right-pane inner wrapper in conversation-main so it can shrink with its container instead of forcing intrinsic width past the visible area. - Clamp the persisted left-pane width in useResizablePanels against the current min/max so a stale localStorage value can't push the divider out of bounds on load. - Drop the md:p-3 padding on /conversations* in root-layout, matching the existing /automations* behavior, so the conversation page renders edge-to-edge. Includes regression tests for all three changes. Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
65e5a3792b |
perf(frontend): shrink eager dev/prod graph with lazy boundaries (Tier 1) (#238)
Investigation showed the dev server was issuing hundreds of requests per
navigation because (a) the 1 MB src/i18n/translation.json was statically
imported into every chunk that touched i18n, (b) several conditionally-
rendered modals/overlays were eagerly imported by the always-mounted
root layout, (c) the terminal tab was statically imported alongside the
already-lazy peer tabs, and (d) the conversation-events/chat barrel
(plus its event-message-components/index.ts) fanned the dev graph out
to ~15 unrelated sibling modules whenever ChatInterface needed Messages.
Changes:
1. Move translation.json into src/i18n/resources.ts and re-export
translationResources from src/i18n/index.ts via a tree-shakable
'export … from' with a /* @__PURE__ */ annotation, so rollup drops
the JSON from the app build (custom-toast-handlers chunk:
909 KB -> 74 KB).
2. Lazy-load Terminal in conversation-tab-content.tsx alongside the
other six tabs; xterm + addon-fit + xterm.css now ship as a
separate ~336 KB chunk loaded only when the tab is opened.
3. Lazy-load conditional UI from the eager root graph:
- root-layout.tsx: AnalyticsConsentFormModal, AlertBanner,
EnvironmentSwitchOverlay
- sidebar.tsx: SettingsModal
- root.tsx: AgentServerConnectionForm
Each is wrapped in <Suspense fallback={null}>.
4. Split environment-switch-overlay into a tiny
environment-switch-store.ts (constants, triggers, snapshot) and the
React component file. backend-selector.tsx imports trigger helpers
from the store so the eagerly-mounted sidebar path no longer drags
the overlay's render code in. The overlay file re-exports the store
API for back-compat with existing tests.
5. Replace barrel imports of #/components/conversation-events/chat (and
its event-message-components barrel) with deep paths in
chat-interface.tsx, shared-conversation.tsx, use-filtered-events.ts,
messages.tsx, and event-message.tsx. Conversation chunk:
728 KB -> 392 KB.
Tests: build, typecheck, and lint pass; 39 affected test files (260
tests) pass. Updated root-layout.test.tsx to await findByTestId for the
now-lazy analytics consent modal. Pre-existing failures
(backend-selector.test.tsx Dropdown crash, root-layout-refetch.test.tsx,
library-namespace.test.ts under full-suite load, two unrelated prettier
errors) are unchanged.
AGENTS.md updated with the new constraints so future edits don't
accidentally re-bundle translation.json or import the chat barrel
inside src/.
Co-authored-by: openhands <openhands@all-hands.dev>
|
||
|
|
3f55620baf |
Fix/refresh convo list on create (#251)
* feat: always request a git worktree when creating conversations PR OpenHands/software-agent-sdk#3180 adds a 'worktree' boolean to the agent-server's StartConversationRequest. When true, the runtime creates a dedicated git worktree on an 'openhands/<conversation_id>' branch so agent changes stay isolated from the original checkout. Always set worktree: true on the local /api/conversations payload built by buildStartConversationRequest. The cloud SaaS path (AppConversationStartRequest) is a separate app-backend contract and is not affected by that PR. Co-authored-by: openhands <openhands@all-hands.dev> * fix: refresh start-tasks query when a conversation is created useCreateConversation already invalidates the paginated conversations list (matching the sidebar's ["user", "conversations", "paginated", ...] key by prefix), so on local backends the new conversation pops into the sidebar immediately. The cloud SaaS path, however, returns a start task that is surfaced via useStartTasks (queryKey ["start-tasks", "search"]) which has no refetchInterval, so an in-flight task wouldn't appear in the conversation list until the page was reloaded. Invalidate the start-tasks query alongside the conversations one so the list refreshes immediately for both ready conversations and provisioning tasks. Co-authored-by: openhands <openhands@all-hands.dev> * fix: invalidate start-tasks query when a conversation is deleted useDeleteConversation already invalidates ["user", "conversations"] on settle, which (by prefix match) refetches the sidebar's paginated conversation list. Mirror the create-side fix and also invalidate ["start-tasks"] so cloud-SaaS in-flight tasks are removed from the panel immediately, regardless of whether the deleted item was a ready conversation or a still-provisioning start task. Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev> |