mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 17:08:34 +08:00
cd03d9cf9c6dd6b08ec978e6d07f887eb62e3d1f
86
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
95b6d7a23e |
fix: persist OH_SECRET_KEY to secret-key.txt in dev mode, same as Docker (#957)
Dev mode (npm run dev) was always using the hardcoded static default key 'openhands-dev-secret-key-change-in-prod' from config/defaults.json. Docker mode (docker/entrypoint.sh) generates a random key on first run and persists it to ~/.openhands/agent-canvas/secret-key.txt. When both modes share the same ~/.openhands directory, they used different keys — causing decryption failures for any settings encrypted by the other mode. Fix: remove the static default in dev-safe.mjs and instead use getOrCreatePersistedApiKey() with a new DEFAULT_SECRET_KEY_PATH constant pointing to the same secret-key.txt file that Docker reads/writes. Whichever mode runs first generates and persists the key; the other picks it up automatically on next start. - Add DEFAULT_SECRET_KEY_PATH export to dev-safe.mjs - Replace 'env.OH_SECRET_KEY || DEFAULT_SECRET_KEY' with 'env.OH_SECRET_KEY || getOrCreatePersistedApiKey(secretKeyPath, "secret")' - Update startup log to show persisted file path (not 'default (for local development)') - Remove now-unused 'defaults.secretKey' from config/defaults.json - Update AGENTS.md to reflect the new shared-file behavior Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
eb3ae22b23 |
fix: fast-fail dev scripts when ports are already in use (#939)
* fix: fast-fail dev scripts when ports are already in use Add `assertPortsFree` to `scripts/dev-safe.mjs` that checks each preferred port and throws a descriptive error if any are already bound, rather than silently binding to an alternative port. - `buildSafeDevConfigAsync` now calls `assertPortsFree` before returning config, so `npm run dev:minimal` exits immediately with a clear message when the agent-server port is occupied. - `dev-with-automation.mjs`'s `buildConfig` does the same for all four service ports (ingress, agent-server, automation, vite), covering `npm run dev` and `npm run dev:static`. - `buildConfig` gains env-var overrides for internal service ports (`OH_CANVAS_SAFE_BACKEND_PORT`, `OH_CANVAS_SAFE_AUTOMATION_PORT`, `OH_CANVAS_SAFE_VITE_PORT`) so tests and advanced users can redirect ports without touching production defaults. Tests updated accordingly: - Old "falls back when port is busy" tests replaced with "throws when port is busy" equivalents. - New `assertPortsFree` describe block with three targeted cases. - `envWithIsolatedKeyPath` in `dev-with-automation.test.ts` now seeds high free ports so the pre-flight check passes when a real dev stack is running during local test execution. Closes #934 Co-authored-by: openhands <openhands@all-hands.dev> * refactor: address review bot suggestions on assertPortsFree - Check ports in parallel with Promise.all instead of sequentially (each check is independent I/O, so parallel is faster and more idiomatic) - Include vscode port in the buildSafeDevConfigAsync pre-flight check alongside the agent-server port, so a conflict on that internal port is also caught with a helpful message rather than a cryptic spawn error Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
caef236c10 |
mock-llm e2e: use bin/agent-canvas.mjs (production binary path) (#906)
Switch mock-LLM E2E tests from npm run dev:minimal (Vite dev server + agent-server only) to the full agent-canvas binary entry point — the same path real users exercise when they run `npx @openhands/agent-canvas`. This means tests now launch: - Pre-built static frontend (via static-server.mjs) - Agent-server via uvx - Automation backend via uvx - Ingress proxy unifying all routes on a single port Changes: - playwright.mock-llm.config.ts: replaced dev-safe.mjs webServer with bin/agent-canvas.mjs; single ingress port (18300) serves both the browser UI and proxied API calls; conditional build step for build/ - scripts/dev-with-automation.mjs: buildConfig now respects OH_CANVAS_SAFE_STATE_DIR from env (needed for test isolation) - tests/e2e/mock-llm/utils/mock-llm-helpers.ts: BACKEND_URL now defaults to the ingress URL (API calls are proxied transparently) - .github/workflows/mock-llm-e2e.yml: added npm run build:app step before tests (pre-build for CI caching) - AGENTS.md: added Mock-LLM E2E Tests section documenting the new production-fidelity test architecture Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
665a258b80 |
feat(acp): inline live model picker for ACP conversations (#769) (#832)
* feat(acp): inline live model picker for ACP conversations (#769) Converge ACP model selection onto the native LLM-profile inline picker UX with live mid-conversation switching, replacing the display-only popover. - Bump @openhands/typescript-client 1.23.3 -> 1.24.0 (adds switchAcpModel). - AgentServerConversationService.switchAcpModel(conversationId, model): POST /switch_acp_model via ConversationClient, with switchProfile's local-only guard. - useSwitchAcpModel hook: live switch for a running ACP session; for the home/no-session case, persist the choice as the agent-settings default (agent_settings_diff { acp_model }) so the next conversation inherits it. - ChatInputModel popover becomes a picker over the provider's available_models (check on the effective model), local backend only; cloud / custom-provider / native surfaces keep the display + Settings link. - New i18n key MODEL$AVAILABLE_MODELS. - Tests for the hook (live vs settings-default branches) and the picker. Local backend only (matches native switching); custom/unknown providers and any app_server route remain out of scope per #769. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(acp): open the model picker on click (don't self-close via click-outside) The inline picker's trigger button sits outside the popover element, so the document click-outside handler (useClickOutsideElement) treated the opening click as an "outside" click and closed the popover in the same interaction — clicking the chip appeared to do nothing. (A programmatic el.click() worked by fluke: the popover isn't rendered yet when that click bubbles, so the ref is null and the close is skipped.) Pass the trigger button as the hook's ignoreOutsideClickRef so a click on the chip toggles the popover instead of being treated as an outside click. Validated end-to-end against a local agent-server 1.24.0: the picker opens and lists the provider's available_models, and selecting one writes the default via PATCH /settings (home case), with the chip updating to the new model. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(acp): drop disableToast in useSwitchAcpModel so switch errors surface useSwitchLlmProfile sets meta.disableToast because it's wrapped by useSwitchLlmProfileAndLog, which re-surfaces errors via its own onError. useSwitchAcpModel is called directly (no such wrapper / no onError), so disableToast was silently swallowing failed switches and settings writes (e.g. a 409 before the first message, network errors, the cloud guard). Remove it and let the global mutation error toast report failures — simpler and gives the user feedback when a switch doesn't take. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(acp): share chat input model picker state * chore: address PR review feedback (#832) - Add unit test for useChatInputModelState pinning its branching contract, incl. the active-ACP getAcpProvider lookup (was home-only in old component). - Document why the overflow model submenu uses overflow-y-auto (scroll long model lists) rather than overflow-visible — no floating children to clip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: address PR review feedback (#832) - Wrap the 'Available models' section label in a presentational <li> so it is a valid child of the ContextMenu <ul> (was a bare <div>). - Drop unnecessary 'as never' casts in use-switch-acp-model tests now that the real return types (Promise<void>, Promise<boolean>) are honored. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(acp): bump agent-server pin to 1.24.0 for /switch_acp_model The inline ACP model picker POSTs to /api/conversations/{id}/switch_acp_model, which is new in openhands-agent-server 1.24.0. The PR description already lists agent-server:1.24.0 as a dependency, but config/defaults.json was left at 1.23.1, so local dev (npm run dev) and Docker installs would 404 on every model switch attempt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(settings): always land on /settings/agent from /settings The fallback order in ``getFirstAvailablePath`` put ``/settings/llm`` first whenever ``hide_llm_settings`` was off, so clicking Settings sent the user to the LLM page. For ACP users that page is disabled and ``redirectIfAcpActive`` only catches them when the *personal* settings already say ``agent_kind === "acp"`` — being in an ACP conversation with non-ACP personal settings (the common case during the inline picker flow) bypassed the guard and dumped them on /settings/llm. Make ``/settings/agent`` the unconditional first fallback. It is always available (no feature flag hides it), houses the agent-kind picker, and the left nav still gets OpenHands users to LLM in one click — so one extra click for non-ACP users buys a much simpler routing surface and kills the ACP misroute. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(settings/agent): clear command when switching to Custom preset Selecting "Custom" in the agent preset dropdown reset ``acpModel`` and flipped ``isCustomAcpModel`` but left ``commandText`` untouched. On the next render, ``detectPreset(commandText, ACP_PROVIDERS)`` still matched the previous provider's ``default_command`` and snapped the dropdown back off "Custom" — the toggle never stayed on Custom. Clear ``commandText`` in the Custom branch so ``detectPreset`` falls through to ``ACP_CUSTOM_PRESET_KEY`` on the next render and the dropdown stays where the user put it. Empty command also matches the intended "user supplies their own" semantics of the preset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(settings): mark Verification page as disabledByAcp The Verification page writes ``confirmation_mode`` and ``security_analyzer`` into ``conversation_settings_diff``. The ACP agent loop never reads either: ``openhands/sdk/agent/acp_agent.py`` has zero references to ``confirmation_policy`` or ``security_analyzer``, and the only runtime readers (``openhands/sdk/agent/agent.py:844,855``) live on the native ``Agent`` class — not on ``ACPAgent``. The backend accepts the values and stores them on conversation state, but the ACP subprocess never consults them. So the page presents real-looking knobs that silently do nothing for ACP users. Mark it ``disabledByAcp: true`` — same pattern as ``/settings/llm`` and ``/settings/condenser`` — so it greys out in the nav and the existing route guard at ``src/routes/settings.tsx:47-51`` bounces direct visits to ``/settings/agent``. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ci): bump doc/script SDK version examples to 1.24.0 The docs-version-sync test enforces that every documented agent-server version example matches ``config/defaults.json:versions.agentServer``. The previous commit bumped that pin from 1.23.1 to 1.24.0 for the ``/switch_acp_model`` route, but left the example references in AGENTS.md, ``scripts/dev-safe.mjs``, and ``scripts/check-sdk-version-sync.mjs`` behind — the drift-detector caught it as ``test-and-build`` failure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ci): bump remaining hard-coded 1.23.1 to 1.24.0 ``__tests__/scripts/dev-safe.test.ts`` asserts ``buildAgentServerCommand``'s default ``uvx`` args literally include ``openhands-agent-server==1.23.1`` and matching ``openhands-{sdk,tools,workspace}==1.23.1``. The CI fix in the prior commit only updated docs and example references; the central pin bump in ``config/defaults.json`` flowed through to this test's runtime expectation but the literal expectations were never updated. Bump them. Also bump the ``MOCK_AGENT_SERVER_VERSION`` placeholder in ``src/mocks/settings-handlers.ts`` for consistency with the central pin — no test asserts on it, but leaving the mock at 1.23.1 invites future drift confusion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Debug Agent <debug@example.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b570b4a2f6 |
fix: buildNpmScriptCommand always uses cmd.exe on Windows (#734)
* fix: buildNpmScriptCommand always uses cmd.exe on Windows On Windows, npm sets npm_execpath to a path like C:\Program Files\nodejs\node_modules\npm\bin\npm-cli.js which contains spaces. buildNpmScriptCommand was returning that path as a spawn argument with the full node.exe path as the command. spawnService uses shell:true on Windows, so Node.js passes the unquoted command to cmd.exe: cmd.exe /d /s /c C:\Program Files\nodejs\node.exe ... cmd.exe splits on the space and fails with 'C:\Program' is not recognized as an internal or external command causing Vite to exit with code 1 immediately after npm run dev. Fix: check platform === 'win32' BEFORE checking npm_execpath so Windows always uses the safe cmd.exe /d /s /c npm run <script> form. Co-authored-by: openhands <openhands@all-hands.dev> * fix: buildNpmScriptCommand always uses cmd.exe on Windows On Windows, npm sets npm_execpath to a path like C:\Program Files\nodejs\node_modules\npm\bin\npm-cli.js which contains spaces. buildNpmScriptCommand was returning that path as a spawn argument with the full node.exe path as the command. spawnService uses shell:true on Windows, so Node.js passes the unquoted command to cmd.exe: cmd.exe /d /s /c C:\Program Files\nodejs\node.exe ... cmd.exe splits on the space and fails with 'C:\Program' is not recognized as an internal or external command causing Vite to exit with code 1 immediately after npm run dev. Fix: check platform === 'win32' BEFORE checking npm_execpath so Windows always uses the safe cmd.exe /d /s /c npm run <script> form. Co-authored-by: openhands <openhands@all-hands.dev> * fix: AnimatePresence mode=wait expects one child, not two ChatStatusIndicator had two separately-keyed motion.span children inside <AnimatePresence mode="wait">. framer-motion's wait mode expects exactly ONE child to exit before the next enters; two children trigger the repeated warning: "attempting to animate multiple children within AnimatePresence, but its mode is set to 'wait'" Fix: wrap both elements in a single motion.span with unified key={status} and className="contents" (CSS display:contents preserves flex layout). Co-authored-by: openhands <openhands@all-hands.dev> * fix: set PYTHONUTF8=1 in agent-server/automation env on Windows Python on Windows defaults to the system ANSI codepage (cp1252). The agent-server writes metadata JSON containing emoji (e.g. U+2705 ✅) that cp1252 cannot encode, producing UnicodeEncodeError → POST /api/conversations 500. Old UTF-8 conversation files also fail to load at startup (UnicodeDecodeError). PYTHONUTF8=1 enables Python's UTF-8 mode (PEP 540) for the process, matching Linux/macOS behaviour. Co-authored-by: openhands <openhands@all-hands.dev> * fix: set PYTHONUTF8=1 in agent-server/automation env on Windows Python on Windows defaults to the system ANSI codepage (cp1252). The agent-server writes metadata JSON containing emoji (e.g. U+2705 ✅) that cp1252 cannot encode, producing UnicodeEncodeError → POST /api/conversations 500. Old UTF-8 conversation files also fail to load at startup (UnicodeDecodeError). PYTHONUTF8=1 enables Python's UTF-8 mode (PEP 540) for the process, matching Linux/macOS behaviour. Co-authored-by: openhands <openhands@all-hands.dev> * refactor: remove unrelated file --------- Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: hieptl <hieptl.developer@gmail.com> |
||
|
|
cbeeee002e |
fix: inject runtime session key into index.html for published binary (#795)
* fix: inject runtime session key into index.html for published binary
The globally installed agent-canvas binary starts the agent-server with a
persisted session API key (~/.openhands/agent-canvas/session-api-key.txt)
as OH_SESSION_API_KEYS_0, making auth required. However, the pre-built
static frontend in the npm package has a different (or empty)
VITE_SESSION_API_KEY baked in at publish time, so every API request gets
401 Unauthorized.
Fix: static-server.mjs now accepts --session-api-key <key> and injects a
tiny bootstrap <script> before </head> in every index.html response. The
script seeds the key into localStorage['openhands-agent-server-config']
only if no key is already stored there, so explicit user overrides (via
Settings > Agent Server) are always preserved.
dev-with-automation.mjs and dev-static.mjs both pass
--session-api-key ${config.sessionApiKey} when spawning the static server,
so the runtime key is always available regardless of what was baked into
the bundle.
Tests: added 8 new cases to __tests__/scripts/static-server.test.ts
covering parseArgs, injection in direct and SPA-fallback index.html
responses, no injection for non-html assets, cache headers, and null key.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix(docker): pass runtime session key to static-server so frontend can authenticate
The entrypoint computed EFFECTIVE_SESSION_KEY and forwarded it to the
agent-server (OH_SESSION_API_KEYS_0) and automation backends, but did not
pass it to the static-server. As a result the pre-built index.html served
with no session key injected, so every browser API call received 401.
Wire --session-api-key "$EFFECTIVE_SESSION_KEY" into the static-server
launch command so the runtime key is injected into index.html responses
(via the mechanism added in this branch to static-server.mjs).
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: address review suggestions on session key injection
- static-server.mjs: add comment clarifying replace() targets first
</head> only; fall back to inserting before </body> when </head> is
absent (avoids prepending before <!DOCTYPE html>)
- docker/entrypoint.sh: add comment documenting source of
EFFECTIVE_SESSION_KEY before the static-server invocation
- static-server.test.ts: add test for </head>-absent fallback path
confirming injection lands before </body>
Co-authored-by: openhands <openhands@all-hands.dev>
---------
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
|
||
|
|
e2dd1b5f17 |
fix: unify session and automation API keys into a single credential with consistent header (#681)
* fix: unify session and automation API keys into a single credential Both the agent-server and automation backend now share the same API key value. The agent-server validates it via `X-Session-API-Key` and the automation backend validates it via `Authorization: Bearer …` — different header formats, same credential. Changes: - Frontend: automation axios client reads `VITE_SESSION_API_KEY` instead of the now-removed `VITE_AUTOMATION_API_KEY` - Dev launcher: removed separate `AUTOMATION_LOCAL_API_KEY` generation and persistence (`automation-api-key.txt`); `localApiKey` is set to `sessionApiKey` so both backends receive the same value - Static build: stopped baking `VITE_AUTOMATION_API_KEY` (the frontend reads from `VITE_SESSION_API_KEY`) - Docker entrypoint: `OPENHANDS_AUTOMATION_API_KEY`, `AUTOMATION_LOCAL_API_KEY`, and `AUTOMATION_AGENT_SERVER_API_KEY` all default to the session key when not explicitly overridden - Tests updated to verify unified key behavior Fixes the 401 on `/api/automation/v1` when the automation backend is running but no separate `VITE_AUTOMATION_API_KEY` was configured. Co-authored-by: openhands <openhands@all-hands.dev> * fix: use X-Session-API-Key header for automation backend auth (consistent with agent-server) Switch automation backend requests from `Authorization: Bearer …` to `X-Session-API-Key` header, matching the agent-server's auth pattern. Both backends now authenticate using the same header and the same key value (`VITE_SESSION_API_KEY`). Co-authored-by: openhands <openhands@all-hands.dev> * fix: address review — remove localApiKey alias, dead constant, add entrypoint guard - Remove `localApiKey` from config; all call sites now use `config.sessionApiKey` directly, making the unified-key intent obvious. - Delete `DEFAULT_AUTOMATION_API_KEY_PATH` constant and its export (no downstream consumers in beta). - Add fail-fast guard in docker/entrypoint.sh when no session key is available, instead of silently exporting empty strings. Co-authored-by: openhands <openhands@all-hands.dev> * fix: update stale comment on AUTOMATION_LOCAL_API_KEY to reflect unified session key Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
45da5606d6 |
fix: bump agent-server SDK to 1.23.1 (#782)
Fixes two bugs in the upstream agent-server SDK v1.23.0 that caused 500 Internal Server Error on POST /api/conversations: 1. LLM registry duplicate usage_id: The condenser and main agent LLMs both used usage_id='default', causing ValueError on conversation creation. v1.23.1 checks for existing usage IDs before registering. 2. Validation error handler crash: The _validation_exception_handler tried to JSON-serialize raw ValueError objects from Pydantic validation contexts, turning 422 errors into 500s. v1.23.1 properly sanitizes validation errors before serializing. Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
cc63f48a07 |
refactor(acp): source ACP model lists from typescript-client (closes #740) (#775)
* refactor(acp): source model lists from typescript-client registry Replace the hand-mined CLAUDE_MODELS / CODEX_MODELS / GEMINI_MODELS lists (and the duplicated provider metadata) with the @openhands/typescript-client ACP registry, which mirrors the Python SDK source of truth (openhands.sdk.settings.acp_providers). acp-providers.ts becomes a thin adapter: it enriches each upstream record with Canvas-only UI fields (brand icon + onboarding description) and keeps the helper functions + public export surface unchanged, so no consumers change. - Bump the @openhands/typescript-client pin to the #187 merge commit (082d4d46), which adds available_models / default_model to the registry. - Delete the three hardcoded model lists; build ACP_PROVIDERS from getAcpProvider() + a small ACP_PROVIDER_UI map. - Incidentally corrects the Gemini default to auto-gemini-2.5 (the CLI's auto-router default), matching the merged SDK/client. Closes #740. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(acp): pin typescript-client to v1.23.2 tag (was SHA) Now that typescript-client v1.23.2 is tagged/released (includes #187's ACP registry, mirroring SDK #3389), pin to the tag instead of the raw #187 merge SHA. v1.23.2 tracks the SDK's v1.23.2 patch line. Resolves to the same commit as the prior SHA, so no resolved-content change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(acp): remove obsolete ACP providers sync check The acp-providers-sync workflow + scripts/check-acp-providers-sync.mjs existed to keep Canvas's hand-kept ACP registry mirror in sync with the SDK source (agent-canvas#587). That mirror is gone — acp-providers.ts now sources its model data from @openhands/typescript-client, which carries its own SDK-drift check (check-acp-drift.py). So this canvas-side check is redundant and was failing on the refactored ACP_PROVIDERS (no longer a literal array). - Delete .github/workflows/acp-providers-sync.yml + the script. - Drop the docs-version-sync test case that asserted the script's example. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Debug Agent <debug@example.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c9616c1153 |
fix: pin openhands-sdk to same version as other SDK packages (#772)
Add openhands-sdk==${VERSION} to the --with list in both PyPI
resolution branches (specific version and default) of
buildAgentServerCommand so all four packages are pinned to the
same versions.agentServer:
openhands-agent-server, openhands-sdk, openhands-tools, openhands-workspace
Without this pin, openhands-sdk floated to the latest release on
PyPI (a transitive dep with no version bound in the published
openhands-agent-server metadata), causing non-reproducible builds
and version skew between the banner and defaults.json.
The local-path (editable) and git-ref branches are unaffected —
they already source all packages from the same checkout/ref.
Update the two affected unit-test cases to expect the new pinned
openhands-sdk entry in the args array.
Fixes #767
Co-authored-by: openhands <openhands@all-hands.dev>
|
||
|
|
e54b04cf38 | chore: refresh stale 1.22.1 doc examples after agent-server bump to 1.23.0 (#694) | ||
|
|
59058c7609 |
fix(ui): left navigation rail, mobile drawer, and responsive chrome (#623)
* fix: archived row icon, default-local config sync, and dockerless dev fixes Archived MISSING sandboxes show an archive icon in the status column instead of a gray dot and pill; ERROR sandboxes keep the error pill. Harden port checks and automation CORS for alternate frontend ports, set agent-server HOME to the host home on macOS, sync legacy agent-server config when editing the default-local backend, and clarify static stack rebuild/skip-build behavior. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): sidebar conversation list flush to rail with stable gutter Drop expanded aside `md:pr-0` so the thread scrollbar aligns with the rail while nav, logo row, and footer keep horizontal padding. Use scrollbar-gutter on the conversation list for consistent right inset with or without overflow. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): 40px backend selector and non-italic combobox text Pin the backend Dropdown trigger to h-10, add an italicPlaceholder escape hatch, and force upright type for value/placeholder in the selector. Add a subtle top border above Add Workspace in the new-conversation menu. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): full-bleed sidebar footer divider to left rail Pull the backend block border past aside `pl-2` with matching width calc; keep inner `px-2` so controls stay aligned with nav. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): align context menus with sidebar filter menu and muted row icons Match list padding, border, and shadow to the conversations filter surface; use theme foreground/muted tokens; reserve leading icons as muted until row hover/focus. Simplify list-item rows and remove unused context-menu height constant. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): unify menu dividers, section labels, and filter actions Standardize full-bleed menu dividers via Divider inset="menu", give filter menu section headings consistent pt-1 padding, and add icons to hide/show and delete-all rows in the conversation panel filter menu. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): rename sidebar conversations link to New Chat The /conversations nav entry should read "New Chat" instead of "Code" to match user expectations for starting a conversation. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): polish cloud new-thread popover search and repo list Use a flat search row with icon and menu divider, align horizontal padding with list items, and show a custom scrollbar on the repository list. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): unify filter menu section headings and action icons Share pt-1 section label padding via MenuHeading, fold hide/delete rows into MenuRow with Eye and Trash icons, and add a regression test for both actions. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): add settings tooltip and neutral delete-all row Show a white hover tooltip on the backend selector settings button and style Delete all like other filter menu actions instead of danger red. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): theme scrollbars/skeletons and improve settings tooltips Derive scrollbar colors from the cool-grey scale, use interactive-active for skeleton loaders, and fix settings tooltip placement when the drawer is open plus collapsed-sidebar coverage. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(ui): replace mobile top nav with left drawer On small screens, hide the horizontal sidebar strip and open the full vertical nav from a top-bar chevron toggle, matching the desktop collapse control icon. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): restore mobile page gutters and center home title Apply 14px horizontal padding in the root outlet on mobile, keep conversation full-bleed, and center the home heading with w-full text-center. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): correct mobile nav icons and home title line height Use PanelLeft for the mobile menu trigger, ChevronLeft to close the drawer, and relax the home headline leading when it wraps. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): remove border under mobile nav menu bar The top-bar menu trigger no longer draws a divider above page content. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(ui): mobile settings/customize hubs and animated nav drawer On mobile, Settings and Customize open list hubs matching desktop left nav; detail pages show drawer and back controls in the top bar. The slide-out nav drawer now animates open and closed with a fading scrim. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): keep sidebar icons aligned when collapsing the rail Use shared h-10 row geometry and a fixed icon column so collapsed and expanded states share padding and gap; labels clip instead of recentering. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): stabilize collapsed sidebar icon column and hover targets Use a shared 18px icon slot in both rail states, square collapsed controls for hover/active, and symmetric rail padding so icons and the logo stay aligned without full-width row highlights. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): refine collapsed sidebar slots and empty-list load-more Use consistent px-2.5 rail padding with 40×40 collapsed icon slots via SidebarCollapsedIconSlot, fix backend dot anchoring and logo alignment, and hide “Load more” when no conversations are visible after filtering. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): keep conversations header on one line during sidebar resize Use flex-nowrap with a truncating title so the toolbar row does not wrap while the drawer animates, and nudge the collapsed backend status dot up-left. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): align DropdownMenu padding and tighten backend footer actions Use uniform `p-1` on the combobox menu panel to match other menus, and remove vertical gap between Add/Manage backend items in the selector footer. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): trim vertical padding on cloud repo search row Drop wrapper `py-1` so the new-conversation repo search aligns with the dropdown chrome; horizontal inset stays `px-2`. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): flush cloud repo menu divider against search and list Drop flex `gap-1` on the popover so the rule sits tight to the search row and repository list; keep tab stripe spacing with `py-1` on the provider row. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): show backend settings tooltip above when sidebar is expanded Use `useSidebarCollapsed()` so the gear tooltip uses top placement on the full-width rail while keeping left placement for the icon-only strip unless the conversation right drawer is open. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): use full-screen route for mobile tools panel and tidy conversation chrome Restore horizontal inset on the conversation route, fold the sidebar opener into the chat header, and open Files/Tools via `/conversations/:id/panel` with a dedicated back affordance instead of a bottom sheet. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): only show sidebar drawer toggle when the rail is hidden The hamburger used the 1024px layout breakpoint while the sidebar stays visible from the `md` rail width up, so it duplicated chrome between tablet widths. Gate the toggle and header padding on the same max-md width as the rail (<=767px). Mirror the right-panel icon horizontally for the right-side drawer. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): align mobile conversation chrome, chat padding, and settings scroll Unify mobile top-bar icon buttons with shared classes; add compact conversation tabs and consistent horizontal padding for chat plus stable composer/git chrome. Move settings and extensions horizontal inset into layout helpers and drop the root outlet gutter so pages control their own padding. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): home mobile padding and Lucide sizes in chat mobile header Add px-4 to the home shell on small viewports and shift launcher inset to md+ only. Pin PanelLeft/ChevronLeft to 20px to match the right-panel icon and restore chat header left inset when the rail menu toggle shows. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): use block-drawer icon for mobile sidebar toggle Match the right-panel control’s SVG so both header icons share the same optical weight instead of Lucide PanelLeft filling the hit target. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: remove unrelated file * refactor: remove unrelated files * fix: failing tests --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: hieptl <hieptl.developer@gmail.com> |
||
|
|
f42d684a03 |
fix(frontend): validate OH_AGENT_SERVER_LOCAL_PATH in dev-with-automation (#650)
* fix: validate OH_AGENT_SERVER_LOCAL_PATH in dev-with-automation * fix: failing tests |
||
|
|
979e64fe19 |
feat: add Docker CI to build all-in-one image with agent-server + automation + frontend (#634)
* feat: add Docker CI to build all-in-one image with agent-server + automation + frontend
Adds a GitHub Actions workflow (.github/workflows/docker.yml) that builds and
publishes ghcr.io/openhands/agent-canvas — a single Docker image combining:
1. Agent Server (ghcr.io/openhands/agent-server base image from SDK repo)
2. Automation server (pip-installed from openhands-automation)
3. agent-canvas frontend (static build from this repo)
The automation server is pip-installed rather than copied from its Docker image
because both services share openhands-sdk, fastapi, uvicorn, pydantic, httpx
etc. — installing into the agent-server's Python 3.13 deduplicates all shared
packages. Only automation-specific deps (asyncpg, sqlalchemy, boto3, …) are
added on top.
An entrypoint script starts all three services and a static-server proxy that
unifies them behind a single port (default 8000):
/api/automation/* → automation backend (:18001)
/api/* → agent-server (:18000)
/* → static frontend + SPA fallback
Workflow triggers:
- Push to main: builds and pushes with branch + SHA tags
- v* tags (releases): also pushes semver tags (1.2.3, 1.2, 1, latest)
- PRs: builds, pushes SHA-tagged image, updates PR description with
pull/run instructions (same pattern as the SDK repo)
- workflow_dispatch: supports overriding base image and automation version
Files added:
- docker/Dockerfile (multi-stage: frontend build + agent-server base)
- docker/entrypoint.sh (process manager for all three services)
- .dockerignore
- .github/workflows/docker.yml
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: build multi-arch Docker images (amd64 + arm64)
Adds QEMU setup for cross-compilation and defaults the platform matrix
to linux/amd64,linux/arm64 so the image works on both Intel and Apple
Silicon machines.
Co-authored-by: openhands <openhands@all-hands.dev>
* refactor: rewrite Docker workflow to match SDK repo structure
Replace the single-job QEMU approach with the same architecture-matrix
pattern used by the SDK repo's server.yml:
1. build-and-push-image — matrix over {amd64, arm64} with native runners
(ubuntu-24.04 for amd64, ubuntu-24.04-arm for arm64). Each job pushes
arch-suffixed tags (e.g. sha-abc1234-amd64) and uploads build-info
artifacts.
2. merge-manifests — downloads both arch build-infos, strips the -amd64
suffix from amd64 tags to derive manifest tags, and creates multi-arch
manifests via `docker buildx imagetools create`.
3. consolidate-build-info — aggregates all build-info and manifest-info
artifacts into a single JSON summary (PR-only).
4. update-pr-description — renders the summary into the PR body between
AGENT_CANVAS_DOCKER_START/END markers.
Native runners avoid the 3-5× slowdown of QEMU emulation for arm64
builds.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: sanitize branch names in Docker tags (/ is not allowed)
Branch names like 'feat/docker-ci' produce invalid Docker tags because
'/' is forbidden in tag names. Replace '/' with '-' so the tag becomes
'feat-docker-ci-amd64'.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: default automation to SQLite and fix wait blocking proxy startup
Two bugs:
1. The automation server defaults to PostgreSQL on localhost, which
doesn't exist in the all-in-one container. Default AUTOMATION_DB_URL
to sqlite+aiosqlite:// so it works out of the box. Users can override
with a real Postgres URL for production.
2. The bare 'wait' command waited for ALL background children — including
the long-running agent-server and automation processes — so the
static-server/proxy on port 8000 never started. Fix by waiting only
for the wait_for_port subshell PIDs.
Verified locally: all three services start, endpoints respond correctly,
no more scheduler ConnectionRefusedError.
Co-authored-by: openhands <openhands@all-hands.dev>
* feat: add VOLUME directives for persistence and project mounts
Declare /home/openhands/.openhands (settings, secrets, conversations,
automation SQLite DB) and /projects (user code) as Docker volumes so
data survives container restarts by default. Users should bind-mount
these for durable persistence:
docker run -v ~/.openhands:/home/openhands/.openhands \
-v ~/projects:/projects \
-p 8000:8000 ghcr.io/openhands/agent-canvas
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: set OH_SECRET_KEY default and pre-create persistence dirs
Three issues fixed:
1. OH_SECRET_KEY was not set → agent-server refused to return encrypted
secrets → conversation creation failed with 503. Set the same static
default used by dev-safe.mjs / dev-docker.mjs.
2. Persistence dirs (conversations, bash_events, automation DB) were not
pre-created → the openhands user got PermissionError when the VOLUME
directive created them as root. Pre-create with correct ownership
before the USER switch in the Dockerfile.
3. Set OH_PERSISTENCE_DIR, OH_CONVERSATIONS_PATH, OH_BASH_EVENTS_DIR
defaults in the entrypoint (matching dev-docker.mjs) so data lands
under the well-known ~/.openhands tree.
Verified locally: all three services start clean, no warnings about
OH_SECRET_KEY, SQLite migrations apply successfully.
Co-authored-by: openhands <openhands@all-hands.dev>
* chore: merge main and remove stale dev-docker.mjs references
Main removed scripts/dev-docker.mjs (Docker is no longer a dependency of
the npm package flow). Update comments in docker.yml, entrypoint.sh, and
AGENTS.md that referenced the deleted file.
Co-authored-by: openhands <openhands@all-hands.dev>
* feat: centralize config into config/defaults.json (single source of truth)
All version pins, port defaults, persistence paths, package names, and
the dev secret key now live in config/defaults.json. Consumers read from
it instead of hardcoding values:
- scripts/dev-safe.mjs: reads via JSON.parse(readFileSync(...))
- scripts/dev-with-automation.mjs: same
- scripts/check-sdk-version-sync.mjs: same (no longer regex-parses JS)
- docker/Dockerfile: config-gen build stage converts JSON to
/opt/agent-canvas/defaults.env (shell-sourceable)
- docker/entrypoint.sh: sources defaults.env at startup; also adds
session API key auto-generation so the image doesn't run wide-open
- .github/workflows/docker.yml: reads versions from JSON in a setup
step (no more hardcoded env vars)
To bump a version, edit config/defaults.json only.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: address PR review feedback (#634)
- Fix PID tracking bug: move PIDS+=($!) inside if/elif branches so the
else (automation-not-found) path doesn't add a stale PID
- chmod 600 session API key file to prevent credential leak
- Warn when using insecure default OH_SECRET_KEY in Docker entrypoint
- Add try/catch + field validation for config/defaults.json loading in
check-sdk-version-sync.mjs
- Fix semver tag parsing: strip pre-release/build metadata, only create
abbreviated tags (major.minor, major, latest) for stable releases
- Sanitize branch names for Docker tags (tr invalid chars, strip leading
dot/dash) to handle branches with #, @, spaces, etc.
- Add arch validation before manifest merge (assert both amd64.json and
arm64.json exist)
- Remove $schema reference to non-existent defaults.schema.json
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: remove hardcoded version defaults from Dockerfile
Replace hardcoded ARG defaults (AGENT_SERVER_IMAGE, AUTOMATION_VERSION)
with empty ARGs. Values are always derived from config/defaults.json:
- CI: reads JSON in the workflow config step, passes --build-arg
- Local: new scripts/docker-build.mjs helper reads JSON and invokes
docker build with the correct --build-arg values
Added npm run build:docker convenience script.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: stabilize snapshot tests and auto-generate Docker secret key
Two fixes:
1. **Flaky snapshot tests**: The 'Local pagination fixture' mock conversation
used a fixed absolute timestamp (PAGINATION_BASE_TIME = May 13, 2026) for
its created_at/updated_at, while 'Errored Project' used a relative
timestamp (now - 7d). As real time progressed past the crossover point,
their sort order in the sidebar flipped, causing 30/73 snapshot diffs on
every PR. Fix: use relative timestamps (now - 6d) for the pagination
fixture's conversation listing fields. The internal event timestamps
(used by pagination tests) still use PAGINATION_BASE_TIME — only the
sidebar ordering is affected.
2. **Docker OH_SECRET_KEY**: The entrypoint used a static insecure default
for OH_SECRET_KEY and warned about it. Now mirrors the session API key
pattern: auto-generate a cryptographic random key on first run, persist
it to ~/.openhands/agent-canvas/secret-key.txt, and reuse on restart.
Users can still override via the OH_SECRET_KEY env var. Removed the
now-unused CONFIG_SECRET_KEY from the Docker defaults.env generation.
Also deduped STATE_DIR computation (was repeated for session key path).
Co-authored-by: openhands <openhands@all-hands.dev>
* docs: update AGENTS.md with mock timestamp and Docker secret key notes
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: include canvas_ui tool in Docker image
The Docker image was missing the tools/ directory and OH_EXTRA_PYTHON_PATH,
so the agent-server couldn't import canvas_ui_tool.py when the frontend
sent canvas_ui in the conversation tools list. This caused:
HTTP 500: ToolDefinition 'canvas_ui' is not registered
Fix: COPY tools/ into the image and set OH_EXTRA_PYTHON_PATH in the
entrypoint, matching what scripts/dev-safe.mjs already does for local dev.
Co-authored-by: openhands <openhands@all-hands.dev>
---------
Co-authored-by: openhands <openhands@all-hands.dev>
|
||
|
|
edb998220a |
Remove Docker dependency from dev workflow (#635)
- Delete scripts/dev-docker.mjs and its test - Simplify package.json: 'npm run dev' now runs local uvx stack directly (agent-server + automation + Vite + ingress), no Docker needed - Remove dev:docker, dev:docker:dynamic, dev:dangerously-dockerless scripts - Add dev:static for production-build frontend variant - Update bin/agent-canvas.mjs CLI to use uvx-based stack - Rename Docker-specific variables: DOCKER_PROJECTS_PATH → PROJECTS_PATH, shouldDefaultToDockerProjects → shouldDefaultToProjectsPath - Update i18n: HOST_HOME_NOT_MOUNTED_HINT no longer references Docker - Update all docs (README, DEVELOPMENT, SELF_HOSTING, AGENTS.md, CHANGELOG) - Rename e2e snapshot: docker-workspace-browser → projects-workspace-browser - Fix all tests to reflect new script names and remove Docker references Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
90ad71dbd9 |
Add recommended automations and MCP marketplace setup flow (#504)
* Add recommended automations marketplace flow Co-authored-by: openhands <openhands@all-hands.dev> * Update GitHub MCP QA findings Co-authored-by: openhands <openhands@all-hands.dev> * Polish MCP and automation marketplace UI Add a shared MCP logo badge, make marketplace cards more compact, and show required MCP logos prominently on recommended automation cards. Update the extensions package lock to the per-entry catalog split and save QA screenshots/results in .pr/. Co-authored-by: openhands <openhands@all-hands.dev> * Align recommended automations styling Remove the custom gradient treatment and match the recommended automation cards and setup modal to the existing Automations and MCP page surfaces, spacing, borders, and typography. Co-authored-by: openhands <openhands@all-hands.dev> * Show existing automations before recommendations Move the recommended automations section below the current automation list and creation guidance so the page prioritizes the user existing automation state. Co-authored-by: openhands <openhands@all-hands.dev> * Add recommendations to onboarding Show recommended automations below the Say Hello input so new users can launch a curated automation from the final onboarding step. Co-authored-by: openhands <openhands@all-hands.dev> * Use extensions MCP marketplace exports Remove the local MCP marketplace wrapper and consume MCP catalog data plus logo mappings directly from @openhands/extensions/mcps. Co-authored-by: openhands <openhands@all-hands.dev> * Archive previous PR QA and add refreshed artifacts Co-authored-by: openhands <openhands@all-hands.dev> * Add scheduled automation QA evidence Co-authored-by: openhands <openhands@all-hands.dev> * Streamline recommended automation launch Co-authored-by: openhands <openhands@all-hands.dev> * Refresh QA evidence and remove old artifacts Co-authored-by: openhands <openhands@all-hands.dev> * Ensure canvas tools are on Python path * Require MCP installs before launching recommendations * Remove archived PR artifacts * Drop redundant PYTHONPATH launcher changes * Inline recommended automation catalog * Point extensions dependency at main * Augment recommended automation prompts with explicit API instructions When a recommended automation is selected, the pre-filled prompt now includes backend-specific API instructions so the agent calls the correct endpoint: - Local backends: directs the agent to use the local automation API from <RUNTIME_SERVICES> with $OPENHANDS_AUTOMATION_API_KEY auth, and explicitly tells it NOT to call the cloud API at app.all-hands.dev. - Cloud backends: directs the agent to use the OpenHands Cloud Automations API at app.all-hands.dev with Bearer $OPENHANDS_API_KEY. The buildAutomationPrompt() helper is exported for testability. Three new unit tests cover both backend kinds and prompt preservation. Co-authored-by: openhands <openhands@all-hands.dev> * Fix recommended automation launch regressions Co-authored-by: openhands <openhands@all-hands.dev> * Expose local automation API key to agent terminals Co-authored-by: openhands <openhands@all-hands.dev> * Stabilize conversation panel stop menu test Co-authored-by: openhands <openhands@all-hands.dev> * chore: Remove PR-only artifacts * fix: pin @openhands/extensions to specific commit for reproducibility Updates the dependency from #main to the exact commit SHA (3bba8e3b) that contains the MCP and automation catalogs added in extensions#237. This ensures reproducible builds since npm ci will use the locked SHA instead of potentially picking up a newer main. * Address final automation marketplace review comments Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com> |
||
|
|
56fc790076 |
ci: detect drift between TS ACP_PROVIDERS mirror and SDK source (#630)
Closes #587. PR #416 introduced src/constants/acp-providers.ts as a hand-kept TypeScript mirror of the Python registry in openhands-sdk/openhands/sdk/settings/acp_providers.py (OpenHands/software-agent-sdk). Drift between the two is hazardous: during PR #416's E2E we briefly shipped ["npx","-y","@openai/codex","acp"], which is not a valid ACP server, and the agent-server deadlocked on the handshake instead of failing loudly. This commit adds the minimum infrastructure to catch that class of drift before it lands: - scripts/check-acp-providers-sync.mjs fetches the SDK file (from a configurable ref, default `main`), parses both registries with a string-aware brace matcher, and diffs them on the three fields canvas mirrors: key, display_name, default_command. The richer SDK record (api_key_env_var, session mode, agent_name_patterns, etc.) is intentionally not compared because canvas does not mirror it. --sdk-file lets the script run offline against a local SDK checkout for development. - .github/workflows/acp-providers-sync.yml runs the check on PRs touching the mirror or the script, on push to main, on a daily cron (to catch SDK-side drift that did not ping us), via workflow_dispatch, and via repository_dispatch (so the SDK repo can trigger us when it changes acp_providers.py — payload key `sdk_ref`). Co-authored-by: Debug Agent <debug@example.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f0c36bac8f |
feat(acp): Settings → Agent + onboarding + chat-UI gating for ACP-driven conversations (#416)
* feat(acp): add minimal ACP agent UI (parity with OpenHands#14401) Adds a Settings → Agent page so users can switch the conversation between the built-in OpenHands agent and an external ACP (Agent Client Protocol) subprocess (Claude Code, Codex, Gemini CLI, or custom command) without hand-editing settings. Discriminates in agent-server-adapter: when `agent_settings.agent_kind === "acp"`, build an `ACPAgent` payload (kind, acp_command, acp_model) instead of the LLM-shaped Agent, and skip the LLM defaults that would otherwise be rejected as extras. Stamps the provider key onto `tags.acpserver` so the chip can resolve a brand name from a single source. Tag-key constant note: the conventional `acp_server` form is invalid — agent-server validates tag keys against `^[a-z0-9]+$` and returns 422. The flattened `acpserver` form survives validation; the named constant `ACP_SERVER_TAG_KEY` keeps the regex and the key colocated. Gates the LLM and Condenser nav items behind a `disabledByAcp` flag, greys them out with a tooltip, and redirects to `/settings/agent` in the settings loader (not a per-route useEffect, so there is no one- frame flash of the LLM page before bouncing). E2E validated against `ghcr.io/openhands/agent-server:fa29ae2-python`: - PATCH /api/settings with `agent_kind: "acp"` round-trips - POST /api/conversations with the adapter's ACP payload returns 201, `agent.kind=ACPAgent`, `acp_command` preserved, tags stamped. Closes #412 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(acp): wire onboarding ChooseAgent step into ACP settings Drops the "Support for other agents coming soon!" banner now that the support exists. Enables the Claude Code / Codex tiles and adds a Gemini CLI tile so the four options here match ``ACP_PROVIDERS`` from the Settings → Agent page. Selecting an ACP option and clicking Next persists ``agent_kind:"acp"`` plus the registry provider key (``acp_server``) via ``useSaveSettings``, mirroring the diff the Settings page emits. The advance only happens on save success — a failed PATCH stays on the step and surfaces a toast. Skips the embedded LLM-setup step (index 2) on both forward and back navigation when an ACP agent is active: the subprocess owns its own LLM and authenticates through Secrets, so the form has nothing to configure. OpenHands path is untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(acp): seamless Claude Code + Codex CLI auth via dev-docker Live-validated against `ghcr.io/openhands/agent-server:1.22.1-python` (the canvas's default pin, which now ships ACPAgent natively — no SHA override needed). Two changes surfaced by the run: 1. **Mount `~/.claude.json` in dev:docker.** Recent Claude Code CLI versions persist auth + workspace state in `~/.claude.json` next to (not inside) `~/.claude/`. Without this single-file mount, `@agentclientprotocol/claude-agent-acp` can't see the user's existing login and prompts to re-auth inside the sandbox. 2. **Use the new ACP package name in `ACP_PROVIDERS`.** Upstream renamed `@zed-industries/claude-code-acp` → `@agentclientprotocol/ claude-agent-acp`. The old name still works but emits an npm deprecation warning; the agent-server's own OpenAPI example uses the new name. Test fixtures pinning the legacy name updated to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(acp): import ACP_PROVIDERS from typescript-client Canvas was carrying its own copy of the ACP provider registry, which drifted out of sync with the canonical Python SDK source and ended up encoding an invalid Codex invocation (``@openai/codex acp`` — codex CLI has no ``acp`` subcommand, so the spawn deadlocked silently with ``Error: stdin is not a terminal`` and no log line). This change deletes ``src/constants/acp-providers.ts`` and imports the registry from ``@openhands/typescript-client`` instead, which now mirrors the Python SDK (see OpenHands/typescript-client#167). The TS SDK pin in ``package.json`` is bumped to the PR-branch SHA (``45a803c``) for now; once #167 merges and a new tagged release is cut, the pin can flip to the tag in a follow-up commit. Shape changes consumers needed to absorb: - ``ACPProviderConfig[]`` → ``Record<string, ACPProviderInfo>`` (lookup by key replaces ``.find``; ``Object.values`` where an array is needed) - ``display_name`` → ``displayName`` (camelCase matches TS conventions) - ``default_command`` → ``defaultCommand`` (and now ``readonly string[]``; components spread into a fresh array before passing to consumers that expect mutability) ``ACP_CUSTOM_PRESET_KEY`` is the only ACP-related constant that stays canvas-local — it's a synthetic sentinel for the "Custom" dropdown option, not a real provider, so it has no SDK counterpart. Moved to ``src/constants/acp-presets.ts``. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * revert(acp): keep ACP_PROVIDERS local to canvas Reverts the brief detour through `@openhands/typescript-client` for the ACP provider registry. Splitting the registry across two repos adds publish-coordination friction and doesn't actually eliminate the drift problem — it just moves it from "canvas vs. python-sdk" to "ts-sdk vs. python-sdk", with extra steps. Now: - `src/constants/acp-providers.ts` is the canvas-local copy again, with the corrected `codex` command (`@zed-industries/codex-acp`, the real ACP-protocol stdio server — not `@openai/codex acp`, which is the codex CLI's interactive mode and deadlocks the agent handshake when spawned without a TTY). - The package.json pin reverts to `v0.6.0` (the typescript-client release that does not include the unmerged `ACP_PROVIDERS` export from #167, which is now closed). - The split `acp-presets.ts` file is folded back in. Drift risk between this file and the Python SDK source is tracked in #587, with a longer-term plan to address it (TS-SDK mirror, code-gen, or runtime endpoint). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(acp): resolve empty acp_command from registry in adapter PR #416 ships the Settings → Agent page (and onboarding) with a "default preset" shortcut that stores ``acp_command: []`` and trusts the agent-server to resolve it from ``acp_server``. It doesn't. The agent-server's ``ACPAgent`` model has no ``acp_server`` field and no registry resolution — it just hands ``acp_command`` straight to a subprocess spawn. Empty list trips ``acp_agent.py:1013`` with ``IndexError: list index out of range``, the agent loop dies silently inside the agent-server's run thread, and the conversation hangs in ``idle`` with the user's message persisted but never answered. No error reaches the UI; from the user's perspective they sent a message and nothing happened. Caught while exercising the live ``dev:safe`` stack: a fresh conversation seeded from the onboarding "Claude Code" tile produced ``Failed to start ACP server: list / IndexError: list index out of range`` in the agent-server log. The fix is purely client-side — expand ``acp_command`` against ``ACP_PROVIDERS`` (canvas's local mirror of the Python SDK registry, see #587) before the payload leaves the adapter, when the user picked a built-in preset. ``acp_server: "custom"`` and any unknown key are left untouched — those genuinely depend on the user's explicit command, and silently inventing one would mask a real config bug. Three new adapter tests cover: - ``acp_command: []`` + ``acp_server: "claude-code"`` → command resolved to ``["npx","-y","@agentclientprotocol/claude-agent-acp"]`` - ``acp_command`` omitted entirely + ``acp_server: "codex"`` → same resolution path - ``acp_command: []`` + ``acp_server: "custom"`` → left untouched 2273 tests pass, lint + typecheck clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(acp): pass disabled state into SettingsDesktopSidebar When ACP is the active agent, ``useSettingsNavItems`` correctly tags the LLM and Condenser entries with ``disabled: true``. The mobile drawer (rendered via ``SettingsNavLink``) already respected that. The desktop sidebar (rendered via ``SidebarNavLink``, came in with the recent sidebar refactor) was constructing the link without forwarding the flag, so both items stayed fully clickable / styled as enabled while the conversation was running on an ACP subprocess. Two tiny changes: 1. ``SettingsDesktopSidebar`` passes ``renderedItem.disabled`` through to ``SidebarNavLink``. That alone gives the right visual state (``opacity-50``, ``pointer-events-none``) and keyboard behaviour (``tabIndex=-1`` + ``onClick preventDefault``) — both already implemented by ``SidebarNavLink``. 2. ``SidebarNavLink`` additionally sets ``aria-disabled="true"`` when ``disabled``, closing a screen-reader gap that existed independently of this regression (the link sounded actionable to assistive tech even though it wasn't). The ``clientLoader`` redirect in ``routes/settings.tsx`` continues to handle direct URL navigation to a disabled-by-ACP page, so even if someone bookmarks ``/settings/condenser`` and lands there while ACP is active, they get bounced to ``/settings/agent``. Two new tests in ``settings-navigation.test.tsx``: - Disabled-by-ACP items in the desktop sidebar carry ``aria-disabled``. - Enabled items don't. 2275 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(acp): address PR #416 review feedback Addresses both human and all-hands-bot review comments on #416: **Critical bugs fixed** - ``acp_args`` duplication on load (bot critical #1): the textarea is the single source of truth for the launch tokens, but save only wrote ``acp_command`` — any API-set ``acp_args`` survived and concatenated at spawn time. Save now always writes ``acp_args: []``. - ``tokenizeCommand`` corrupted quoted Custom commands (human bug #2): ``bash -c "echo hello"`` got split into ``["bash","-c","\"echo","hello\""]`` and silently misbehaved. New ``src/utils/acp-command.ts`` wraps ``shell-quote`` with selective re-quoting (so ``npx -y @org/pkg`` renders verbatim, not ``\@org/pkg``) and filters non-string entries (redirects, env-var refs) out of the parsed argv. Round-trip tests pin the contract. - Loader/component settings cache mismatch (bot critical #4): loader used ``SETTINGS_QUERY_KEYS.byScope("personal")``; ``useSettings`` used ``[...byScope("personal"), backend.id, orgId]``. They didn't share cache. Aligned + set ``staleTime: 0`` on the loader read so cross-tab kind flips are picked up immediately (the in-render hook keeps its 5-minute stale window). - ``getFirstAvailablePath`` ignored the new agent route (human bug #3): ``/settings/agent`` now precedes the others in the fallback list, so first-time / hide_llm_settings users land on the agent picker rather than ``/settings/app``. - ``ACP_SETTINGS_KEYS`` documentation (human #4): pre-empts the "why not trim this list to UI-visible fields" question by spelling out that it serves as both the ACP allow-list and the OpenHands deny-list — trimming would silently leak API-set ``acp_*`` state. **Refactor (human #2 + #3)** - ``description_key`` moves into ``ACP_PROVIDERS``; the onboarding ``AGENT_OPTIONS`` is now derived from the registry so adding a new provider only needs one edit. - One ``buildAcpAgentSettingsDiff`` helper replaces the two near-copies in ``choose-agent-step.tsx`` and ``agent-settings.tsx``; both call sites are now under a single contract for the agent_settings_diff shape. **UX (bot)** - Onboarding progress bar shows the actual visited-step count when the LLM step is skipped (3 segments for ACP, 4 for OpenHands). Previously segment 2 popped "completed" on a slide the user never visited. **Test coverage (bot)** - New ``__tests__/utils/acp-command.test.ts`` covers parseCommand / formatCommand round-trips, quoted args, embedded escapes, shell- operator filtering, package-style tokens. - Adapter: empty ``acp_model: ""``, unknown ``acp_server`` key, ACP→OH→ACP round trip (no field leakage either direction). - agent-settings: cleared input keeps Save disabled, whitespace-only same, full Custom command with quoted args round-trips through shell-quote. - choose-agent-step: provider switching (claude-code → codex) rebuilds the diff cleanly, no leak from the prior selection. **Acknowledged (no action)** - Bot critical #2 (supply chain drift) — same problem as the existing agent-canvas#587, already tracked. - Bot critical #3 (desktop sidebar disabled) — fixed in 27a3e79 a few commits before this review was written; review snapshot was stale. - Translation duplication (human #1) — matches the existing ``translation.json`` convention (every key has all 15 locales). - Option-bag → split functions (human #5) — cosmetic; defer. 2292 tests pass, lint + typecheck clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(acp): pre-bundle shell-quote so the Vite dev server can load it ``shell-quote`` is a CommonJS module that does ``module.exports = { parse, quote }``. The previous commit wired it into ``src/utils/ acp-command.ts`` with a named ESM import, which the dev server rejected on the first ``agent-settings.tsx`` load: SyntaxError: The requested module '/node_modules/shell-quote/ index.js?v=...' does not provide an export named 'parse' Switching to a namespace import (``import * as shellQuote from "shell-quote"; const { parse, quote } = shellQuote;``) makes the named-export check pass, but Vite then served the raw CJS file to the browser unchanged and the next request died with: ReferenceError: exports is not defined This second failure is because ``vite.config.ts`` sets ``optimizeDeps.noDiscovery: true`` — new dependencies must be listed in ``optimizeDeps.include`` or Vite won't run them through its CJS-to-ESM prebundler. Adding ``"shell-quote"`` there fixes it; the existing entry has a comment block explaining the same constraint for other deps. The Rollup-based prod build was unaffected. Verified: dev server boots clean, ``GET /settings/agent`` returns 200, no ``exports is not defined`` in the Vite client log, 9 unit tests in ``__tests__/utils/acp-command.test.ts`` pass on the Node test runner (vitest) where the CJS interop already worked. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(acp): address second-pass review on PR #416 Addresses the second all-hands-bot review's critical + improvements: **Critical: load path mis-merged acp_command + acp_args** Settings stored with the registry-default shortcut (``acp_command: []``, ``acp_server: "claude-code"``) plus a non-empty ``acp_args`` showed only the args in the textarea — no registry prefix. Saving then sent ``acp_command: ["--extra-arg"]`` and flipped the preset to ``custom``, silently losing the ``npx -y @agentclientprotocol/ claude-agent-acp`` prefix. The fix expands the registry default *before* concatenating with args, so the textarea always shows the full launch command and round-trips cleanly. **Improvement: formatCommand drops empty-string args** ``formatCommand(["bash", "-c", ""])`` rendered as ``"bash -c "`` which parsed back to ``["bash", "-c"]``, silently losing the empty slot. Now quotes empty tokens explicitly so they survive. **Improvement: desktop sidebar disabled tooltip parity** Mobile drawer's ``SettingsNavLink`` already showed "Disabled while {agentName} is active" on greyed-out items; the desktop ``SidebarNavLink`` had no explanation. Added a ``disabledReason`` prop (i18n-agnostic; the caller formats the string) and wrap with ``StyledTooltip`` when disabled-with-reason. ``SettingsDesktopSidebar`` now forwards the formatted message — same UX on both surfaces. **Test coverage gaps the bot flagged** - ``agent-settings``: new regression guard for ``acp_command:[]`` + non-empty ``acp_args`` load (would have caught the critical bug above). - ``acp-command``: empty-string round-trip case + explicit assertion; five more shell-operator filters (pipe, ``;``, ``&&``, ``||``, ``>>``). - ``settings-navigation``: desktop sidebar wraps disabled items in StyledTooltip when ``disabledReason`` is supplied; not when omitted. Plus a clean merge from ``origin/main`` (one-line import conflict in ``agent-server-adapter.ts``). 2350 tests pass, lint + typecheck clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(acp): address third-pass review on PR #416 - parseCommand: try/catch around shell-quote.parse so a malformed command in the textarea can't crash Settings → Agent mid-render - Rewrite shell-metasyntax tests to pin the *actual* shell-quote behaviour (it's a parser, not a security filter) — operators, globs, and comments are dropped; backticks / $VAR / $(...) survive as literal tokens but are NOT expanded at parse time - Add npm URLs + verification date (2026-05-19) to each ACP_PROVIDERS entry so future maintainers can re-check upstream packages - Document the silent preset-switch behaviour on detectPreset (the dropdown follows the textarea; the textarea is the source of truth) - Use the exported ACP_SERVER_TAG_KEY constant in the adapter test so a rename surfaces as a compile error rather than a runtime schema mismatch - Restore the canonical typescript-client lock entry (drop the git+ssh:// + SHA bump that crept in from a local npm install) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(acp): don't expose LLM-switch UI on ACP conversations The SDK's ACPAgent carries a sentinel ``llm`` (``acp-managed``) for cost-attribution only — the real model lives on the ACP subprocess via ``acp_model`` and isn't visible on ``agent.llm.model``. Without this fix, ``toAppConversation`` surfaced the sentinel as the conversation's ``llm_model``, and the chat header's SwitchProfileButton happily let users "change the model" while the running Claude-Code / Codex / Gemini subprocess kept its own. A confusing silent no-op. Two layers of defence so no future consumer has to re-derive the rule: 1. Boundary normalisation: ``toAppConversation`` reads the pydantic discriminator (``info.agent.kind === "ACPAgent"``), surfaces it as ``agent_kind: "acp" | "openhands"`` on AppConversation, and nulls ``llm_model`` for ACP. Mirrors OpenHands PR #14401. 2. UI gate: SwitchProfileButton returns null when ``conversation.agent_kind === "acp"``. The right control for ACP model switching is the ``acp_model`` field on Settings → Agent, not this picker. Tests cover both: a new ``toAppConversation`` case asserts ``agent_kind === "acp"`` + ``llm_model === null`` for an ``{kind: "ACPAgent"}`` payload, and a new SwitchProfileButton case asserts the button hides for an ``agent_kind: "acp"`` conversation even when profiles are present. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(acp): bridge Settings → Secrets into the ACP subprocess env The bare ``payload.secrets`` channel lands in the agent-server's ``secret_registry`` server-side, which the OpenHands ``Agent`` reads directly — but ``ACPAgent._start_acp_server`` builds its subprocess env from ``agent_context.secrets``, not from the registry. Without a bridge, a Settings → Secrets entry like ``ANTHROPIC_API_KEY`` is silently invisible to the ACP CLI (Claude Code, Codex, Gemini), so users hit "authentication failed" with no on-screen hint that their configured secret never reached the subprocess. Mirror the same LookupSecret map onto ``payload.agent.agent_context.secrets`` when ``acpMode === true``, so the agent-server's existing env-injection loop picks them up. The bare ``payload.secrets`` channel is also kept (it serves other consumers + remains the canonical "conversation secrets" wire). The mirroring fires only when there's something to bridge; non-ACP payloads are unchanged. This is a shim. Once canvas pins to an agent-server build that includes software-agent-sdk PR #3299 (which teaches ACPAgent to also read from ``state.secret_registry``), the ``if (acpMode)`` branch can be deleted with no behaviour change. Tests: - New: ACP payload mirrors customSecrets onto agent_context.secrets - New: empty customSecrets does NOT synthesize an empty bridge map - New: non-ACP payload does NOT get an agent_context.secrets bridge - All 42 adapter tests pass Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(acp): hide MCP nav + cloud LLM-model fallback while ACP is active Two ACP-leak fixes the review surfaced: MCP page reachable + editable under ACP - The SDK's ``ACPAgent`` rejects ``mcp_config`` on init (acp_agent.py:845) and the canvas adapter already strips it from start payloads, but the /mcp route and the Extensions nav still let users add / edit / delete MCP servers — silent no-ops against the running subprocess. - Add a ``clientLoader`` on /mcp that bounces to /settings/agent when ``agent_kind === "acp"``. Grey out the MCP item in ExtensionsNavigation with the same explanatory tooltip the LLM / Condenser items already use under ACP. - Extract the redirect into ``utils/acp-route-guard.redirectIfAcpActive`` so /settings and /mcp share one cache-key + redirect-target definition. settings.tsx's clientLoader now calls into it. Cloud chat ``ChatInputModel`` falls back to ``settings.llm_model`` for ACP - ``toAppConversation`` writes ``llm_model: null`` on ACP conversations (commit 8f0efe62), but ChatInputModel did ``conversation?.llm_model ?? settings?.llm_model``, resurrecting the user's default OpenHands model on a Claude-Code conversation and linking to /settings (which is itself ACP-disabled). Gate on ``conversation?.agent_kind === "acp"`` and return null instead. Tests: - New: ExtensionsNavigation greys MCP under ACP, leaves Skills + non-ACP clickable - New: /mcp clientLoader redirects under ACP, returns null otherwise + on settings-fetch errors (no redirect-loop) - New: ChatInputModel returns null for ACP even when settings has a model - All 18 affected tests pass Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(acp): preserve unknown acp_server on no-op saves + reviewer cleanups Fourth-pass review (PR #416 review comment 4486154133). Triage: Fixed: - **Unknown ``acp_server`` demoted to ``"custom"`` on save** (P1, real data corruption). A user with ``acp_server`` set out-of-band to a provider canvas's registry doesn't carry yet (e.g. a future provider, or one removed from the local mirror) would open Settings → Agent and lose the original key on the next Save — ``detectPreset`` routes every unknown server to ``ACP_CUSTOM_PRESET_KEY``. Now we capture the loaded ``acp_server`` + textarea at load time, and on save — when both are unchanged and the loaded key is non-empty, non-``"custom"``, and absent from ``ACP_PROVIDERS`` — pass it back verbatim via a new ``allowUnknownServer`` opt on ``buildAcpAgentSettingsDiff``. Editing the command still demotes to ``"custom"`` (user is configuring a new thing, so the preset name follows the command). - **Dead ``...existingContext`` spread** in the ACP secret bridge. ``createAgentFromSettings`` never populates ``agent_context`` on the ACP branch, so the spread always merged into ``{}``. Direct assignment — and a comment explaining why a deep-merge would be the wrong direction (ACPAgent only treats ``secrets`` as acp_compatible). - **Misleading ``$VAR`` test comment**. Reworded to lead with the no-leak contract (host env values must not end up in the persisted ``acp_command``) rather than the implementation-detail tangent. Documented but not changed: - **``acp_args: []`` "data loss" concern** — false alarm. Load merges ``acp_command + acp_args`` into the textarea before render; save persists the merged tokens as ``acp_command`` with ``acp_args: []``. Round-trip is correct. Added an inline comment on the load merge so the next reviewer doesn't re-flag the reset. - **Silent preset migration without user feedback** — by design. The dropdown re-derives from the textarea so it always reflects what will be saved; adding a toast on every keystroke would be noise. Already documented as intentional on ``detectPreset``. Tracked elsewhere: - Supply-chain drift / npm verification: agent-canvas#587. - Gemini onboarding icon: agent-canvas#621. Tests: - New: ``preserves an unknown loaded acp_server when the user saves without editing`` - New: ``demotes an unknown loaded acp_server to 'custom' when the user edits the command`` - All 73 affected tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(acp): stop silently corrupting argv, restore AgentContext, fix home-screen gating Three real review findings, all wired: 1. ``parseCommand`` silently dropped URL tokens with ``?`` query strings (and any other shell-glob metacharacter). Reproducer: node acp.js --endpoint https://example.com/acp?tenant=abc ``shell-quote.parse`` read ``?tenant=abc`` as a glob pattern and emitted a non-string AST node; the ``.filter(string)`` then dropped the URL entirely, persisting ``["node","acp.js","--endpoint"]``. Replaced ``shell-quote.parse`` with a small custom argv tokenizer that handles single/double quotes + backslash escapes and treats every other character — ``?``, ``*``, ``$``, ``|``, ``>``, ``#``, ``&``, ``;``, ``(``, ``)``, backticks — as literal. The agent-server passes the argv straight to ``subprocess.create_subprocess_exec`` anyway (no shell intermediary), so the literal-only model matches what actually happens at spawn time. ``shell-quote.quote`` is still used by ``formatCommand`` for output. 2. The ACP path skipped the ``agent_context`` block that the OpenHands path seeded with ``load_public_skills`` / ``load_user_skills`` / optional ``system_message_suffix``. All three are marked ``acp_compatible: true`` on the SDK ``AgentContext`` model — the ACP CLI renders them via ``ACPAgent._render_suffix`` — so ACP conversations were silently shipping a smaller system prompt than OpenHands ones. ``createAgentFromSettings`` now seeds the same block on both branches. The secret bridge below merges into that block (was overwriting it) so ``{ secrets }`` no longer wipes the skill flags. 3. ``ChatInputModel`` and ``SwitchProfileButton`` only checked ``conversation?.agent_kind``. On the home screen (and during the task-startup window) ``conversation`` is undefined, so the per-conversation check missed and both surfaces fell back to ``settings.llm_model`` / the LLM-profile picker — even when ``settings.agent_settings.agent_kind === "acp"`` made it clear the next-created conversation would be ACP. Added a settings fallback so both controls hide consistently with the rest of the ACP nav gating. Tests: - parseCommand: new "preserves URLs with query strings" + "preserves URLs with multiple query params" + "preserves shell metacharacters as literal argv tokens" cases; the old "filters operator" cases flipped to "preserves operator as literal". 17 parseCommand cases pass. - adapter: assertion on the ACP payload's ``agent_context`` updated to expect the skill flags instead of ``undefined``. - chat-input-model + switch-profile-button: new "hides on the home page when ACP is the default agent" cases. - 83 tests pass across the 5 affected files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(acp): two chat-rendering UX glitches on streaming ACP tool calls 1. Half-formed ACP tool-call cards flashed in the chat before the final state arrived. ACP servers stream multiple events per ``tool_call_id`` (status flips ``in_progress`` → ``completed`` / ``failed``); the intermediate events carry partial ``raw_input`` / ``raw_output`` / ``title``. The previous gate suppressed only ``in_progress`` and let ``null`` through (a "backwards compat" carve-out for older agent-server builds that no longer apply at our pinned version). Streaming intermediates often arrive without a status set yet, so they leaked. Tighten ``shouldRenderEvent`` to require ``status === "completed" || "failed"``. ``handleEventForUI`` already collapses by ``tool_call_id`` in place, so the terminal event lands at the original position once it arrives — no flash, no double-render. 2. "Reading Read /Users/foo/bar" — Claude Code emits titles like ``"Read /Users/foo/bar"`` for a read tool, and our i18n template ``"Reading <cmd>{{title}}</cmd>"`` then doubles up the verb. Add ``stripRedundantTitlePrefix`` keyed by ``tool_kind``: read → strip ``"Read"``, edit → strip ``"Edit"`` / ``"Write"``, execute → strip ``"Bash"`` / ``"Run"``, fetch → strip ``"Fetch"`` / ``"WebFetch"``. Boundary-checked via trailing whitespace so a token like ``"Reads-from"`` is left alone. English-only on purpose: ACP servers are anglophone and emit english titles regardless of the user's canvas locale; matching translated verbs would go stale the moment a new server is added. Titles already lacking a redundant prefix (the OpenHands ACP wrapper, future servers) round-trip verbatim — the strip is a no-op there. Tests: - ``shouldRenderEvent``: ``null`` status now flips to false + comment explains why (treated as in-flight, not legacy). - New ``stripRedundantTitlePrefix`` describe block covers the four tool kinds, the no-op case, the word-boundary guard, ``tool_kind: null`` (no strip), and empty titles. - 53 tests pass across the conversation-events helpers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(acp): drop React Router type import on /mcp clientLoader (CI build) CI's ``build:lib`` failed with: src/routes/mcp.tsx(3,23): error TS6059: File '.../.react-router/types/ src/routes/+types/mcp.ts' is not under 'rootDir' '/src'. ``tsconfig.lib.json`` sets ``rootDir: "src"`` and pulls in ``src/components/**/*.tsx``. ``src/components/settings/index.ts`` re-exports from ``routes/mcp-settings``, which imports ``routes/mcp`` — so the lib's typecheck graph reaches ``routes/mcp.tsx`` and trips on the generated ``./+types/mcp`` import that lives under ``.react-router/types/``, outside the lib's rootDir. (``routes/ settings.tsx`` uses the same import pattern but isn't reachable from the lib graph, which is why local typecheck passed.) Drop the type import and declare the loader with no parameters — matches the existing ``index-redirect`` and ``mcp-settings-redirect`` loader pattern. Test calls collapsed to ``clientLoader()`` to match the new signature. ``npm run build:lib`` now passes locally; 248 tests pass across the affected suites. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(acp): tighten adapter comments around SDK refs Two reviewer-flagged comment fixes: - ``ACP_SETTINGS_KEYS`` docblock no longer claims there's a matching ``ACP_SETTINGS_KEYS`` constant in the Python SDK (there isn't; the fields are model attributes on ``ACPAgentSettings``). Reworded to "Keep aligned with the ``acp_*`` fields on ``ACPAgentSettings`` in ``openhands-sdk/openhands/sdk/settings/model.py``" with an explicit "no matching SDK constant — hand-maintained" note, and cross-linked to the existing #587 drift tracker. - ``createAgentFromSettings`` now spells out where the ``acp_compatible`` markers live on each of the three fields we set (``system_message_suffix`` L66, ``load_user_skills`` L80, ``load_public_skills`` L89 in ``openhands-sdk/openhands/sdk/context/agent_context.py``) plus what happens when a future SDK bump drops one (422 at conversation start → drop the demoted field, don't wrap a workaround). Line refs are brittle by design — they're the tripwire that surfaces a regression here rather than in production. No behaviour change; lint + 42 adapter tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Debug Agent <debug@example.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1ee1de48cd |
fix(dev:docker): use localhost for AUTOMATION_AGENT_SERVER_URL, separate sandbox URL (#609)
The previous attempt (#603) collapsed the host-side and sandbox-side agent-server URLs onto a single value, `host.docker.internal:<hostPort>`, on the assumption that the host's /etc/hosts would alias it back to loopback. That holds on Docker Desktop macOS but NOT on OrbStack / colima / Linux hosts, where `host.docker.internal` only exists inside containers. On those hosts the automation backend (which runs on the host via uvx) fails to resolve the URL on its very first `_upload` call: [Errno 8] nodename nor servname provided, or not known so dispatch never reaches `_start_bash`, and every automation run ends up with `bash_command_id: NULL`. Fix: * Restore `AUTOMATION_AGENT_SERVER_URL` to `http://localhost:<hostPort>` in every mode. localhost on the host always resolves to the published agent-server port; this is the URL the *backend* uses for HTTP calls. * Add a new launcher option `sandboxAgentServerUrl` that is exported as `AUTOMATION_SANDBOX_AGENT_SERVER_URL`. The automation backend (OpenHands/automation#125) uses this to override the AGENT_SERVER_URL it exports into the in-sandbox bash chain. In dev:docker this is `http://127.0.0.1:8000` — the agent-server's in-container loopback, which avoids bouncing through the host port-forward. * Plumb `sandboxAgentServerUrl` through dev-with-automation.mjs::main (mirrors the existing `automationApiHost` / `automationWorkspaceBase` pattern) and set it in dev-docker.mjs. * Older automation backends that don't recognise AUTOMATION_SANDBOX_AGENT_SERVER_URL ignore it and fall back to AUTOMATION_AGENT_SERVER_URL, so this is forward-compatible. Dockerless modes (`dev`, `dev:automation`) leave `sandboxAgentServerUrl` undefined; the backend falls back to AUTOMATION_AGENT_SERVER_URL, which is correct for them. Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
2ac97d1bd8 |
fix(dev:docker): container-safe automation paths, hostnames, and AGENT_SERVER_URL alias (#603)
* fix(dev:docker): set AUTOMATION_WORKSPACE_BASE to a container-safe path
When agent-canvas is started via `npm run dev:docker`, the agent-server
runs in a container but the automation backend runs on the host via uvx.
The dispatcher resolves `AUTOMATION_WORKSPACE_BASE` on the host (where
it expands to the host's $HOME, e.g. /Users/<you>/.openhands/...) and
then embeds that absolute path into a `mkdir -p ...` shell command
that is executed *inside* the agent-server container. The container
has no /Users directory and the non-root user can't write to /, so
every preset automation fails with:
mkdir: cannot create directory '/Users': Permission denied
Fix:
* Thread a new `automationWorkspaceBase` option through the launcher
(mirroring the existing `viteWorkingDir` pattern).
* `dev-docker.mjs` now passes `CONTAINER_WORKSPACES_DIR` — the same
in-container path already used as the agent-server's working-dir
root, so it's guaranteed to exist and be writable.
* Resolution order for `AUTOMATION_WORKSPACE_BASE` is now:
1) explicit user env var (wins over everything; previously the
hard-coded value silently clobbered user-set values)
2) launcher-provided default (container-safe in docker mode)
3) host-side fallback under config.stateDir (unchanged for dockerless)
Co-authored-by: openhands <openhands@all-hands.dev>
* fix(dev:docker): route AUTOMATION_BASE_URL through host.docker.internal
In `npm run dev:docker`, the automation backend's `AUTOMATION_BASE_URL`
was hard-coded to `http://localhost:${ingressPort}`. That URL is then:
1. propagated into each automation sandbox as `AUTOMATION_API_URL`
(dispatcher.py:213), and
2. consumed by the auto-generated `setup.sh` inside the sandbox to
hit `${AUTOMATION_API_URL}/sdk-version`.
The sandbox is a separate Docker container, so `localhost` resolves to
the sandbox itself, not the host ingress. Every preset automation run
therefore failed at the very first step with:
[setup] ERROR: Failed to fetch SDK version from
http://localhost:8000/api/automation/sdk-version
Reachability check from inside a container in the same network:
http://localhost:8000/api/automation/sdk-version -> 404
http://host.docker.internal:8000/api/automation/sdk-version -> 200
Fix (mirrors the existing `automationWorkspaceBase` plumbing for the
same host-vs-container class of bug):
* New `automationApiHost` launcher option threaded through main() and
stamped onto config.
* `dev-docker.mjs` passes `automationApiHost: "host.docker.internal"`.
* `startAutomationBackend` resolves `AUTOMATION_BASE_URL` with
precedence: user env > launcher-provided host > `localhost`.
Dockerless modes (`dev`, `dev:automation`) are unaffected — they
don't pass `automationApiHost` and fall back to the existing
`http://localhost:${ingressPort}` value.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix(dev:docker): expose AGENT_SERVER_URL and SESSION_API_KEY aliases to the sandbox
The OpenHands SDK boilerplate emitted by automation prompt/plugin presets
reads AGENT_SERVER_URL and SESSION_API_KEY in main.py / setup.sh when
calling back into the agent-server from an automation run. The
agent-server itself sets OH_INTERNAL_SERVER_URL at startup and we set
OH_SESSION_API_KEYS_0 via the container/process env, but neither of the
unprefixed aliases the SDK actually reads were defined — so every preset
automation hit ECONNREFUSED / 401 the moment its setup.sh tried to hit
the agent-server.
Mirror the values under their canonical SDK names in both the dockerless
buildAgentServerEnv() (covers dev / dev:automation) and in dev-docker.mjs
containerEnv (covers dev:docker). Inside the dev:docker container the
agent-server always listens on port 8000, and 0.0.0.0 normalises to
127.0.0.1 (matching the agent-server's own OH_INTERNAL_SERVER_URL
construction), so the docker path uses http://127.0.0.1:8000 directly.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix(dev:docker): route AUTOMATION_AGENT_SERVER_URL via host.docker.internal
This env var plays a dual role: the automation backend uses it directly
to call the agent-server's upload/bash REST APIs (host-side), and the
same value is exported as AGENT_SERVER_URL into the in-sandbox bash
command that main.py uses to call back. In dev:docker the script runs
inside the agent-server container, so the URL has to be reachable from
both sides.
http://localhost:${agentServerPort} was fine for dockerless modes
(both backend and agent-server live on the host) but broke dev:docker:
from inside the container localhost:${hostPort} doesn't resolve, and
`main.py` failed at the first RemoteWorkspace call.
Reuse the same automationApiHost launcher option already plumbed for
AUTOMATION_BASE_URL (Bug 2): dev:docker passes host.docker.internal, so
the URL works as a loopback alias from the host and bounces back into
the container via the published port-forward from the sandbox side.
Dockerless modes keep the previous `localhost` default.
Co-authored-by: openhands <openhands@all-hands.dev>
* revert: drop SESSION_API_KEY alias from agent-server env
The OpenHands SDK's sanitized_env() strips SESSION_API_KEY from every
bash subprocess as a (cosmetic) defense-in-depth measure, so setting
this alias on the agent-server's process env had no effect on what the
sandbox script actually sees. Worse, it created cross-purposes between
the dev stack (which exports it) and the SDK (which strips it).
Keep the AGENT_SERVER_URL alias — that one is genuinely needed and
isn't subject to filtering. SESSION_API_KEY is now obtained inside the
sandbox script via OH_SESSION_API_KEYS_0 (handled in an accompanying
PR against openhands/automation), which the SDK does not strip.
Co-authored-by: openhands <openhands@all-hands.dev>
---------
Co-authored-by: openhands <openhands@all-hands.dev>
|
||
|
|
3300768da8 |
chore: remove unused assets, env var, and obsolete script (#564)
* chore: remove unused assets, env var, and obsolete script * chore: Remove PR-only artifacts --------- Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com> |
||
|
|
0cf58f8657 |
fix(dev-docker): allow exec on the home tmpfs so stdio MCP servers work (#535)
Docker's `--tmpfs` flag defaults its mount option set to
`rw,noexec,nosuid,nodev`. When dev:docker overlays the agent-server
container's `/home/openhands` with a tmpfs (so the mapped host user can
own it), it was inheriting that `noexec` flag unintentionally.
That breaks any stdio MCP server installed via npx -- e.g.
`npx -y @modelcontextprotocol/server-github` caches its binary under
`~/.npm/_npx/<hash>/node_modules/.bin/mcp-server-github`, npx then tries
to exec it, and the kernel returns EACCES regardless of the 0755 mode
bits on the file. The user sees:
sh: 1: mcp-server-github: Permission denied
Failed to connect to MCP server 'github', skipping
...
MCPError: MCP Connection Failure
and the conversation that triggered it aborts during agent init.
Pass `exec` explicitly to override only the `noexec` default. We keep
`nosuid` and `nodev` (the home dir has no business hosting setuid
binaries or device nodes) so we lose no defense-in-depth beyond what's
necessary to make the supported MCP integration actually function.
Co-authored-by: openhands <openhands@all-hands.dev>
|
||
|
|
5c42bd3492 |
Rename PROJECT_PATH to PROJECTS_PATH (#521)
* Rename PROJECT_PATH to PROJECTS_PATH Co-authored-by: openhands <openhands@all-hands.dev> * Apply suggestion from @enyst --------- Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
5c33c10b18 |
feat(frontend): add canvas_ui tool so the agent can drive the UI (#420)
* feat: add canvas_ui tool so the agent can drive the UI * refactor: update the code based on feedback * fix: failing tests * refactor: update the code based on feedback |
||
|
|
b2b71855c6 |
feat(dev): surface dev-stack runtime services in agent system prompt (#503)
* feat(dev): surface dev-stack runtime services in agent system prompt Add a structured 'runtime services' info object that the dev launchers (`dev:safe`, `dev:automation`, `dev:docker`, and the published `agent-canvas` binary) propagate to the frontend via `VITE_RUNTIME_SERVICES_INFO`. The frontend renders it into a `<RUNTIME_SERVICES>` markdown block and attaches it as `AgentContext.system_message_suffix` on every `POST /api/conversations`. This means agents start each conversation knowing exactly what services exist in the current dev stack (ingress URL, automation backend URL + `/api/automation` prefix, auth header, etc.), instead of having to probe or — worse — assume `localhost:8000` is the automation server when it is actually the Agent Server they are running inside of. URLs are written from the agent's point of view: dockerless modes use `localhost`, `dev:docker` uses `host.docker.internal`. When automation isn't running in the current mode (e.g. `dev:safe`), the block says so explicitly so agents know to skip `/api/automation` calls. Co-authored-by: openhands <openhands@all-hands.dev> * fix(runtime-services): address review feedback on PR #503 - Validate required `agentServerPort` in `buildRuntimeServicesInfo`; previously a missing port baked `http://localhost:undefined` into the agent's system prompt. - Skip the automation entry when the supplied `automation` object has no `port` (e.g. a bare `{}` from a misconfigured launcher). - Rename the JSON service key from `vite` to `frontend` and add a `kind: "vite" | "static"` discriminator + mode-aware description, so static-build dev stacks (`dev:docker`, the published binary, ...) no longer surface a misleading "Vite dev server" line in the agent system prompt. The renderer still accepts the legacy `vite` key. - Anchor the "don't guess" warning to the actual agent-server URL from runtime info instead of hardcoded `localhost:8000`, since the agent-server uses different ports across dev modes (18000 in dev:safe, 8000 in dev:docker, ...). - Plumb `frontendKind` through `buildAutomationRuntimeServicesInfo` and stamp `config.frontendKind` in `dev-with-automation.mjs::main` so both Vite spawn and static-build paths describe the frontend correctly. - Expand AGENTS.md with the JSON schema of `VITE_RUNTIME_SERVICES_INFO` and a concrete example of the rendered `<RUNTIME_SERVICES>` block. - Tests: assert the new URL-in-warning behavior, the new `frontend` / legacy `vite` rendering, the `agentServerPort`-required guard, the `automation: {}` skip, and the legacy `vitePort` alias. Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
4db59b8b94 |
Proxy agent-server FastAPI docs (/docs, /redoc, /openapi.json) through the ingress (#501)
* feat(ingress): route /docs to the agent server Add /docs to the list of prefixes proxied to the agent-server in: - vite.config.ts (Vite dev server proxy) - scripts/dev-with-automation.mjs (ingress + static-server fallback) - scripts/dev-static.mjs (ingress + static-server fallback) Update the explanatory comment in scripts/static-server.mjs to match. This exposes the agent-server's FastAPI Swagger UI at `/docs` on the ingress port, alongside the automation backend's existing `/api/automation/docs`. Co-authored-by: openhands <openhands@all-hands.dev> * feat(ingress): also route /redoc and /openapi.json to the agent server Without /openapi.json, the Swagger UI page served at /docs (added in the previous commit) renders but fails to load any spec. /redoc is the FastAPI-served ReDoc alternative and benefits from the same fix. Routes are added everywhere /docs already is: - vite.config.ts (Vite dev server proxy) - scripts/dev-with-automation.mjs (ingress + static-server fallback) - scripts/dev-static.mjs (ingress + static-server fallback) Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
2d5a52d4be |
chore: bump agent-server default to 1.22.1 (#477)
* chore: bump agent-server default to 1.22.1 * chore: update OH_AUTOMATION_VERSION * chore: update DEFAULT_AUTOMATION_SDK_VERSION |
||
|
|
e1f5a6662f |
design: cool-grey palette, runtime theme switcher, and token-system cleanup (#458)
* fix(conversation): cap chat column width at 800px Replace responsive max-w-4xl / max-w-6xl with max-w-[800px] so the middle column stays narrower on large viewports. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(chat): Connect Repo CTA and hide empty branch pill - Use COMMON$CONNECT_REPO with FolderOpen when no repo/workspace is linked - Show branch control only when selectedBranch is set (drop No Branch) - Cap chat interface wrapper at max-w-[800px] without right-panel width coupling Co-authored-by: Cursor <cursoragent@cursor.com> * fix(chat): refine input controls and local auth fallback Improve chat input pills and model dropdown interactions while ensuring local agent-server auth uses the configured session key for default-local and cloud-proxy calls to avoid stale-key 401s. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(chat): align attachment and placeholder control styling Move the file-attach trigger into the chat action controls so it sits before Tools, and restyle it as a grey plus button with a circular hover state to match adjacent controls. Also align the chat input placeholder color with the same neutral control tone for visual consistency. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(chat): simplify agent status labels and tone Shorten English agent-status messages for the chat pill and align the status text color with the other grey controls for a more consistent compact UI. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(chat): align model popover settings row styling Add an LLM Settings action to the model popover and normalize its layout, spacing, and divider treatment to match existing dropdown menu patterns while keeping left-aligned positioning. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(chat): restyle status controls and move send action Make the agent-status control transparent by default with gray-to-white icon hover behavior, and move the submit button to the bottom-right controls area beside agent status. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(chat): tighten spacing above git control bar Reduce the top margin before the git control bar so it better matches the bottom spacing around the chat action controls. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(chat): gate submit button on input content Keep the send button inactive until the input has non-whitespace text, and align the revised button sizing/positioning with the bottom action row layout. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(chat): streamline overlays and remove legacy event rails Unify chat control styling and overlay behavior so status/typing/scroll controls float above the thread without adding layout bars, and remove left-rail/checkmark affordances from grouped and generic event cards for a cleaner stream. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(chat): tighten status indicator spacing Reduce status indicator pill padding and icon size, and add right text padding to balance the compact layout in the chat control overlay. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(chat): prioritize centered scroll control over loader Keep the scroll-to-bottom control centered and visible whenever the user is away from the bottom, and use solid base/hover fills so it matches the updated chat surface styling. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(chat): soften conversation event header styling Use the lighter gray chat tone for conversation event header labels/icons and switch those labels to normal weight so grouped event rows match the updated control styling. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(chat): refine markdown spacing and divider styling Tighten markdown vertical rhythm in chat content, add a shared grey horizontal-rule renderer, and tune heading hierarchy to medium/compact styles for clearer structure without heavy emphasis. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(chat): tighten vertical spacing in action event rows Reduce stacked margins and paddings across grouped action rows, generic event cards, and collapsible thinking blocks so adjacent conversation entries read as a denser, more consistent stream. Co-authored-by: Cursor <cursoragent@cursor.com> * docs(design): add app gray palette reference artifacts Capture the current gray color usage in dedicated SVG references, including both a curated palette and a strict exhaustive inventory for design and UI consistency work. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(chat): align compact input overflow menus with menu conventions Keep add-file pinned inline, collapse controls only when width truly runs out, and switch overflow entries to standard context-menu row/submenu patterns while preserving the send button layout at tight widths. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(conversation-panel): use list filter icon for older filters Swap the older-conversations summary toggle icon to ListFilter so it matches the intended sidebar filter affordance. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(chat): complete local workspace launch flow in git controls Switch the local git control CTA from repository connection to workspace launching, including an above-button workspace menu and automatic add-workspace modal when none exist. This also captures the pending chat action/menu styling and test updates in the current working tree. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(chat): relocate desktop vertical padding to input controls Remove desktop top/bottom padding from the main chat panel and apply equivalent bottom spacing to the chat control area so the open repo/workspace controls and input footer keep consistent breathing room. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(conversation): remove bottom margin from chat pane header Drop the chat header bottom margin so the conversation title row sits flush with the content below. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(chat): refresh git control bar immediately after Connect Repo The "Connect Repo" empty-state in the chat input footer kept rendering even after the Open Repository modal had successfully launched a clone and the agent had reported the repository as ready. The bar would only heal after a hard refresh (or never, on cloud backends). Three independent bugs were stacking: 1. Optimistic update was writing to the wrong React Query cache key. `useUpdateConversationRepository.onMutate` called `setQueryData` with `["user", "conversation", id]` (3 elements), but `useUserConversation` reads from `["user", "conversation", id, backendId, orgId]` (5 elements). `setQueryData` requires an *exact* key match, so the update landed on an orphan cache entry that no observer ever read. Switched to `setQueriesData`/`getQueriesData` with the 3-element prefix so the optimistic write actually reaches the active query (Tanstack v5 prefix-matches `setQueriesData` filters). Also normalized `branch`/`gitProvider` to `null` to match the shape produced by the server-side refetch and prevent identity-flicker between the two updates. 2. Cloud `batchGetCloudConversations` / `searchCloudConversations` ignored the local repo selection entirely. For local backends `toAppConversation` overlays `selected_repository`/`selected_branch`/ `git_provider` from `localStorage`, but the cloud path returned the raw SaaS payload — and the SaaS often returns `null` for those fields until its own background hydration finishes. So every refetch (mutation invalidation, 30s poll, panel mount) overwrote the optimistic value with `null` and the bar snapped back to "Connect Repo". Added `overlayStoredRepoSelection` which fills only the `null` slots from local storage; populated server values still win, so we don't shadow real backend changes. 3. `updateConversationRepository` overwrote the entire metadata blob. `setStoredConversationMetadata` is replace-not-merge, so calling it with just `{selected_repository, selected_branch, git_provider}` silently dropped `selected_workspace` (the local-folder attach marker used by the Files tab to default to diff view, see the "Files tab diff-view default logic" note in `AGENTS.md`). Now reads the existing entry first and spreads it under the new repo fields. Defense-in-depth changes: - `useLocalGitInfo` now stays enabled until the conversation reports a *complete* repo tuple (`selected_repository` + `git_provider` + `selected_branch`), not just `selected_repository`. This lets the bar recover from partial-metadata cases (e.g. cloud hydration populates only the repo name first, or the user clones into a subdirectory of `working_dir`). The probe also gained a nested `find . -mindepth 2 -maxdepth 4 -name .git` fallback so a clone into `<workingDir>/<repo>/` is still detected after the direct `git remote get-url origin` in `<workingDir>` returns "no such remote 'origin'" (the agent-server pre-initialises every workspace as a worktree, so the parent directory always has a `.git` folder with no remote). - `useUpdateConversationRepository.onSettled` invalidates `["local-git-info", conversationId]` so the bar re-probes immediately after a connect rather than waiting on the next 10s refetch tick. - `git-control-bar.tsx`'s `hasRepository` predicate now keys off the *resolved* `selectedRepository` + `gitProvider` (which include the local-git probe's findings), not just the conversation field. This lets pull/push/PR buttons light up for local-workspace conversations whose repo metadata was inferred from `git remote`, matching what the repo + branch chips already showed. Verification I traced the failure mode by hitting the live agent-server directly: $ curl -s -X POST .../api/bash/execute_bash_command \\ -H "X-Session-API-Key: \$KEY" \\ -d '{"command":"git remote get-url origin", "cwd":"<workingDir>"}' git remote: error: No such remote 'origin' git rev-parse HEAD: ambiguous argument 'HEAD': unknown revision confirming the worktree-without-remote shape that broke the direct probe and forced the nested-find fallback. Tests __tests__/hooks/mutation/use-update-conversation-repository.test.tsx - optimistically updates the cached conversation under the prefix-extended key used by useUserConversation - rolls back the prefix-keyed cache entry when the mutation rejects __tests__/api/cloud-conversation-service.test.ts (new) - overlays locally-stored repo selection onto batchGetCloudConversations results when the server returns nulls - prefers the cloud server values over locally-stored selections when present - leaves null entries untouched when the cloud server returns null for a missing conversation - returns an empty array without calling the proxy when no ids are provided - overlays repo selection on each item returned from searchCloudConversations Wider sweep: npx vitest run __tests__/hooks/mutation \\ __tests__/api/cloud-conversation-service.test.ts \\ __tests__/api/conversation-metadata-store.test.ts \\ __tests__/api/agent-server-adapter.test.ts \\ __tests__/components/features/chat -> 22 files, 152 tests passed. User-visible behavior after this change: 1. Clicking Launch in the Connect Repo modal flips the bar to repo + branch chips immediately (optimistic update now reaches the active query). 2. The bar stays flipped through the next refetch on cloud backends (overlay keeps the local selection visible until the SaaS catches up). 3. Bar picks up nested clones within ~1s on local backends (local-git-info invalidation forces a re-probe instead of waiting on the 10s poll), and the nested-find fallback handles 'clone into <workingDir>/<repo>/' flows. 4. Pull/push/PR buttons now light up for local-workspace conversations whose remote was inferred from git remote. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(conversation): make right-panel drawer state session-only The right-side drawer's open/closed state (`isRightPanelShown` / `hasRightPanelToggled`) was persisted in localStorage, which made the panel feel sticky in a way users didn't expect — it would still be open after reloads or revisits even though they wanted a clean, focused chat view. Move drawer state fully into the in-memory Zustand store so it: - always starts closed on app load (or on opening a conversation after a restart), - survives in-app navigation because Zustand stays alive across React Router transitions, - only persists tab selection (`selectedTab`), which is the part users do want to come back to. The legacy `rightPanelShown` field is silently stripped from older persisted blobs by `sanitizeStoredState`, so old localStorage data doesn't churn or leak into the new schema. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(ui): standardize three-dots ellipsis trigger across the app Different surfaces had drifted to slightly different "more options" buttons: - conversation header used a 24x24 icon with a hardcoded fill color, - conversation cards in the side panel used a separate square `ellipsis.svg` glyph, - the conversation tab bar used a 20x20 icon with bespoke colors, - LLM profile rows wrapped the icon in a bordered button with yet another color. Promote `EllipsisButton` to be the canonical trigger and route every inline variant through it so size (w-4 h-4 / 16x16), color (`text-[#9299AA]`), and hover treatment (`hover:text-white hover:bg-white/10`) stay consistent everywhere. Layout-only overrides (e.g. translate, opacity-when-paused) flow through `className`, and a `testId` escape hatch keeps the existing `profile-menu-trigger` selector working. The chat-input overflow button intentionally keeps its pill-shaped custom variant; a doc comment on `EllipsisButton` calls that out so future contributors don't replace it. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(design): add 15-stop cool grey palette and complete migration plan Documents migration of ~98 scattered grey values across agent-canvas to a unified 15-stop cool blue-grey family (hue ≈ 220–224°), with all existing hex values, Tailwind utilities, CSS variables, and alpha variants mapped to the nearest new token by RGB + lightness proximity. Artifacts: - cool-grey-palette.svg: visual palette strip + per-shade migration map - cool-grey-migration.md: CSS/Tailwind definitions, per-file migration tables, alpha variant equivalents, and a 6-phase implementation plan Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): remove circular pill and ripple effect from autocomplete caret buttons Replace the default HeroUI selector button styling (rounded-full, fixed dimensions, hover fill) with a flat transparent icon and disable the press ripple via selectorButtonProps={{ disableRipple: true }} on all Autocomplete instances. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(design): enforce single border color token across all UI elements - Unify --oh-border-input to cool-grey-700 (same as --oh-border), eliminating the 3-way border split across inputs, cards, and dividers - Replace border-neutral-600 in .button-base with border-[var(--oh-border)] - Replace all border-tertiary usages (27 files) with --oh-border for outer borders and --oh-border-subtle for within-panel dividers - Fix border-tertiary-light on toggle switch OFF state → --oh-border - Fix border-t-tertiary on app-settings Git section divider → --oh-border-subtle - Fix divide-tertiary in profiles-body → divide-[var(--oh-border-subtle)] - Fix secrets table row dividers: --oh-border-subtle → --oh-border - Fix files-tab toolbar and file-quick-row header lines → --oh-border - Fix repo-connector and new-conversation card borders → --oh-border - Remove bg-surface from automations-list and automation-detail routes so they inherit bg-base from the root layout, matching all other pages Co-authored-by: Cursor <cursoragent@cursor.com> * fix(design): migrate automations to shared tokens; fix secondary button, card, and hover strokes - Replace all legacy tailwind.config.js color tokens in automations/ (27 files): bg-surface-card → bg-[var(--oh-surface)], bg-surface-elevated → bg-surface-raised, border-border → border-[var(--oh-border)], text-content-muted → text-muted, status/toggle/badge tokens → --oh-success/--oh-danger/--oh-muted variants - Fix active-status-badge and status-badge inactive fills from bg-border → bg-surface-raised - BrandButton secondary variant: yellow outline+text → border-[var(--oh-border)] text-white hover:bg-surface-raised; move hover:opacity-80 off base onto primary/tertiary only - BrandButton primary: replace text-base (font-size conflict) with text-[var(--oh-color-base)] so all variants share the base text-sm font size - Card primitive default/outlined themes: --oh-border-input → --oh-border (fixes visible mismatch between repo-connector and Start from Scratch cards on home screen) - marketplace-card, skill-card, skills-toolbar: replace hover:border-white/40 with hover:border-[var(--cool-grey-500)] and focus:ring-primary/60 with focus:ring-[var(--oh-border)] - automations routes: remove explicit bg-surface so pages inherit bg-base from root layout Co-authored-by: Cursor <cursoragent@cursor.com> * fix(design): normalize spinners, borders, and accent colors to design tokens Replace hardcoded `border-primary`, `border-blue-500`, and `text-primary` with cool-grey-aligned tokens (`border-white`, `border-white/20`, `var(--oh-border)`, `var(--oh-muted)`) across loading spinners, modals, dropdowns, and link styles. Switch dropdown selected-item highlight from `--oh-interactive-active` to `--oh-interactive-selected`. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(theme): add runtime color theme switcher with OpenHands-Neutral palette - Add src/themes/color-themes.ts: two themes (OpenHands-DeepSea / OpenHands-Neutral) with --cool-grey-* scale overrides and matching --heroui-* HSL channel overrides (default, content, background, foreground families) so HeroUI components and portalled popovers both respond to theme changes. - Inject overrides via a <style> tag on [data-agent-server-ui] AND [data-theme=dark] so portal content rendered to document.body picks up the new palette alongside inline components. - Add ThemeInput (SettingsDropdownInput) to Application Settings; applies the theme immediately on selection and persists to localStorage under openhands-color-theme. - Add ColorThemeApplier to root.tsx so the persisted theme is re-applied on every page load with no flash. Additional token fixes found during theme testing: - Define --color-tertiary-alt → --oh-text-dim in tailwind.css so the ~25 placeholder:text-tertiary-alt / text-tertiary-alt usages (API key input, helper text, badges) resolve correctly. - Fix environment-switch-overlay: replace bg-card / border-border / text-foreground with --oh-surface / --oh-border / --oh-foreground. - Fix AutocompleteSection headings in model-selector: add classNames={{ heading: "text-[var(--oh-muted)]" }} so Verified / Other Models labels are readable against the dropdown background. - Unify structural panel dividers: sidebar right edge + footer separator + files-tab tree divider all changed from --oh-border-subtle to --oh-border, matching the right panel. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): improve suggestion card hover to match secondary button style Co-authored-by: Cursor <cursoragent@cursor.com> * fix(theme): set OpenHands-Neutral as the default color theme Co-authored-by: Cursor <cursoragent@cursor.com> * fix: failing tests --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: hieptl <hieptl.developer@gmail.com> |
||
|
|
2899c7b383 |
Fix static automation auth and switch LLM tool (#474)
* Fix static automation auth and switch LLM tool * Allow automation SDK release lag in sync check * chore: update baseline snapshots [skip ci] * chore: trigger CI after snapshot update --------- Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
e5711e74b2 |
Clean up dev stack process trees on shutdown (#475)
Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
5b05321483 | Fix Windows static asset serving (#456) | ||
|
|
0fc85df818 |
Run Docker dev server with host user and tmpfs home (#443)
* Run Docker dev server as host user * Use tmpfs for Docker agent home * Document Docker tmpfs home rationale |
||
|
|
3f28f2d625 |
Fix static automation agent-server auth (#442)
Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
a4089e0e4a |
Default user launchers to static frontend (#434)
Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
5ccc8e627c |
Fail fast when frontend deps are missing (#404)
Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
12bd8171cb |
Fix automation agent-server auth (#410)
Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
2b40e3425e |
Clarify Docker permission errors in dev script (#403)
Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
8b7ed9de2f |
fix(vercel): force HTTPS for typescript-client git dep on Vercel (#391)
npm normalizes the `github:OpenHands/typescript-client#sha` shorthand (and even an explicit `git+https://github.com/...` URL) to `git+ssh://git@github.com/...` whenever it rewrites package-lock.json during a plain `npm install`. Vercel's build environment has no GitHub SSH key, so an ssh-pinned lockfile causes Vercel to fall back to a stale cached copy of the package whose dist/clients.js predates the addition of ConversationClient, FileClient, and SharedClient. Rolldown then fails the build with: [MISSING_EXPORT] ConversationClient is not exported by node_modules/@openhands/typescript-client/dist/clients.js PR #382 fixed this once by hand-editing the lockfile, but the very next local `npm install` (e.g. PR #387 bumping React Query hooks) silently rewrote the resolved URL back to ssh and the bug returned. This change makes the Vercel build self-healing: * package.json now pins the dep as an explicit `git+https://` URL so the intent is documented in one place. * package-lock.json's top-level dep spec matches that URL; the nested `node_modules/@openhands/typescript-client` entry already resolved to https, so this brings both halves of the lockfile in sync. * vercel.json sets `installCommand` to `bash scripts/vercel-install.sh`, which: - rewrites any leftover `git+ssh://git@github.com/` resolved URLs back to https (handles future regressions), - configures `git config --global url."https://github.com/".insteadOf` for both `ssh://git@github.com/` and `git@github.com:` (handles anything npm has already normalized in cache), - then runs `npm ci` for a strict, lockfile-driven install. Locally verified: * `bash scripts/vercel-install.sh` produces a clean install with the https-resolved typescript-client. * `npm run build` and `npm run lint` both succeed after the install. * Re-running `npm install` rewrites `resolved` back to `git+ssh` as expected — the install script normalizes it again on every Vercel build, so the lockfile drift no longer breaks deploys. Refs: #384 (Vercel preview build fails: MISSING_EXPORT for SharedClient / ConversationClient / FileClient). Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
4738f5b7c6 |
Add npm publish workflow and release infrastructure (#330)
* Add npm publish workflow and release infrastructure - Add .github/workflows/npm-publish.yml for automated npm publishing on GitHub releases - Update CI to verify library build (npm run build:lib) and package contents - Add CHANGELOG.md for version history tracking - Update README.md with npm installation and usage documentation Closes #197 Co-authored-by: openhands <openhands@all-hands.dev> * correct package version * chore: update npm-publish workflow for trusted publishing - Remove NODE_AUTH_TOKEN secret dependency - Keep id-token: write permission for OIDC - Add provenance flag for npm attestations - Add comment explaining trusted publisher setup on npmjs.com Co-authored-by: openhands <openhands@all-hands.dev> * feat: add CLI entry point for npx execution - Add bin/agent-canvas.mjs as executable CLI - Add bin field to package.json for npm bin linking - Include bin/ and build/ directories in published files - CLI serves the built application with SPA routing support - Supports --port, --host, and --help options Co-authored-by: openhands <openhands@all-hands.dev> * refactor: consolidate npm executable to use dev-docker infrastructure - bin/agent-canvas.mjs now uses dev-with-automation.mjs main() with dev-docker.mjs's Docker-specific agent-server starter - Added --static and --static-dir support to dev-with-automation.mjs so the npm executable serves pre-built static assets instead of Vite - Added startStaticFrontend() function that uses static-server.mjs - npm executable runs full stack: Docker agent-server + uvx automation backend + static frontend + ingress proxy Co-authored-by: openhands <openhands@all-hands.dev> * fix: include scripts/ in npm package files The bin/agent-canvas.mjs executable imports from scripts/dev-with-automation.mjs and scripts/dev-docker.mjs, so the scripts directory must be included in the published package. Co-authored-by: openhands <openhands@all-hands.dev> * fix: address review comments - Fix CHANGELOG.md version mismatch: 1.6.0 -> 1.0.0-alpha.1 to match package.json - Add NODE_AUTH_TOKEN env var to npm-publish workflow for authentication - Add CLI entry point mention to CHANGELOG Co-authored-by: openhands <openhands@all-hands.dev> * fix: use OIDC trusted publishing (no NPM_TOKEN needed) npm trusted publishing with OIDC doesn't require NODE_AUTH_TOKEN. Instead it uses short-lived OIDC tokens generated by GitHub Actions. Requirements: - id-token: write permission (already set) - npm CLI 11.5.1+ (added npm install -g npm@latest step) - Trusted publisher configured on npmjs.com See: https://docs.npmjs.com/trusted-publishers/ Co-authored-by: openhands <openhands@all-hands.dev> * chore: bump version to 1.0.0-alpha.2 Co-authored-by: openhands <openhands@all-hands.dev> * Build app assets before npm publish Co-authored-by: openhands <openhands@all-hands.dev> * fix: use Node 24 for npm trusted publishing Trusted publishing requires Node 22.14.0+ and npm 11.5.1+. Node 24 ships with npm 11.x which meets the requirement. Node 22.12.0 (previous) ships with npm 10.x which doesn't support OIDC. Also removed the manual npm upgrade step since Node 24 includes a compatible npm version by default. Co-authored-by: openhands <openhands@all-hands.dev> * chore: align all workflows to Node 24 and regenerate lockfile - Update ci.yml to use Node 24 - Update sdk-version-sync.yml to use Node 24 - Regenerate package-lock.json with npm 11.12.1 All workflows now use Node 24 which ships with npm 11.x, required for OIDC trusted publishing (npm 11.5.1+). Co-authored-by: openhands <openhands@all-hands.dev> * fix: remove incorrect LLM env vars from CLI help LLM_MODEL and LLM_API_KEY were listed in the help text but aren't actually used by the scripts. LLM settings are configured through the web UI settings page instead. Co-authored-by: openhands <openhands@all-hands.dev> * fix: address PR review feedback Critical fixes: - Guard prepare script to only run in dev context (check for ../.git) - Add missing existsSync import in dev-with-automation.mjs Workflow improvements: - Update checkout/setup-node actions to v6 for consistency - Add npm version validation (must be 11.5.1+ for trusted publishing) - Add package version validation (must match release tag) CLI improvements: - Add try-catch for dynamic imports with helpful error message - Use console.error directly instead of imported logError/c Documentation: - Fix README export names: ChatInterface→ChatPanel, Terminal→TerminalPanel - Add dist/ to .gitignore Co-authored-by: openhands <openhands@all-hands.dev> * ci: trigger npm publish on tag push instead of release Simpler workflow - just push a tag like v1.0.0-alpha.2 to publish. Co-authored-by: openhands <openhands@all-hands.dev> * chore: remove tarball and add *.tgz to gitignore Co-authored-by: openhands <openhands@all-hands.dev> * fix: npm publish errors 1. Fix bin path - remove './' prefix (npm pkg fix) 2. Add --tag for prerelease versions (alpha/beta/rc) Co-authored-by: openhands <openhands@all-hands.dev> * fix: add repository field for npm provenance verification npm provenance requires repository.url to match the GitHub Actions source. Also added description, homepage, and bugs fields. Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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 |
||
|
|
c8c08acd67 |
chore(dev:docker): bump default agent-server tag to 0924962-python (#289)
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> |
||
|
|
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>
|
||
|
|
137fbae87f | fix: pass container workspaces path as VITE_WORKING_DIR (#259) | ||
|
|
fc0ab39809 |
Restore "Mount dev:docker project path at /projects and auto-list it as a work…" (#243)
* Revert "Revert "Mount dev:docker project path at /projects and auto-list it a…" This reverts commit c70ac51417733d8c053b9581a678a743f738781c. * Apply suggestion from @rbren * Apply suggestion from @rbren * Fix mangled workspace parent restore --------- Co-authored-by: openhands <openhands@all-hands.dev> |
||
|
|
fef07e52ee |
Revert "Mount dev:docker project path at /projects and auto-list it as a work…" (#242)
This reverts commit fc7efa9202dd01b603ef96de47fcbadf69882517. |