Commit Graph
7377 Commits
Author SHA1 Message Date
Rohit Malhotraandopenhands 43e6db6919 docs: add API access rules to AGENTS.md and code review skill (#506)
Document the two mandatory API access conventions that are enforced by
the CI test src/api/no-direct-agent-server-calls.test.ts:

1. All agent-server calls must use typed @openhands/typescript-client
   classes (ConversationClient, FileClient, VSCodeClient, ServerClient,
   RemoteWorkspace, RemoteEventsList) instantiated via
   getAgentServerClientOptions() -- never raw axios/fetch.

2. All cloud SaaS and runtime-sandbox calls must go through
   callCloudProxy() in src/api/cloud/proxy.ts to avoid CORS, using
   hostOverride for runtime-sandbox URLs and authMode='session-api-key'
   for those endpoints.

AGENTS.md gets a full '## API Access Rules' section with client
listings, option helper references, CORRECT/WRONG code examples, and
the allowed-exceptions list.

The custom-codereview-guide.md skill gets a '## Frontend API Access
Conventions' section with DO NOT APPROVE triggers, forbidden pattern
lists, correct examples, and a note about the silent hostOverride bug.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-16 04:30:01 +00:00
Rohit Malhotraandopenhands acb04fd9ba perf(snapshots): skip retries on comparison pass, suppress consent modal (#505)
- Add --retries=0 to test:e2e:snapshots: snapshot pixel-diff failures are
  deterministic — retrying cannot fix them. On a PR that changes 36/60
  snapshots this tripled execution count and added ~2 min to the comparison
  pass.

- Extract seedLocalStorage() helper (tests/e2e/snapshots/support/) that
  seeds openhands-onboarded and openhands-telemetry-consent in a single
  addInitScript call. All 13 snapshot specs now use it instead of
  duplicated inline addInitScript blocks.

- Pre-seeding openhands-telemetry-consent='denied' eliminates the race
  condition where changes-tab timed out at 60 s (x3 retries = 3 min) because
  dismissConsentModal fired before the modal rendered with domcontentloaded.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-16 00:04:29 -04:00
14d2e9454b test(snapshot): changes tab diff viewer + backend management UI (6 tests) (#450)
* test(snapshot): changes tab diff viewer + backend management UI (6 tests)

Pre-seed MOCK_GIT_CHANGES with M/A/D entries (using AgentServerGitChangeStatus
values: UPDATED/ADDED/DELETED) so changes-tab tests can exercise the file list,
Monaco diff viewer, and deleted-file placeholder without per-test MSW manipulation.

Expose window.__setMockGitChanges__ so the empty-state test can clear the list
after boot and trigger a React Query refetch via __TEST_INVALIDATE_QUERIES__,
avoiding a full page reload that would reinitialise module state.

Backend management tests exercise the selector dropdown, add-backend modal, and
manage-backends modal — all driven by localStorage seeding via addInitScript.

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

* chore: update baseline snapshots [skip ci]

* ci: trigger re-run against CI-generated baselines

* fix(snapshot-tests): mask Monaco editor for stable CI screenshots; fix unit test

- changes-tab spec: mask data-testid=editor-container so Monaco's sub-pixel
  font hinting (which varies per OS) doesn't cause false pixel-diff failures
- mock-conversation-handlers test: update assertion to match the new pre-seeded
  MOCK_GIT_CHANGES (3 M/A/D entries) instead of the previous empty array

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

* chore: update baseline snapshots [skip ci]

* ci: trigger re-run against CI-regenerated baselines (Monaco mask + unit test fix)

* fix(snapshot-tests): normalize RandomTip height via addStyleTag for stable empty-state screenshot

RandomTip renders a randomly-chosen tip whose line-count varies, causing the
flex-1 container above it to have different heights across runs. Fix by injecting
a CSS rule via page.addStyleTag() that pins .text-m.bg-tertiary.p-4 to 80px
(visibility:hidden so the variable text is invisible) — layout is now deterministic.
Switch back to screenshotting the full files-tab panel since dimensions are stable.

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

* chore: update baseline snapshots [skip ci]

* ci: trigger re-run against baselines (empty-state RandomTip height fix)

* fix(snapshot-tests): use inner content div for empty-state screenshot to avoid left-strip artefact

Screenshot files-tab's last direct div child (the flex-1 content wrapper)
instead of the outer main element.  During CI baseline generation the outer
main's bounding box occasionally captured a ~30px left-panel overlay artefact
that made the baseline permanently diverge from subsequent verification runs.
Targeting the inner wrapper excludes the outer-element overflow while still
showing the full empty-state (icon + 'no changes yet' text + hidden tip area).

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

* chore: update baseline snapshots [skip ci]

* ci: validate against fresh inner-div empty-state baseline

* test(snapshot): extended backend UI flows — 12 tests, 19 screenshots

Add backends-extended.snapshot.spec.ts covering 8 behaviour flows
with iterative screenshot captures at each state transition:

Flow 1a  Blank add form — Save disabled until name+host filled
Flow 1b  Local backend — Save enabled with name+host, no API key needed
Flow 1c  Cloud backend — Save disabled without API key, enabled with it
Flow 2a  Host auto-infers Local kind; OAuth section disappears
Flow 2b  Cloud-domain URL keeps Cloud kind; OAuth section shows
Flow 2c  Manual kind selection locks type (touchedKind=true) even when
         a cloud URL is later typed into the Host field
Flow 3   OAuth Login button disabled while host is empty; enabled once filled
Flow 4   Remove backend: shows ConfirmationModal → Cancel keeps row →
         Confirm removes it from the list (4 screenshots)
Flow 5   Edit modal pre-populates name/host/key from stored backend
Flow 6   Switch active backend: environment-switch overlay captured via
         page-level screenshot + animation override so the card is
         opaque at frame-0; after-switch state verified via selector label
Flow 7   Whitespace-only host keeps Save disabled; syntactically invalid
         URL is accepted by the frontend (no URL-format validation)
Flow 8   Cancel add form: dismisses modal, Manage Backends confirms no
         phantom entry was saved

Notable decisions:
- Uses body[data-environment-switching="true"] as the early DOM signal
  before React paints the portal div for the switch overlay
- Adds inline style-tag override before the overlay screenshot because
  .environment-switch-overlay > div has opacity:0 at animation frame 0;
  Playwright's animations:"disabled" pauses there, making the card
  invisible without the override
- Backends seeded via page.addInitScript localStorage injection so
  tests are fully self-contained with no MSW state dependency

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

* chore: update baseline snapshots [skip ci]

* ci: validate extended backend snapshot tests against CI baselines

* ci: always post snapshot PR comment even when test generation step fails

The 'Post snapshot report to PR' step was skipped whenever 'Generate
current PR snapshots' exited non-zero (e.g. a test crash like a hidden
element, not just a snapshot diff). GitHub Actions skips steps without
an always() guard when a prior step fails.

Add always() so the comment is posted regardless — showing diffs or
the test failure output — which was the intended behaviour.

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

* ci: fix snapshot comment - remove tracked screenshots, add crash reporting

Three fixes:

1. Remove 28 git-tracked snapshot PNGs from this branch.
   These were committed by the old baseline-in-git workflow before #482
   migrated to artifact storage. Because they stayed tracked (gitignore
   doesn't untrack already-indexed files), every CI checkout put them in
   tests/e2e/__snapshots__/ BEFORE the baseline artifact was downloaded.
   The Save step then copied them into /tmp/main-baselines, making the
   new tests appear as 'Unchanged' instead of 'New' in the PR comment.

2. Add 'Clear snapshot directory before downloading baselines' step.
   Wipes tests/e2e/__snapshots__/ before the artifact download so any
   future accidentally-tracked files can never contaminate the baseline.

3. Surface test crashes in the PR comment.
   - Generate step gets continue-on-error + an id so subsequent steps
     can read its outcome.
   - GENERATE_OUTCOME is passed to the comment script.
   - If outcome == 'failure', a GitHub-flavoured WARNING callout is
     prepended to the comment with a direct link to the CI run logs.
   - A dedicated 'Fail if snapshot generation had test crashes' step
     restores the job failure that continue-on-error absorbed.

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

* ci: use PR number in snapshot concurrency group for cleaner cancellation

The previous group used github.ref which resolves to refs/pull/{N}/merge
for PR events — correct but opaque. Using github.event.pull_request.number
makes the grouping explicit and human-readable (snapshot-tests-450), and
falls back to github.ref for main pushes and workflow_dispatch.

cancel-in-progress: true was already set, so new commits already cancelled
prior runs. This just makes the intent clearer.

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

* fix: syntax error in post-snapshot-comment.mjs (] vs ) in lines.push)

lines.push(...) was accidentally closed with ]; instead of ); after
splitting the original lines = [...] array literal into a push call.
Caused a SyntaxError at startup, preventing any comment from being posted.

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

* fix: snapshot test disabled states, changes-tab crash, and CI false-failures

Three fixes:

1. BrandButton disabled visual styling (brand-button.tsx)
   disabled:opacity-30 pseudo-class was not applying in Vite dev mode
   (Tailwind v4 + postcss-prefix-selector interaction), making disabled
   and enabled buttons visually identical in snapshot screenshots.
   Fix: add isDisabled conditional class directly ('opacity-30
   cursor-not-allowed pointer-events-none') so the disabled appearance
   is applied regardless of whether :disabled pseudo-class works.

2. changes-tab test crash (changes-tab.snapshot.spec.ts)
   Test waited for data-testid='files-tab' but the right panel always
   starts CLOSED (isRightPanelShown = false is session-only Zustand
   state; sanitizeStoredState strips any persisted rightPanelShown key).
   Fix: click data-testid='right-panel-toggle' after navigation to open
   the panel before waiting for files-tab. Also remove the no-op
   rightPanelShown: true from the localStorage seed.

3. CI false-failures for new snapshot tests (snapshot-tests.yml +
   post-snapshot-comment.mjs)
   The 'Fail if comparison found differences' step fired on
   'missing baseline' failures (expected for new tests in a PR) as
   well as actual pixel-diff failures.
   Fix:
   - post-snapshot-comment.mjs outputs has_changes=true/false to
     GITHUB_OUTPUT (true only when changed.length > 0, i.e. real diffs)
   - 'Fail if' step now checks steps.post-comment.outputs.has_changes
     == 'true' instead of compare.outcome == 'failure', so PRs that
     only add new snapshot tests pass CI cleanly.

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

* fix: reject invalid host URLs in backend form; use http for local addresses

Two related fixes to backend host validation / normalisation:

1. isValidHostUrl() — reject invalid host strings
   canSubmit previously only checked host.trim().length > 0, so
   garbage like 'not://:::a valid url!!!' passed through and enabled
   the Save button. isValidHostUrl() adds two checks before the URL
   constructor: (a) the trimmed value must be non-empty, (b) it must
   contain no whitespace. This catches the test-case input whose spaces
   are the tell-tale sign of a malformed value.

2. normalizeHost() — http:// for local addresses
   Bare hostnames (no explicit scheme) were unconditionally prepended
   with https://, but local servers almost never have TLS certificates.
   The new isLocalAddress() helper detects localhost, 127.x, RFC-1918
   private ranges (10.x, 192.168.x, 172.16-31.x), .local / mDNS names,
   and single-label hostnames — all get http:// instead of https://.
   Hostnames with dots that are not in those ranges (e.g. app.all-hands.dev)
   still default to https://. Explicit http:// or https:// prefixes are
   always preserved as-is.

Test update: the 'backend-add-invalid-url-accepted' snapshot is renamed
to 'backend-add-invalid-url-disabled' and the assertion flips from
not.toBeDisabled() → toBeDisabled(), reflecting the new behaviour.

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

* feat: inline error feedback on Name and Host fields in BackendForm

Three parts:

1. SettingsInput gains error / showRequiredTag / onBlur props
   - error?: string — red border on the input plus a small red alert
     paragraph below it (role=alert, data-testid=${testId}-error, linked
     via aria-describedby).
   - showRequiredTag?: boolean — renders a red * after the label to
     signal that the field is mandatory, consistent with OptionalTag.
   - onBlur?: () => void — forwarded directly to the <input>.
   - aria-invalid is set automatically when error is truthy.

2. BackendForm wires touched state → errors → inputs
   - nameTouched / hostTouched (both false on open, set on blur)
   - nameError: 'Name is required' when touched + empty
   - hostError: 'Host is required' when touched + blank/whitespace;
                'Enter a valid URL (e.g. http://localhost:8080)' when
                touched + non-empty but fails isValidHostUrl()
   - Both name and host SettingsInputs get showRequiredTag, the
     computed error, and onBlur={() => setXTouched(true)}.
   Errors are intentionally suppressed until blur so the form does not
   scold the user before they have had a chance to type anything.

3. Three snapshot tests call .blur() after .fill() to reveal errors
   - backend-add-name-only-disabled: focus+blur empty host → 'Host is
     required' appears below the Host field.
   - backend-add-whitespace-host-disabled: blur after fill('   ') →
     same 'Host is required' (whitespace counts as empty).
   - backend-add-invalid-url-disabled: blur after invalid URL fill →
     'Enter a valid URL...' appears below the Host field.
   The backend-add-blank-disabled snapshot is unchanged (neither field
   touched, no errors yet — correct for the fresh-open state).

New i18n keys: BACKEND$NAME_REQUIRED, BACKEND$HOST_REQUIRED,
BACKEND$HOST_INVALID (English only; other locales fall back to en).

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

* fix: prettier formatting on nameError / hostError ternaries

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

* fix: disable OAuth Login button until name and host are both valid

Previously the 'Login with OpenHands' button was enabled as soon as
a non-empty host was typed, even when the Name field was still blank.
This let users go through the full OAuth device-flow only to find they
still couldn't save because the name was missing.

Gate isDisabled on !name.trim() || !isValidHostUrl(host) so the button
stays disabled until the form is actually ready to save (modulo the
API key that OAuth itself will provide).

Update Flow 3 snapshot test to fill the name before asserting the
button becomes enabled, and update the test description accordingly.

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

* chore: address PR review feedback (#450)

IPv6 parsing fixes (normalizeHost / isLocalAddress):
- normalizeHost: handle bracket notation [::1]:8080 (extract ::1),
  bare IPv6 addresses with multiple colons (use whole string as
  hostname), and regular host:port as before — prevents split(':')[0]
  from grabbing only the first segment of a multi-colon IPv6 address
- isLocalAddress: strip brackets before comparison; add :: (any-addr),
  ::ffff:127.x.x.x (IPv4-mapped loopback), fe80::/10 (link-local),
  fc00::/7 (unique local); tighten single-label check to exclude
  addresses that contain colons (bare IPv6 non-local addresses)

Mark fields touched on submit attempt:
- handleSubmit sets nameTouched + hostTouched when !canSubmit so
  inline errors appear for keyboard users who press Enter on an
  incomplete form

Snapshot workflow comparison-crash detection:
- Pass COMPARE_OUTCOME=${{ steps.compare.outcome }} to post-comment
- post-snapshot-comment.mjs reads COMPARE_OUTCOME and prepends a
  '[!WARNING]' block when the comparison step itself crashed
  (timeout/OOM) so the comment accurately reflects the run state
  instead of silently showing an incomplete/empty diff table

Remove unnecessary serial mode from backends-extended snapshot suite:
- Each test calls setupPage() with fresh state on its own Playwright
  page; no shared mutable state exists between tests, so serial is
  unnecessary and slows the suite

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

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-15 23:14:30 -04:00
Tim O'Farrellandopenhands 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>
2026-05-15 20:53:45 -06:00
Tim O'Farrellandopenhands 8db469487b docs(readme): mention OpenAPI docs alongside UI URL (#502)
Both the agent server's FastAPI docs and the automation backend's docs
are reachable through the local ingress proxy on port 8000, so point
users at them right next to the existing UI URL in both the Docker and
non-Docker quickstart sections.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-15 19:16:55 -06:00
Tim O'Farrellandopenhands 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>
2026-05-15 18:46:13 -06:00
Rohit Malhotraandopenhands 9203a72d64 feat(snapshot-ci): group PR comment snapshots by spec file (#499)
Previously every changed/new snapshot got its own ### heading and table,
making it hard to see which snapshots belong to the same feature or flow
(e.g. the four-step MCP Slack install flow, the five-step secrets
lifecycle, or the skills search/filter sequence).

Group all three sections (🔴 Changed, 🆕 New, ✅ Unchanged) by spec file:

- Replaced formatRelPath() with specFromRelPath() + groupBySpec() helpers.
- Changed / New: one ### heading per spec (with count when > 1 snapshot),
  then **bold name** + the 3-column expected|actual|diff table per snapshot.
- Unchanged: compact grouped bullet list — bold spec heading, then one
  bullet per snapshot name, no superfluous intro sentence.

No workflow changes required; the grouping is purely in the comment script.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-15 22:45:14 +00:00
Rohit Malhotraandopenhands 63704f65e5 test: rename sidebar nav label "New" → "Chats" to verify snapshot CI diff comment (#497)
* test: rename sidebar nav label New → Chats to trigger snapshot diff

Intentional one-line change to verify that the snapshot CI workflow
correctly posts a PR comment showing the expected/actual/diff images.

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

* fix(snapshot-ci): save comparison test-results before update step clears them

Root cause: Playwright wipes its output directory (test-results/) at the
start of each new run.  The workflow runs the tests twice:
  1. npm run test:e2e:snapshots        → comparison, writes *-diff.png files
  2. npm run test:e2e:snapshots:update → regenerates baselines, clears
                                         test-results/ first, no diffs written

By the time post-snapshot-comment.mjs runs, all diff files are gone.
diffBySnapshotName is always empty, so every snapshot is classified as
"unchanged" even when Playwright reported 16 failures.

Fix:
- Add a "Save comparison test-results" step immediately after the
  comparison run that copies test-results/ to /tmp/comparison-results
  before the update pass can delete them.
- Pass COMPARISON_RESULTS_DIR=/tmp/comparison-results to the comment script.
- In post-snapshot-comment.mjs, read TEST_RESULTS_DIR from
  COMPARISON_RESULTS_DIR env var (falls back to "test-results" for local use).

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

* docs: document snapshot CI comparison-results ordering in AGENTS.md

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

* revert: restore sidebar nav label to New

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

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-15 18:34:00 -04:00
Rohit Malhotraandopenhands c3de18580d ci: increase Playwright CI workers from 1 to 2 (#495)
ubuntu-24.04 runners have 2 vCPUs. Each Playwright worker runs in its own
browser context so tests are fully isolated (MSW state, localStorage, and
React Query cache are all page-level). Doubling from 1→2 workers should
roughly halve wall-clock test time on CI with no risk of interference.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-15 16:53:27 -04:00
Rohit Malhotraandopenhands d886a68a43 fix: prevent snapshot CI runs from sending PostHog analytics events (#490)
* fix: separate PostHog keys for production and dev builds

The telemetry service (canvas_install events) was using a single
hardcoded PostHog key as a fallback in every build, so CI snapshot
tests sent events to the same project as real users, inflating the
unique-persons count and making install metrics unreliable.

Changes:

src/services/telemetry.ts
- POSTHOG_API_KEY is now string | null keyed off import.meta.env.PROD:
  - Production: VITE_POSTHOG_API_KEY || 'phc_REPLACE_WITH_PRODUCTION_KEY'
    (placeholder — swap in the real key before deploying)
  - Dev/test: VITE_POSTHOG_API_KEY || null
    PostHog never initialises when the key is null, so no events reach
    any PostHog project from dev servers or CI snapshot runs.
- initializePostHog() now returns null immediately when POSTHOG_API_KEY
  is null, before loading the posthog-js module at all.

src/mocks/analytics-handlers.ts
- Added https://z.openhands.dev/* intercept alongside the existing
  https://us.i.posthog.com/e handler. The library telemetry service
  routes through z.openhands.dev (the OpenHands reverse proxy), which
  MSW previously never saw.

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

* feat: hardcode PostHog keys per deployment environment

Select the PostHog key based on hostname rather than a single constant or
env var override:
- app.all-hands.dev  → POSTHOG_PROD_KEY
- staging.app.all-hands.dev → POSTHOG_STAGING_KEY (shares prod key for
  now; swap to a dedicated project key when one is provisioned)
- all other origins   → null (no tracking)

Both the app-level PostHog (option-service / posthog-wrapper) and the
library telemetry service (telemetry.ts) follow the same pattern.
VITE_POSTHOG_API_KEY still works as an escape hatch for library consumers.

Drop VITE_DO_NOT_TRACK=1 from dev:mock: hostname detection already
returns a null key for localhost, so PostHog never initialises there.
Removing the flag also restores the TelemetryConsentBanner in snapshot
tests (the banner can be snapshotted correctly again).

Also adds PRODUCT_URL.STAGING to constants and a null guard in
initializePostHog for the key-null case.

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

* fix: use prod PostHog project for now, drop placeholder staging URL

Remove the speculative staging.app.all-hands.dev hostname that doesn't
exist yet. Both POSTHOG_STAGING_KEY constants now alias POSTHOG_PROD_KEY
so staging events flow to the same project once the staging hostname is
wired up. The TODO comments mark exactly where to add the real staging
hostname and swap in a dedicated key.

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

* fix: scope telemetry key to prod builds only, revert app-level PostHog changes

telemetry.ts: switch from hostname detection to import.meta.env.PROD as
the gate — this tracks local installs everywhere they are deployed, not
just on a specific hostname. Dev/test builds get null so no events reach
PostHog. Both POSTHOG_PROD_KEY and POSTHOG_STAGING_KEY are named
constants pointing at the same project for now; swap POSTHOG_STAGING_KEY
once a dedicated staging project exists.

option-service.api.ts: revert to posthog_client_key null. The app-level
PostHog wrapper belongs to the SaaS analytics path and is not relevant
for local install tracking.

constants.ts: drop the speculative STAGING URL addition.

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

* feat: use VITE_APP_ENV to select staging vs prod PostHog key at bundle time

Both POSTHOG_PROD_KEY and POSTHOG_STAGING_KEY are now referenced in the
assignment. VITE_APP_ENV is a build-time constant: Vite replaces it with
a literal in the static bundle so the correct key is compiled in with no
runtime branching.

  VITE_APP_ENV=staging npm run build  → POSTHOG_STAGING_KEY
  (any other prod build)              → POSTHOG_PROD_KEY
  dev build (PROD=false)              → null

Set VITE_APP_ENV=staging in the staging deployment build config (Vercel
env vars, CI, etc.) to activate the staging key once it is provisioned.

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

* fix: gate PostHog key on VITE_APP_ENV, not PROD

import.meta.env.PROD is true for any vite build output including
npm run dev (which runs a production static build via dev-docker.mjs),
so local developers would inadvertently compile in POSTHOG_PROD_KEY.

Switch to requiring VITE_APP_ENV to be explicitly set at bundle time:
  VITE_APP_ENV=production → POSTHOG_PROD_KEY
  VITE_APP_ENV=staging    → POSTHOG_STAGING_KEY
  unset (local dev, CI)   → null, PostHog never initialises

Document VITE_APP_ENV in .env.sample so it is visible to developers.

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

* fix: bake VITE_APP_ENV=production into build:lib for npm releases

Without this, the published library bundle has POSTHOG_API_KEY=null
and library telemetry never fires for any consumer of the package.
A released npm package is a production artifact, so it should always
get POSTHOG_PROD_KEY compiled in.

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

* refactor: simplify PostHog key to staging default, prod only when explicit

Drop the null/three-way branch. The key is now always a string:
- VITE_APP_ENV=production (hardcoded in build:lib and production CI) → POSTHOG_PROD_KEY
- everything else (local dev, CI, staging builds)                    → POSTHOG_STAGING_KEY

Since the key is never null, the null guard in initializePostHog is also removed.

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

* Apply suggestions from code review

Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-15 16:24:24 -04:00
a998f88b72 fix(skills): populate slash menu and skills modal (#362)
* fix(skills): populate slash menu and skills modal on cloud backends

* fix(skills): always load public skills

* fix(deps): use HTTPS instead of SSH for typescript-client git dep

The package-lock.json was resolving @openhands/typescript-client as
git+ssh://... which requires SSH authentication. Vercel deployments
don't have SSH access configured for GitHub, causing deployment
failures.

Changed to git+https://... which allows Vercel to fetch the dependency
using standard HTTPS authentication.

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

* chore: retrigger Vercel deployment

---------

Co-authored-by: hieptl <hieptl.developer@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-16 03:03:27 +07:00
Hiep Le 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
2026-05-16 02:38:59 +07:00
Rohit Malhotraandopenhands 77b7012993 ci: add resolution guidance to failing snapshot PR comment (#489)
* ci: add resolution guidance to failing snapshot PR comment

When snapshots differ from the main baseline, the comment now includes
a short blockquote explaining both resolution paths:
- merge the latest main (in case upstream baselines have moved)
- add the update-snapshots label to acknowledge intentional changes

* ci: wait for main baseline workflow before downloading artifact

Before downloading the snapshot-baselines artifact on PR runs, resolve
main's current HEAD SHA and check if the snapshot-tests.yml run for
that exact commit is still in-progress or queued. If so, poll every
10 s (up to 10 min) until it completes, then proceed.

This eliminates the race condition where a PR job starts while main's
baseline upload is still in-flight, causing it to pull the previous
(stale) artifact and produce false snapshot failures.

The wait targets only the run for the current HEAD SHA — not an older
in-progress run from a different commit — so two rapid commits to main
can't trick the check into waiting for the wrong run.

Also bumps job timeout-minutes from 20 → 30 to accommodate the wait.

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-15 19:03:57 +00:00
Rohit Malhotraandopenhands 38121aaf2f fix: normalize path separators in no-direct-agent-server-calls test for Windows (#488)
On Windows, path.relative() returns backslash-separated paths, but
ALLOWED_AD_HOC_HTTP_FILES uses forward slashes. The Set.has() check
therefore never matches on Windows, causing false positive violations.

Normalize relPath to forward slashes before the check so the test
passes correctly on all platforms.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-15 14:42:45 -04:00
e9b5e07293 ci: store snapshot baselines as GitHub Actions artifacts instead of git (#482)
* ci: store snapshot baselines as GitHub Actions artifacts, not in git

Move baseline PNG storage from git to a 90-day GitHub Actions artifact
named 'snapshot-baselines', uploaded on every push to main.

- PRs download the latest main-branch artifact and run Playwright
  comparison against it; no more checked-in PNGs causing merge conflicts.
- New 'post-snapshot-comment.mjs' script classifies each snapshot as
  Changed/New/Unchanged, commits images to .pr/snapshots/<run_id>/ and
  posts a PR comment with collapsed <details> sections showing
  side-by-side expected/actual/diff images via raw.githubusercontent.com.
- For fork PRs or if push fails, falls back to a workflow run link for
  downloading the 'snapshot-test-results' artifact.
- Force-refresh baselines any time via workflow_dispatch force_update=true
  (replaces the old update_snapshots=true flow that committed PNGs to git).
- Remove 44 baseline PNGs from git; gitignore tests/e2e/__snapshots__/.
- Update AGENTS.md with the new workflow model.

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

* ci: fix bootstrap case — pass CI when no baseline artifact exists yet

When no main-branch 'snapshot-baselines' artifact has been uploaded yet
(e.g. this very first run after merging from an old baseline-in-git flow),
the comparison step fails because Playwright has nothing to compare against.
Gate the 'Fail if differences' step on has_baselines==true so the bootstrap
PR passes with all snapshots shown as new. Once it merges to main the
artifact is created and subsequent PRs compare normally.

Also derive the PR comment status from the classification (changed.length > 0)
rather than from TEST_OUTCOME, which is 'failure' in the bootstrap case
despite zero actual regressions.

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

* ci: delete stale comment and re-post on each push; always embed new snapshot images

- Replace PATCH-in-place with DELETE + POST so every push posts a fresh
  comment whose image URLs reference the current run's .pr/snapshots/<run_id>/.
  Editing in-place would leave raw.githubusercontent.com URLs pointing at
  the previous run's images once new images are committed under a new run_id.
- Embed new snapshot images inside the collapsed <details> section when
  commitSha is available; add a fallback artifact-download link when the
  push fails (e.g. fork PRs).
- Handle 204 No Content returned by DELETE in githubFetch.

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

* ci: fix find-run to query artifacts API by name, not workflow runs by status

The previous approach (find latest successful run of snapshot-tests.yml on
main) would match old runs that predate the artifact upload step, causing
actions/download-artifact to hard-fail with 'Artifact not found' before the
comparison or comment steps could run.

Fix: query the artifacts REST API directly for name=snapshot-baselines,
filtering to non-expired artifacts from the main branch. This guarantees
we only match runs that actually uploaded the baseline artifact.

Also add continue-on-error: true to the download step as a safety net
against the artifact expiring between the API lookup and the download.

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

* chore: snapshot images for run 25929925639 [skip ci]

* ci: replace .pr/snapshots on each run instead of accumulating per-run dirs

Previously each CI run committed images under .pr/snapshots/<run_id>/, so
reruns would accumulate multiple directories on the PR branch. The PR comment
always pointed to the current run's images (via SHA in the raw.githubusercontent
URL), but old directories silently piled up.

Fix: use a fixed .pr/snapshots/ path and git rm -rf --ignore-unmatch it before
staging new images. Each run completely replaces the previous images rather than
appending alongside them. Raw URLs still use the commit SHA so they remain stable
per push.

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

* chore: snapshot images for run 25930435218 [skip ci]

* ci: allow review_requested on draft same-repo PRs to trigger pr-review

GitHub does not fire pull_request events for review_requested on draft PRs —
only pull_request_target fires. The existing if condition rejected
pull_request_target for same-repo PRs via the fork check, so requesting
all-hands-bot or openhands-agent on a draft PR was always silently skipped.

Add a carve-out: pull_request_target is also accepted for same-repo PRs
when draft==true AND action==review_requested. Non-draft same-repo PRs are
unaffected — they continue to be handled by the pull_request event, and the
draft==true guard prevents pull_request_target from also running (no duplicate).

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

* ci: add update-snapshots label bypass for intentional snapshot changes

When snapshot diffs are expected (UI redesign, intentional change, etc.) the
author now adds the 'update-snapshots' label to the PR to acknowledge them:

- Fail step gains a !contains(labels, 'update-snapshots') guard so CI passes
  even when Playwright reports differences.
- The PR comment status adjusts: ❌ 'N snapshots differ — add label to
  acknowledge' when unapproved, ✅ 'N snapshots changed — acknowledged via
  label' when approved.
- The snapshot workflow now triggers on labeled/unlabeled events so that adding
  or removing the label immediately re-runs CI with the current label state in
  scope (no manual re-run or empty commit needed).
- New baselines are uploaded automatically when the PR merges to main, so no
  separate 'regenerate on main' step is needed.

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

* docs: update snapshot testing section in AGENTS.md

Add details on: artifact lookup by name, delete-then-post comment behavior,
fixed .pr/snapshots/ path (no accumulation), update-snapshots label bypass
for intentional changes, labeled/unlabeled triggers, bootstrap behavior.

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

* fix: git rm must run before copyFile, not after

On the second CI run the branch already has .pr/snapshots/ tracked from the
previous run. The old order was: copyFile → git rm → git add. git rm removes
tracked files from disk, which deleted the freshly written images, leaving the
directory empty and causing 'fatal: pathspec did not match any files'.

Fix: run git rm --ignore-unmatch before copyFile so the tracked files are
cleared from disk first; then copyFile writes clean new files with nothing
to conflict; then git add finds them as expected.

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

* chore: snapshot images for run 25931876588 [skip ci]

* fix: push snapshot images to orphan branch, not PR branch

Pushing to the PR branch with [skip ci] caused required checks to never
run on the HEAD commit, permanently blocking the PR.

New approach:
- publishImages() creates a fresh git repo in a temp directory, adds the
  images, and force-pushes to snapshot-artifacts/pr-<N> — a dedicated
  ephemeral branch that no CI workflow watches.
- The PR branch is never touched by CI, so required checks always run on
  the actual code commits.
- [skip ci] is removed; no loop prevention is needed because nothing
  triggers snapshot CI on the artifacts branch.
- Images in the orphan commit live at changed/<relPath>-{actual,expected,diff}.png
  and new/<relPath>.png (no .pr/snapshots/ prefix).
- raw.githubusercontent.com/<owner>/<repo>/<sha>/changed/... URLs are
  stable because they pin the orphan commit SHA.
- pr-artifacts.yml gains a closed trigger + cleanup-snapshot-artifacts job
  that deletes snapshot-artifacts/pr-<N> when the PR merges or is abandoned.
- Stale .pr/snapshots/ files from previous CI runs removed from this branch.

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

* docs: update AGENTS.md — orphan branch image storage, branch cleanup

* docs: tighten AGENTS.md snapshot section and pr-artifacts description

- Remove duplicate gitignore mention (already stated in baseline-storage line)
- Update pr-artifacts.yml description to cover both cleanup responsibilities:
  .pr/live-e2e/ (on approval) and snapshot-artifacts/pr-<N> (on close)

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

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-15 14:12:53 -04:00
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>
2026-05-16 00:57:13 +07:00
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>
2026-05-15 15:30:29 +00:00
Xingyao Wangandopenhands e5711e74b2 Clean up dev stack process trees on shutdown (#475)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-15 10:48:28 -04:00
Hiep Leandgithub-actions[bot] c50fe2ace3 fix(frontend): show full LLM model name on button and popover (#479)
* fix: show full LLM model name on button and popover

* chore: update baseline snapshots [skip ci]

* ci: trigger build

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-15 21:34:11 +07:00
Hiep Leandgithub-actions[bot] 94890db862 fix: refresh stale visual snapshot baselines (#473)
* fix: refresh stale visual snapshot baselines

* chore: update baseline snapshots [skip ci]

* ci: retrigger workflows

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-15 20:08:56 +07:00
Hiep Le f26ed89ea7 refactor: remove unused Recent Conversations section (#471) 2026-05-15 18:39:42 +07:00
Hiep Le b6df038ff1 fix: add cursor-pointer to sidebar Settings and onboarding Skip buttons (#469) 2026-05-15 18:17:38 +07:00
Hiep Le c4ae12863e fix: align sidebar conversation list loading skeleton with loaded card (#466) 2026-05-15 16:58:58 +07:00
Hiep Le 00f35cd8b1 fix(frontend): hide repo button on local backend in GitControlBar (#464)
* fix: hide repo button on local backend in GitControlBar

* refactor: update the code based on feedback
2026-05-15 16:42:25 +07:00
Hiep Le 818b788191 fix: localize ChatAddFileButton aria-label (#462) 2026-05-15 15:34:17 +07:00
Hiep Le a3d90fa4b1 fix: sync status-label translations with shortened English copy (#460) 2026-05-15 15:23:10 +07:00
bcd50c9832 chat: conversation UX polish (input controls, drawer state, ellipsis, git controls) (#455)
* 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>

* fix: lint

* fix: failing tests

* refactor: package-lock.json

* refactor: package-lock.json

* refactor: remove artifacts

* refactor: update the code based on feedback

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-05-15 14:43:16 +07:00
Graham Neubig 5b05321483 Fix Windows static asset serving (#456) 2026-05-15 01:24:11 -04:00
ed216c38aa feat(chat): add /model slash command for LLM profiles (#418)
* feat: integrate LLM profiles into settings route (PR C)

- Add LlmSettingsLocalView component for integrated profile management
- Extend SdkSectionSaveControl to expose form values for custom save flows
- Update LLM settings route to render profile list with create/edit views
- Add i18n keys for profile create/edit UI (CREATE_PROFILE, EDIT_PROFILE,
  PROFILE_CREATED, PROFILE_UPDATED, MODEL_REQUIRED, STATUS, BUTTON)
- Add test coverage for LlmSettingsLocalView

The integrated view shows:
- Profile list with active badge and action menu
- Add Profile button that opens create form
- Edit button that loads profile config and opens edit form
- Back/Cancel buttons to return to list view

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

* chore: address PR review feedback (#393)

- Improve mock typing with properly typed helper functions that provide all
  required React Query fields, eliminating incomplete 'as unknown as' casts
- Add integration test that verifies the save flow (fills in profile name,
  clicks save, verifies UI state transitions)
- Add component documentation noting future refactoring opportunity (extract
  useProfileForm, useProfileSave hooks for better testability)
- Document API key preservation behavior: currently preserves existing encrypted
  key in edit mode with no new key; note about potential 'Clear API Key' UX
  enhancement for future
- Document auto-derive name race condition: client-side uniqueness check uses
  render-time state, so concurrent profile creation by another client would
  result in server conflict error (handled gracefully)
- Document default export change in route file: LlmSettingsLocalView is now
  the default export; named export LlmSettingsScreen remains for embedded use

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

* fix: update llm-settings test to use named export

The default export of llm-settings.tsx changed to render LlmSettingsLocalView
(the profiles manager). The test needs to import the named export LlmSettingsScreen
to test the form component directly.

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

* Fix LLM profile button/badge sizing and update typescript-client to v0.6.0

- BrandButton: Change padding from p-2 to px-3 py-2 for better text display
- ProfileRow: Increase active badge vertical padding from py-0.5 to py-1
- Update @openhands/typescript-client from commit SHA to v0.6.0 tag

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

* Fix LLM settings to show regular form in cloud mode and empty form in create mode

- LlmSettingsRoute: Render LlmSettingsScreen (standard form) for cloud backends
  and LlmSettingsLocalView (profile manager) for local backends only
- LlmSettingsLocalView: Pass empty initial values in create mode to ensure
  fresh form fields, add key prop to force form remount between profiles
- Add unit tests for cloud vs local backend rendering
- Add unit tests for create mode empty form initialization

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

* Fix edit mode form initialization to display profile values

- Fix initialValueOverrides logic to properly check for edit mode AND
  existing initialValues before using them
- Add prefix to edit mode key for clearer remount semantics
- Add unit tests verifying edit mode populates profile name correctly
- Add unit tests verifying getProfile is called with encrypted mode

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

* Add debug logging to trace edit profile data flow

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

* Fix edit profile config parsing - read from config directly not config.llm

The API returns profile config with llm settings at the top level
(config.model, config.api_key, config.base_url), not nested under
config.llm. Fixed the parsing to read directly from detail.config.

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

* Handle profile rename during edit and update active profile

When editing a profile and changing its name:
1. Rename the profile first using ProfilesService.renameProfile
2. Then save the profile config to the new name
3. If the renamed profile was the active profile, re-activate it
   after the rename (since rename doesn't update active_profile)

This prevents creating duplicate profiles when just changing the name.

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

* Fix package-lock.json to use https protocol for typescript-client

The lock file was using git+ssh:// protocol which causes Vercel build
failures since Vercel doesn't have SSH keys configured. Changed to
git+https:// and removed the integrity hash (git deps don't have one).

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

* fix(profiles): Available Profiles heading translation

* fix(profiles): use brand badge for active profile indicator

* feat(profiles): replace form heading with "Back to LLM profiles list"

* fix(profiles): unify profile-name validation and reject any whitespace

* fix(onboarding): persist onboarding LLM choice as an active profile

* refactor(profiles): drop redundant trim/wrapper after validator change

* feat(chat): add /model slash command for LLM profiles

* Add model profile slash completions

* chore: address model command review feedback (#418)

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

* chore: address model command follow-up review (#418)

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

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2026-05-15 01:19:55 -04:00
c371afec55 feat: LLM profiles route integration (PR C) (#393)
* feat: integrate LLM profiles into settings route (PR C)

- Add LlmSettingsLocalView component for integrated profile management
- Extend SdkSectionSaveControl to expose form values for custom save flows
- Update LLM settings route to render profile list with create/edit views
- Add i18n keys for profile create/edit UI (CREATE_PROFILE, EDIT_PROFILE,
  PROFILE_CREATED, PROFILE_UPDATED, MODEL_REQUIRED, STATUS, BUTTON)
- Add test coverage for LlmSettingsLocalView

The integrated view shows:
- Profile list with active badge and action menu
- Add Profile button that opens create form
- Edit button that loads profile config and opens edit form
- Back/Cancel buttons to return to list view

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

* chore: address PR review feedback (#393)

- Improve mock typing with properly typed helper functions that provide all
  required React Query fields, eliminating incomplete 'as unknown as' casts
- Add integration test that verifies the save flow (fills in profile name,
  clicks save, verifies UI state transitions)
- Add component documentation noting future refactoring opportunity (extract
  useProfileForm, useProfileSave hooks for better testability)
- Document API key preservation behavior: currently preserves existing encrypted
  key in edit mode with no new key; note about potential 'Clear API Key' UX
  enhancement for future
- Document auto-derive name race condition: client-side uniqueness check uses
  render-time state, so concurrent profile creation by another client would
  result in server conflict error (handled gracefully)
- Document default export change in route file: LlmSettingsLocalView is now
  the default export; named export LlmSettingsScreen remains for embedded use

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

* fix: update llm-settings test to use named export

The default export of llm-settings.tsx changed to render LlmSettingsLocalView
(the profiles manager). The test needs to import the named export LlmSettingsScreen
to test the form component directly.

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

* Fix LLM profile button/badge sizing and update typescript-client to v0.6.0

- BrandButton: Change padding from p-2 to px-3 py-2 for better text display
- ProfileRow: Increase active badge vertical padding from py-0.5 to py-1
- Update @openhands/typescript-client from commit SHA to v0.6.0 tag

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

* Fix LLM settings to show regular form in cloud mode and empty form in create mode

- LlmSettingsRoute: Render LlmSettingsScreen (standard form) for cloud backends
  and LlmSettingsLocalView (profile manager) for local backends only
- LlmSettingsLocalView: Pass empty initial values in create mode to ensure
  fresh form fields, add key prop to force form remount between profiles
- Add unit tests for cloud vs local backend rendering
- Add unit tests for create mode empty form initialization

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

* Fix edit mode form initialization to display profile values

- Fix initialValueOverrides logic to properly check for edit mode AND
  existing initialValues before using them
- Add prefix to edit mode key for clearer remount semantics
- Add unit tests verifying edit mode populates profile name correctly
- Add unit tests verifying getProfile is called with encrypted mode

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

* Add debug logging to trace edit profile data flow

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

* Fix edit profile config parsing - read from config directly not config.llm

The API returns profile config with llm settings at the top level
(config.model, config.api_key, config.base_url), not nested under
config.llm. Fixed the parsing to read directly from detail.config.

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

* Handle profile rename during edit and update active profile

When editing a profile and changing its name:
1. Rename the profile first using ProfilesService.renameProfile
2. Then save the profile config to the new name
3. If the renamed profile was the active profile, re-activate it
   after the rename (since rename doesn't update active_profile)

This prevents creating duplicate profiles when just changing the name.

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

* Fix package-lock.json to use https protocol for typescript-client

The lock file was using git+ssh:// protocol which causes Vercel build
failures since Vercel doesn't have SSH keys configured. Changed to
git+https:// and removed the integrity hash (git deps don't have one).

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

* chore(profiles): UI polish, shared validation, onboarding integration (#417)

* fix(profiles): Available Profiles heading translation

* fix(profiles): use brand badge for active profile indicator

* feat(profiles): replace form heading with "Back to LLM profiles list"

* fix(profiles): unify profile-name validation and reject any whitespace

* fix(onboarding): persist onboarding LLM choice as an active profile

* refactor(profiles): drop redundant trim/wrapper after validator change

* Fix LLM profile route mocks and warnings

* chore: update baseline snapshots [skip ci]

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Vasco Schiavo <115561717+VascoSch92@users.noreply.github.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-15 02:46:35 +00:00
e20fd0c105 test(snapshot): onboarding modal steps + sidebar new-conversation popover (#449)
* test(snapshot): onboarding modal 4 steps + sidebar new-conversation popover

4 onboarding-step snapshots (choose-agent, check-backend, setup-llm,
say-hello) and 1 sidebar new-conversation popover snapshot spec.

The sidebar popover test is marked test.fixme pending re-wiring of
NewConversationButton in sidebar.tsx (temporarily commented out at
lines 241-244 with 'Temporarily hide the dedicated New Conversation
button' comment). The component and its testids exist in
new-conversation-button-local.tsx.

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

* chore: update baseline snapshots [skip ci]

* ci: trigger re-run against CI-generated baselines

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-14 23:18:02 +00:00
Xingyao Wangandopenhands b06ac9114b Add Docker projects workspace shortcut (#397)
* Add Docker projects workspace shortcut

* Address Docker projects review feedback

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-14 17:23:38 -04:00
Xingyao Wang a654d54ae9 Use ws for HTTP websocket targets (#453) 2026-05-14 20:59:05 +00:00
863f479f07 test(snapshot): skills page loaded/search/filter states (#447)
* test(snapshot): skills page loaded / search / filter states

Expose window.__OH_QUERY_CLIENT__ in dev/mock mode so Playwright tests
can seed the React Query cache and bypass MSW's empty skills response.
Adds 4 new tests: loaded grid, search-filtered, no-match, type-filter.

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

* chore: update baseline snapshots [skip ci]

* ci: trigger re-run against CI-generated baselines

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-14 20:30:20 +00:00
Xingyao Wang 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
2026-05-14 16:15:03 -04:00
0f6bfe7b42 test(snapshot): batch 2 — secrets, verification/condenser, sidebar (12 tests, 12 baselines) (#441)
* test(snapshot): batch 2 — secrets, verification/condenser, sidebar (12 tests)

Add three new Playwright visual snapshot spec files with 12 tests total,
all passing against local baselines:

**settings-secrets.snapshot.spec.ts** (5 tests)
- secrets list with two pre-seeded rows
- add-new-secret form open (empty)
- add-new-secret form filled with name + value
- after saving — list shows three rows
- delete confirmation modal open

**settings-verification.snapshot.spec.ts** (4 tests)
- verification settings page (confirmation mode OFF, default)
- verification settings with confirmation mode ON — reveals Security
  Analyzer combobox
- verification settings dirty — Save Changes button enabled after toggle
- condenser settings page renders schema-driven form

**sidebar.snapshot.spec.ts** (3 tests)
- conversation panel with three conversations + status dots
- sidebar collapsed to icon rail
- conversations filter menu opened from toggle button

Implementation notes:
- SettingsSwitch renders the checkbox as `<input hidden>`; click the
  enclosing `<label>` via `label:has([data-testid])` selector instead of
  trying to click the hidden input directly.
- HeroUI Autocomplete does not forward `data-testid` to the DOM; use
  `getByRole("combobox", { name: /.../ })` for the Security Analyzer field.
- Added condenser section to MOCK_AGENT_SETTINGS_SCHEMA so
  /settings/condenser renders the schema-driven form (not the
  "SDK settings schema unavailable" fallback).
- Added condenser default values to MOCK_DEFAULT_USER_SETTINGS.agent_settings.
- Local baselines committed; CI must regenerate via the
  "Update baseline snapshots" workflow dispatch before merging.

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

* docs: update AGENTS.md with snapshot testing lessons from batch 2

Add three key patterns discovered while implementing batch 2 snapshot tests:
- Hidden checkbox pattern (SettingsSwitch label-click workaround)
- HeroUI Autocomplete testId non-forwarding (use role selector)
- SdkSectionPage early return when schema section is missing

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

* chore: update baseline snapshots [skip ci]

* ci: trigger re-run against CI-generated baselines

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

* test(snapshot): drop redundant verification-settings-dirty test

The dirty test was pixel-for-pixel identical to the ON test: both
navigate to /settings/verification (confirmation_mode defaults to false),
click the toggle ON, then screenshot. There is no visual distinction
between 'form is now ON' and 'form is dirty after being toggled ON' because
clicking the toggle is both the action that makes the form dirty and the
action that reveals the Security Analyzer field.

The 'ON' snapshot already captures the dirty/enabled state implicitly:
  - toggle gold (ON)
  - Security Analyzer dropdown visible
  - Save Changes button bright/enabled (vs. dimmed in the OFF snapshot)

Remove the test and its baseline. The spec now has 3 tests:
  1. verification OFF  — toggle gray, no analyzer, Save dimmed
  2. verification ON   — toggle gold, analyzer visible, Save enabled
  3. condenser         — schema-driven form

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

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-14 19:34:59 +00:00
Graham Neubigandopenhands 3f28f2d625 Fix static automation agent-server auth (#442)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-14 14:54:04 -04:00
Graham Neubigandopenhands a4089e0e4a Default user launchers to static frontend (#434)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-14 18:18:36 +00:00
15363865b7 test: add snapshot coverage for Automations, MCP, and Skills pages (#436) (#438)
* test: add snapshot tests for Automations, MCP, and Skills pages

Add 7 new visual regression snapshot tests covering three previously
untested pages:

  automations.snapshot.spec.ts (3 tests)
    - automations-list-active-inactive: full list with active/inactive groups
    - automations-search-no-results: search filter that matches nothing
    - automations-delete-modal: delete confirmation modal overlay

  mcp-page.snapshot.spec.ts (3 tests)
    - mcp-empty-installed: empty installed section + full marketplace
    - mcp-custom-server-editor: "Add custom MCP server" modal form
    - mcp-search-filtered: marketplace filtered by "slack" query

  skills-page.snapshot.spec.ts (1 test)
    - skills-empty: empty skills state (MSW returns { skills: [] })

Supporting changes:
  - src/mocks/automation-handlers.ts: add GET /api/automation/health MSW
    handler (returns { status: "ok" }) so the automations list page can
    render without a real backend in mock mode
  - tests/e2e/snapshots/COVERAGE_PLAN.md: 16-spec snapshot coverage plan
    with TODOs for backend-not-configured and loaded-skills states that
    require exposing window.__MSW_WORKER__ for per-test handler overrides

Key design decisions documented in spec file headers:
  - Automation health/list: MSW intercepts all same-origin requests before
    page.route() (service worker takes precedence); the new MSW health
    handler is required for the list page to load in mock mode
  - MCP "with installed": settings mock cannot be injected via page.route()
    since MSW wins for GET /api/settings; replaced with the custom server
    editor modal which is state-independent
  - Skills loaded/search: POST /api/skills is intercepted by MSW (returns
    []) before page.route() can override; deferred with TODO comment

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

* test: add empty-automations + iterative MCP install snapshot tests

Merge latest main (2 commits), then add 3 new snapshot tests:

  automations-no-automations (1 new PNG)
    - Deletes all 5 seed automations via MSW DELETE REST calls, then
      triggers a React Query refetch (without page.reload) to show the
      EmptyState component with the "How to create an automation" cards.
    - Key insight: the MSW automations Map is PAGE-level JS state, not
      service-worker state. page.reload() reinitialises it to 5 items;
      using window.__TEST_INVALIDATE_QUERIES__() avoids this.

  mcp-slack-install-{1-4} (4 new PNGs) – iterative marketplace install
    step 1: marketplace before install (Slack card visible)
    step 2: Slack install modal open (empty SLACK_BOT_TOKEN / SLACK_TEAM_ID)
    step 3: install modal with filled credentials (token masked, T01ABC123)
    step 4: after clicking Install — Slack in Installed section, success
            toast "MCP server saved.", Slack card shows "INSTALLED" badge

  mcp-custom-server-{1-4} (4 new PNGs) – iterative custom SSE server add
    step 1: custom server editor open (SSE type selected, empty URL)
    step 2: URL filled in (https://api.example-mcp.com/sse)
    step 3: API key also filled (optional field)
    step 4: after Add Server — custom SSE card in Installed section
            (toast dismissed, clean installed view)

All flow tests use real simulated clicks + form fills via data-testid
selectors; no page.route() overrides needed because the MSW settings
PATCH/GET cycle persists mcp_config across the refetch.

Supporting changes:
  - src/entry.client.tsx: expose window.__TEST_INVALIDATE_QUERIES__ in
    mock mode so specs can trigger React Query refetches without reload.
    Documented why reload-based approaches fail (MSW handler state is
    PAGE-level JS, reset on every full page load).

All 10 snapshot tests pass (34s).

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

* docs: document MSW page-level JS state and __TEST_INVALIDATE_QUERIES__ helper

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

* chore: update baseline snapshots [skip ci]

* ci: trigger snapshot validation against CI-regenerated baselines [skip ci]

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

* ci: validate snapshots against CI-regenerated baselines

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

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-14 13:51:34 -04:00
4ed58e2379 fix: restore data-testid="chat-interface" removed by #340 (#437)
* fix: restore data-testid="chat-interface" removed by #340

PR #340 added left padding to the ChatInterface wrapper div but
accidentally dropped the data-testid attribute in the same edit.
This broke both the collapsible-thinking snapshot tests and the live
e2e test, which both use getByTestId('chat-interface') as the load
signal and screenshot target.

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

* chore: update baseline snapshots [skip ci]

* ci: trigger re-run after snapshot baseline update

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-14 16:02:33 +00:00
Hiep Le ba3ab46251 fix: allow chat column to shrink below content min-content (#435) 2026-05-14 22:27:26 +07:00
Graham Neubigandopenhands 5ccc8e627c Fail fast when frontend deps are missing (#404)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-14 10:32:03 -04:00
Hiep Le b1b3a37e24 fix: stop skeleton from flashing on 10s background refetch (#431) 2026-05-14 20:55:01 +07:00
Hiep Le 301273dae1 fix: prevent add custom server button from being shrunk by flex header (#429) 2026-05-14 19:32:05 +07:00
Hiep Le da1eb5318a fix: remove stale MCP item from Settings sidebar (#427) 2026-05-14 19:11:51 +07:00
0205ad89c8 feat(extensions): Extensions nav, unified card design, and UX polish across Skills & MCP (#386)
* feat(extensions): add Extensions left-nav with Skills/Plugins/MCP tabs

- Add ExtensionsNavigation sidebar component with Skills, Plugins, and MCP Servers tabs
- Add /skills/plugins route and page scaffold
- Refactor skills-settings page to use original switch-list style with auto-save
- Rename sidebar Skills nav link to "Extensions" with Boxes icon

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(extensions): remove Integrations from main sidebar, add nav to MCP page

- Remove the standalone Integrations/MCP link from the main sidebar nav
- Add ExtensionsNavigation to the MCP page so all three extensions pages
  (Skills, Plugins, MCP Servers) share the same left-nav two-column layout
- Drop unused PuzzleIcon import from sidebar

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(extensions): normalize ExtensionsNavigation position across all pages

Align outer container on skills-plugins and mcp pages to match
skills-settings: remove redundant px-[14px] from wrappers (the aside
already carries md:pl-[14px]), add pr-[14px] to right scroll columns,
and drop overflow-hidden from the plugins wrapper so sticky works.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(extensions): move plugins route from /skills/plugins to /plugins

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(sidebar): highlight Extensions link when on /mcp or /plugins

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(extensions): reorder nav — Skills, MCP Servers, Plugins

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(extensions): use custom stacked-boxes icon for Skills nav item

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(extensions): use hexagon icon for Plugins nav item

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(extensions): update Plugins icon to box with cross-section lines

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(extensions): unify page header styles across Skills, MCP, and Plugins

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(skills): move toggle to right side of skill rows

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(extensions): use text-muted for grey description text on all extension pages

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(extensions): use text-tertiary-light for page subtitle text

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skills): add max-w-5xl content wrapper to match MCP page width

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(skills): wrap each skill in a card matching MCP marketplace style

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(skills): two-column grid layout and white hover border on cards

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): use white hover border on marketplace cards

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): move backend synced badge below description text

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(skills): bring in full SkillCard UI with description, badges, triggers, and details

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(skills): move toggle to right side of skill card header

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(extensions): move backend synced badge from MCP page to extensions nav

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): change marketplace grid from 3 columns to 2 columns

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(badge): remove icon, border, background, and rounded pill from backend synced badge

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skills): align skill card colors with MCP marketplace card tokens

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): align top padding and header spacing with other extensions pages

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): use items-center on header row to stop title being pushed down

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): group title+description together so button doesn't inflate the gap

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): remove orphaned BackendSyncedSettingsBadge JSX reference causing crash

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skills): remove monospace font from trigger chip pills

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skills): hide empty description placeholder when no description provided

Co-authored-by: Cursor <cursoragent@cursor.com>

* Match skills and MCP search input style and width to one grid column

Replace skills toolbar search with same rounded-lg/border-tertiary/bg-base-secondary
style as the MCP MarketplaceSearch, and constrain both to w-1/2 so they span
exactly one column of the two-column grid layout.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Increase skills toolbar gap from gap-3 to gap-6

Co-authored-by: Cursor <cursoragent@cursor.com>

* Make extensions page grids and search inputs responsive at smaller sizes

- Skills and MCP grids: grid-cols-1 on mobile, md:grid-cols-2 on larger screens
- Skills and MCP search inputs: w-full on mobile, md:w-1/2 on larger screens

Co-authored-by: Cursor <cursoragent@cursor.com>

* Remove Details collapsible section from skill cards

Co-authored-by: Cursor <cursoragent@cursor.com>

* Remove phantom gap from labelless SettingsSwitch by rendering label div only when needed

Co-authored-by: Cursor <cursoragent@cursor.com>

* Change search focus ring from yellow/primary to white on both Skills and MCP

Co-authored-by: Cursor <cursoragent@cursor.com>

* Change active filter pill highlight from yellow/primary to white

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add 'Soon' badge to Plugins nav item in Extensions navigation

Co-authored-by: Cursor <cursoragent@cursor.com>

* Change badge text from 'Soon' to 'Coming Soon'

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add 'Installed' section label above skills grid to match MCP page

Co-authored-by: Cursor <cursoragent@cursor.com>

* Replace Marketplace heading and description with plain 'Library' label

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add text-foreground to section labels to fix black text rendering

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix MCP delete hang: close modal on error, reduce save retry count to 1

Co-authored-by: Cursor <cursoragent@cursor.com>

* Remove 'Installed' section label from skills grid

Co-authored-by: Cursor <cursoragent@cursor.com>

* Remove icons from skill type badges

Co-authored-by: Cursor <cursoragent@cursor.com>

* Restore Details dropdown on skill cards, remove only the top border line

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skills): stabilize settings tests and no-match testid

- Add data-testid hooks for the intro copy and zero-result message.
- Align skills route tests with Extensions nav (/plugins) and the current
  description block (no separate badge).
- Use fireEvent.change for the search no-match case; user.type hit the
  default test timeout in isolation.

PR note: Empty Skills filter chips and search placeholder in dev were
from stale generated i18n (keys exist in translation.json but not in
ignored public/locales until make-i18n). Run npm run make-i18n after
editing translations; npm run build already runs it via pre-build.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-05-14 18:39:15 +07:00
607ddae7fb fix: refine sidebar and home onboarding layout (#340)
* fix: simplify sidebar nav and add settings-focused rails

Remove the legacy settings submenu from the left sidebar, move key actions into sticky footer controls, and add a dedicated settings-page left rail so navigation matches the new layout and interaction flow.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: improve collapsed sidebar expand affordance

Make the collapsed rail open reliably from empty-space clicks and hover by swapping the logo area to an explicit expand control. Also update the OpenHands logo asset to use white/transparent fills for the requested visual treatment.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: refresh conversation panel filtering and sidebar visual separation

This aligns the conversation list behavior with the new older-conversation filter/menu flow and adds a clear desktop divider for the left navigation rail to improve layout clarity.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: prevent conversation list horizontal overflow

This makes repo/branch metadata rows shrink and truncate correctly and enforces horizontal clipping in the conversation list scroller so long labels never introduce sideways scrolling.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: refine sidebar conversation list layout and interactions

This updates the left-nav conversation list styling/spacing and keeps the older-conversations controls fixed above the scroll region, while tightening dropdown/timestamp behavior to match the intended hover and layering UX.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: simplify sidebar new conversation entry

This temporarily hides the standalone New Conversation button and reuses the first sidebar nav item as a compact "New" entry with a plus icon to streamline the left rail.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: align sidebar conversation header and new-link state

This tightens the conversation summary indent to align with the nav icon column and makes the + New sidebar link active only on the base conversations route.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: tighten sidebar conversation list spacing

Unify sidebar and conversation-list horizontal padding at 2px offsets so nav items and rows align consistently, and reduce extra empty row spacing by only rendering metadata footers when shown.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: align collapsed backend switcher with shared dropdown

Reuse the existing dropdown menu implementation for the collapsed sidebar backend switcher so it matches the primary backend selector behavior and opens upward with the expected menu layout.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: show backend connection dot on collapsed server control

Surface the active backend health indicator on the collapsed server button so connectivity status remains visible even when the full backend selector is hidden.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: hide conversation rail items in collapsed sidebar

Stop rendering the compact conversation row list when the sidebar is collapsed so the left rail only shows the intended navigation controls.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: align older-conversations menu with dropdown styles

Apply the shared dropdown menu treatment to the older-conversations filter and add a checkmark indicator for repo/branch metadata visibility.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: standardize conversation and backend dropdown color tokens

Align the older-conversations and backend selector menus with the shared context-menu visual tokens so dropdown surfaces and separators match the conversation view styling.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: make backend footer controls transparent at rest

Remove default border and fill from the backend selector footer controls so they match sidebar button behavior with hover-only emphasis.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: refine sidebar settings state and load-more styling

Restore an explicit active style for the backend settings button and remove the default underline from the conversation list load-more action.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: remove extra indentation from settings sidebar tabs

Stop using the indented sidebar link variant in desktop settings navigation so tab items align with the rest of the sidebar.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: add matching left padding to chat layout container

Apply the same responsive left inset as the right side in the chat interface wrapper so content alignment is balanced on desktop.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: streamline and center home onboarding layout

Move the guide banner to the top, center the main onboarding content, and remove the recent conversations/tasks section from the home screen to simplify first-run focus.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: restore collapsed settings navigation action

Wire the collapsed settings control to navigation context and add coverage to prevent regressions when the sidebar is collapsed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(sidebar): open backend dropdown on hover in both collapsed and expanded states

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(sidebar): add close delay so cursor can reach dropdown before it dismisses

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(sidebar): keep backend popover open on mouse-leave so modal clicks don't get swallowed

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(sidebar): lift modal state to Sidebar so Add/Manage Backend work from collapsed popover

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(sidebar): restore missing AutomationsIcon import

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(sidebar): make collapsed backend popover dropdown actions work

Two distinct bugs prevented users from interacting with the backend popover
when the sidebar was collapsed:

1. Selecting a backend option silently expanded the sidebar instead of
   switching backends. The aside's rail-collapse handler only bails on
   `a, button, input, [role='button'], [role='link']`, but dropdown options
   are `<li role='option'>`. The click bubbled up and `setCollapsed(false)`
   ran before the backend switch completed, unmounting the popover mid-flight.
   Fix: stop click propagation at the popover wrapper so the rail handler
   never sees clicks from inside it.

2. Clicking Add Backend / Manage Backends opened the modal but it
   immediately disappeared. The modal state lived inside `BackendSelector`,
   which was mounted inside the popover. Once the cursor moved toward the
   centred modal, the popover's onMouseLeave timer fired -> the popover
   closed -> `BackendSelector` unmounted -> the modal state was destroyed.
   Fix: lift the modal state into `Sidebar` and render the modals there.
   `BackendSelector` accepts new optional `onOpenAddBackend` and
   `onOpenManageBackends` callbacks; when provided it skips its internal
   modal rendering.

Also restores the expanded sidebar's hover-close behavior on the shared
Dropdown (had been left as `onMouseLeave={undefined}`).

Adds focused regression tests covering all three behaviors.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(dropdown): wire hidden combobox trigger for collapsed nav

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: lint

* fix: failing tests

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-05-14 16:42:35 +07:00
a1befdd89d Fix stale backend health errors after recovery (#424)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-05-14 11:59:14 +07:00
3a0eb9a9b1 feat: collapse thinking and extended reasoning into collapsible sections (#409)
* feat: collapse thinking and extended reasoning into collapsible sections

Render ThinkAction events and LLM reasoning content (reasoning_content /
thinking_blocks) inside collapsible sections instead of inline chat
messages. This keeps the chat compact, especially when the thinking
language (usually English) differs from the conversation language.

Changes:
- Add CollapsibleThinking component with lightbulb icon and expand/collapse toggle
- Update EventMessage to render ThinkAction as CollapsibleThinking
- Show reasoning_content/thinking_blocks from action events as collapsible
- Add i18n translations for Thinking title/expand/collapse (15 languages)
- Add getReasoningContent helper to extract extended thinking from events
- Update tests for new collapsible behavior

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

* docs: add collapsible thinking notes to AGENTS.md

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

* fix: resolve prettier/eslint lint errors in collapsible thinking

- Reformat multiline ternary to single line in collapsible-thinking.tsx
- Remove unnecessary JSX wrapping around <CollapsibleThinking /> in event-message.tsx
- Remove unused eslint-disable directive for react/jsx-props-no-spreading

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

* test: add Playwright visual snapshot tests for collapsible thinking

Add 4 visual snapshot tests covering:
- ThinkAction collapsed state (lightbulb icon + 'Thinking' label)
- ThinkAction expanded state (full thinking content visible)
- Reasoning content (reasoning_content field) collapsed with bash action
- Reasoning content expanded alongside bash action

Uses MSW mock dev server for API handling and injects events
directly into the Zustand event store via __OH_EVENT_STORE__.

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

* chore: update baseline snapshots [skip ci]

* ci: trigger CI with updated baseline snapshots

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

* docs: update AGENTS.md with conversation snapshot testing patterns

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

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-14 11:38:32 +07:00
ebe6b392d4 fix: pre-bundle all dependencies to prevent Safari 504 errors (#414)
* fix: pre-bundle all dependencies to prevent Safari 504 errors

Safari gets stuck in 504 Gateway Timeout errors when Vite discovers new
dependencies at runtime and triggers 'optimized dependencies changed. reloading'.
Chrome handles this gracefully, but Safari cannot recover.

The fix is to pre-bundle all dependencies that were being discovered at runtime:
- @openhands/typescript-client and its subpath exports
- posthog-js/react, class-variance-authority, downshift, framer-motion
- rehype-raw, rehype-sanitize, unist-util-visit
- uuid, zustand, zustand/middleware
- All react-syntax-highlighter language definitions (60+ languages)

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

* fix: add no-store header to prevent Safari caching stale modules

After a fresh npm install, Vite generates new hashes for pre-bundled deps.
Safari's aggressive caching holds references to old URLs that no longer
exist, causing blank screens that require manually clearing cache.

Adding Cache-Control: no-store ensures Safari always fetches fresh
module references in dev mode.

* fix: force dep re-bundling on every server start for Safari

Adding optimizeDeps.force=true ensures Vite re-bundles all dependencies
on every server start, generating fresh hashes. This prevents Safari from
trying to load stale cached module URLs after npm install.

* fix: use noDiscovery instead of force to prevent Safari 504s

The force: true option was causing Vite to rebuild ALL deps on every server
start, creating a race condition where Safari requests deps during optimization
and gets 504s. Chrome silently retries through this, Safari doesn't.

Switching to noDiscovery: true instead - this tells Vite to only pre-bundle
deps listed in 'include' and never discover new ones at runtime. This avoids
the runtime optimization that causes 504s, while still using cached deps
when available (faster startup).

* chore: remove build step from README - no longer needed

The vite.config.ts fix (noDiscovery + comprehensive include list) makes
the build step unnecessary for Safari compatibility.

* chore: remove Cache-Control header - not needed with noDiscovery

The no-store header was a workaround for Safari caching stale module URLs
after 504 errors. With noDiscovery: true, 504s can't happen, so the header
is unnecessary.

* docs: add dev:static as alternative for non-development use

dev:static serves pre-built static files instead of running Vite dev server.
Faster loads, better for slow networks, no hot reload needed if you're just
running agent-canvas rather than developing on it.

---------

Co-authored-by: John-Mason Shackelford <john-mason@openhands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-13 20:24:58 +07:00