Commit Graph
7817 Commits
Author SHA1 Message Date
4bd3baccde ci: adopt release-please via shared release-actions (#1496)
* ci: adopt release-please via shared release-actions

Standardize agent-canvas releases on the org-wide release-please
automation (OpenHands/release-actions), matching typescript-client and
OpenHands/OpenHands.

Adds the three caller workflows:
- release.yml      release-please on push to main / release/**
- pr.yml           Conventional-Commit title lint + type: labels
- release-ready.yml draft release PR -> "Ready for review" gate
                   (Slack alert to #proj-agent-canvas + optional tests)

Adds the release-please state files: release-please-config.json
(node, draft PRs, extra-files), .release-please-manifest.json,
.github/release.yml (changelog categories), version.txt. Manifest is
seeded at 1.0.0 (the current GA release, v1.0.0 shipped 2026-06-15).

extra-files keeps the non-package.json version pins in lockstep:
config/defaults.json ($.versions.agentCanvas) and the README docker
example. The node release-type already bumps package.json and
package-lock.json.

Corrects stale on-main version refs (package.json rc.6,
config/defaults.json + README rc.11) to the real released 1.0.0, so all
pins share a single source of truth.

Removes create-release.yml: release-please now owns tag + GitHub Release
creation. npm-publish.yml and docker.yml are unchanged - they trigger on
the v* tags release-please still produces (pushed with the release App
token so tag-triggered workflows fire).

Co-authored-by: smolpaws <engel@enyst.org>

* fix: sync README.windows.md docker tag + track it in release-please

The docs-version-sync test also checks README.windows.md, which still
pinned 1.0.0-rc.11. Update both its docker image refs to the released
1.0.0 and add it to release-please extra-files (with the
x-release-please-version annotation) so future bumps keep it in lockstep
alongside README.md and config/defaults.json.

Co-authored-by: smolpaws <engel@enyst.org>

* ci: pin v-prefix tagging and first-release start point

Two robustness fixes after confirming agent-canvas's release conventions:

- include-v-in-tag: true (explicit). agent-canvas tags are vX.Y.Z
  (v1.0.0, v1.0.0-rc.12), and npm-publish.yml + docker.yml trigger on
  push tags 'v*'. release-please's node default already adds the v, but
  pin it explicitly so a default change can't silently drop the prefix
  and break tag-triggered publishing. (Matches typescript-client, which
  also ships v-prefixed tags; differs from OpenHands/OpenHands, which
  sets include-v-in-tag:false for its no-v scheme.)

- last-release-sha pinned to the v1.0.0 release commit
  (7b9c17e). v1.0.0 is a real, published release (2026-06-15; npm
  latest). This tells release-please the first managed release starts
  from there, so its first run scans only post-1.0.0 commits instead of
  walking the whole history.

Co-authored-by: smolpaws <engel@enyst.org>

* ci: drop orphaned version.txt

Per AI review: version.txt would never be updated and nothing consumes
it. The README's "four state files" guidance assumes the `simple`
release-type (which release-actions itself uses, where version.txt is
the canonical version source). agent-canvas uses `release-type: node`,
where package.json is the source of truth — so version.txt is redundant
and not auto-bumped. The other node adopters (typescript-client,
OpenHands/OpenHands) ship no version.txt either. Remove it rather than
keep a file that silently goes stale.

Co-authored-by: smolpaws <engel@enyst.org>

* docs: document the release-please flow

---------

Co-authored-by: smolpaws <engel@enyst.org>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-07-09 12:22:29 +07:00
Hiep Le 5538b699a7 fix: let users declare self-hosted OHE as a Cloud backend (#1627)
* fix: let users declare self-hosted OHE as a Cloud backend

* fix: failing tests
2026-07-09 03:15:24 +07:00
simonrosenbergandClaude Opus 4.8 54d718ad4e feat(agent-profiles): Agent Profiles — Settings → Agent as the profile library (local + cloud) (#1571)
* feat(agent-profiles): minimal local Agent Profiles library reusing the Agent settings form

Adds a Settings → Agent profiles library (local backends only) that mirrors
the LLM-profiles UX: a list of named profiles with a create/edit view that
reuses the existing Agent settings form as the editor — you just add a name
(and, for OpenHands agents, pick an LLM profile).

Deliberately minimal vs the full Phase-4 UX: no chat-input picker, no live
switch, no Settings information-architecture rework. Condenser / verification /
MCP stay global, exactly as on main.

- Data layer: AgentProfilesService + list/save/delete/rename/activate hooks
  wrapping the ts-client AgentProfilesClient (endpoints shipped in
  agent-server v1.29.0).
- Editor: AgentSettingsScreen gains an opt-in `embedded` mode (hides its
  header + global Save, seeds from an override, and reports state via a save
  control) — mirroring how LlmSettingsScreen is embedded in the LLM-profiles
  view. The global Agent settings page is unchanged.
- Library: AgentProfilesLocalView (list/create/edit) + manager/body/row/menu +
  delete modal, at the additive route /settings/agents, gated to local
  backends (cloud has no /api/agent-profiles surface yet, epic #3730).
- Maps the form to AgentProfileSaveInput: OpenHands requires an llm_profile_ref
  (via a picker); ACP stores acp_server/acp_model and the command as a shell
  string. Validated end-to-end against a real agent-server.

Part of OpenHands/software-agent-sdk#3713 (Phase 4). An alternative to the
larger #1550.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(agent-profiles): add chat-input agent-profile picker + live in-conversation switch

Adds the full chat integration for Agent Profiles (epic #3713, #3727), keeping
the simplified library/editor from the previous commit:

- New-conversation picker (home): an agent-profile toggle replaces the LLM-
  profile toggle. Selecting activates the profile so the next conversation
  launches from it; conversations start via `agent_profile_id` (resolved
  server-side) instead of an inline agent_settings dump.
- Mid-conversation switch, capability-gated by the running agent:
  - OpenHands conversation → live LLM-profile switch (`/switch_profile`).
  - ACP conversation → live model switch (`set_session_model`, existing
    ChatInputModel).
  - Home / cloud fall back to the agent-profile picker / model picker.
- Threads `agent_profile_id` through the conversation-start path
  (buildStartConversationRequest: agent_profile_id XOR agent_settings; skip the
  ACP tag / encrypted-settings / subscription check on the profile path) and
  reads the server's `launched_agent_profile` provenance to mark the current
  profile without settings-matching.
- Replaces the old SwitchProfileButton/context-menu with the new pickers.

Validated end-to-end against a real agent-server (SDK main): starting a
conversation with `agent_profile_id` returns 201 and stamps
`launched_agent_profile { agent_profile_id, revision }`.

Ported from #1550's chat implementation. Gates green: typecheck, eslint,
prettier, i18n (15 langs), vitest (3496 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(agent-profiles): extract + unit-test buildAgentProfileFields mapping

Addresses the code-review feedback that the profile-fields builder — the ACP
"built-in default command → null vs verbatim shell string" branch plus the
schema-driven tool_concurrency_limit coercion — was the most novel logic in the
PR yet had no automated coverage (every test mocked the embedded form away).

- Extracts the closure into a pure exported `buildAgentProfileFields()` in
  agent-settings.tsx; the embedded control now just snapshots state into it.
- Adds 8 unit tests locking the round-trip: ACP built-in-default → null, custom
  command → shell string, custom preset, blank-model → null, OpenHands
  enable_sub_agents passthrough, concurrency coercion (valid / empty / throws).
- Clarifies the service header (client ships in ts-client 1.28.0; the server
  endpoints it targets shipped in agent-server v1.29.0) per the version-doc nit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent-profiles): address review feedback + fix e2e regression

- Fix mock-LLM E2E regression: the profile-identity spec still targeted the
  removed `switch-profile-button`; point it at the new `chat-input-llm-profile`
  picker (mirrors #1550's e2e update).
- Use the `useRenameAgentProfile` hook in the editor instead of calling the
  service directly (the hook was otherwise dead code; now the rename gets list
  invalidation for free).
- Drop the unreachable in-conversation branch from the home AgentProfile picker:
  the picker only renders on home (a running conversation shows the LLM/model
  picker), so `useChatInputProfileState` is now home-only (activate as launch
  default), and the "start new with profile" hint + its
  CHAT$START_NEW_WITH_PROFILE_HINT key (15 langs) are removed.
- Update the two affected tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent-profiles): correctness fixes from #1571 code review

- switch-llm-profile: run the inline "Switched to" message, #1082 metadata
  persist, and error reporting in mutation-level callbacks so they survive the
  switcher menu unmounting on select
- agent-server-adapter: derive acp_server from agent.acp_server when the
  acpserver tag is absent, so a profile-launched ACP conversation keeps its
  model picker and provider chip
- use-create-conversation: await the LLM-profile list before the
  dangling-llm_profile_ref launch guard so a mid-load send can't launch blind
- chat-input pickers: read switch/activate pending state via useIsMutating so
  the pill button actually disables during an in-flight switch
- use-activate-agent-profile: surface activation errors (drop disableToast) and
  optimistically flip active_agent_profile_id with rollback
- chat-input-actions: fall back to the LLM picker on the home page when the
  backend has no /api/agent-profiles surface

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent-profiles): pass embedded props to reused Agent settings form

The profile editor reused AgentSettingsScreen via the route module's default
export. React Router's Vite plugin wraps a route default with
withComponentProps, which invokes it with route props and drops any props a
parent passes — so `embedded`/`onSaveControlChange` never reached it,
`saveControl` stayed null, and the Save button was permanently disabled
(couldn't create or edit a profile at all).

Split the route into a named `AgentSettingsScreen` export (the reusable
component embedded consumers import) plus a thin default `AgentSettingsRoute`
wrapper, mirroring `LlmSettingsRoute`. The local-view now imports the named
export. Updated the unit-test mock to provide the named export (the old mock
only stubbed `default`, which is exactly what masked this at unit level).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(agent-profiles): un-gate Agent Profiles on cloud backends

The cloud enterprise app-server now exposes the same /api/agent-profiles
contract as the local agent-server (OpenHands #15060, epic #3730), so lift
the local-only gating and route cloud calls through the cloud proxy.

Transport:
- cloud/agent-profiles-service.api.ts: CRUD via callCloudProxy (bearer +
  X-Org-Id) against the identical /api/agent-profiles paths; org resolved
  server-side from the session, so no {org_id} segment.
- cloud/org-profiles-service.api.ts: list org LLM profiles at
  /api/organizations/{org_id}/profiles so the editor's llm_profile_ref
  picker works on cloud. Only listing is cloud-routed.
- AgentProfilesService + ProfilesService.listProfiles branch to the cloud
  transport when the active backend is cloud (mirrors SettingsService).

Surfaces un-gated:
- Settings → Agent profiles nav item + route (no more redirect to /settings/agent).
- Home chat-input agent-profile picker (fetch + pickerKind) on cloud.
- Launch-from-profile: cloud AppConversationStartRequest now carries
  agent_profile_id (added to the type + the cloud create request), which the
  backend resolves and stamps as launched_agent_profile.

In-conversation live switch on cloud is intentionally left on the model
picker for now: the cloud backend has no per-conversation profile-switch
endpoint yet and org LLM-profile detail masks the api_key, so a client-side
switch isn't possible — tracked as a follow-up for full parity.

Tests updated for the new nav behavior (agent-profiles shown on both).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(agent-profiles): update useAgentProfiles docstring for cloud support

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(agent-profiles): clarify cloud in-conversation switch is intentionally local-only

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent-profiles): preserve acp_server through the wire normalizer

An ACP conversation launched from an agent profile (agent_profile_id) showed
a generic chip and an empty in-conversation model picker: the provider
identity never reached the UI.

Root cause: #1571 taught the conversation adapter to source acp_server from
`agent.acp_server` (SDK #3692) when the `acpserver` tag is absent — which is
exactly the profile-launch case, since that path doesn't stamp the tag. But
`normalizeAgent` (the wire parser feeding the adapter) projected only
`{kind, acp_model, llm}` and dropped `acp_server`, so the adapter's fallback
always saw undefined → acp_server null → no ACP provider → generic chip + no
model list.

Add `acp_server` to the normalizeAgent projection (the type already declared
it). Regression test covers the no-tag / agent-sourced path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(agent-profiles): make Settings → Agent the profile library

Collapse the two Settings sections ("Agent" global form + "Agent profiles"
library) into a single "Agent" entry that IS the Agent Profile library: it
lists the user's profiles and its create/edit view is the reused Agent
settings form plus a name (the embedded AgentSettingsScreen). The active
profile is the current agent.

- settings-nav: one "Agent" item → /settings/agents (the library).
- /settings/agent redirects to /settings/agents; default settings path +
  ACP route-guard target updated accordingly.

Also derive the ACP-enabled state from the ACTIVE AGENT PROFILE rather than
settings.agent_settings.agent_kind. Activate is pointer-only and never writes
agent_settings, so the global settings are stale when an ACP profile is
active; the nav-disable, home ACP context, useLlmConfigured, and the ACP
route guard now read the active profile (new useActiveAgentProfile hook) and
fall back to settings only while the profile list is loading.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent-profiles): gate the LLM-setup banner on the active agent profile's LLM

useLlmConfigured decided "is the LLM ready" from the standalone active LLM
profile, but conversations now launch from the active AGENT profile. For an
OpenHands profile the relevant LLM is the one it references via
llm_profile_ref — not whichever LLM profile happens to be "active". So the
"Your LLM isn't set up" banner could be wrong in both directions (e.g. the
active LLM profile has a key but the agent profile references a keyless one).

Resolve the LLM profile to check from the active agent profile's
llm_profile_ref (openhands), falling back to the active LLM profile only when
there's no ref yet. ACP agent profiles stay always-configured (subprocess
owns its LLM). New unit test covers the discriminating case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(agent-profiles): relabel LLM profile "Active" → "Default"

The active LLM profile no longer drives new conversations (the active AGENT
profile does) — it's just the default llm_profile_ref seeded into new agent
profiles. Relabel the LLM-profile badge "Active" → "Default" and the row
action "Set as active" → "Set as default" to stop implying it launches
conversations. New i18n keys (SETTINGS$PROFILE_DEFAULT / _SET_DEFAULT, 15
langs). The agent-profile "Active" badge is unchanged — that one IS active.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(onboarding): land the user's choice on the active agent profile

Onboarding configured global agent_settings + an LLM profile (OpenHands) or
ACP secrets, but never touched an AGENT profile — so the active agent profile
stayed the seeded `default` (openhands → ref `default`), disconnected from what
onboarding set up. Result: an OpenHands user who entered a key still hit "LLM
isn't set up" (the active agent profile referenced a keyless profile), and ACP
users never got an ACP agent profile at all.

Add useApplyOnboardingAgentProfile: upsert + activate the well-known `default`
agent profile from the onboarding choice. The OpenHands LLM step now points it
at the LLM profile it just created; the ACP secrets step makes it an ACP
profile for the chosen provider (opus[1m]/valid default, no LLM key needed).

Verified e2e: OpenHands onboarding → default agent profile refs the configured
LLM + banner clears; Claude Code onboarding → default agent profile is
acp/claude-code, active, no LLM required.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: drop unused eslint-disable in onboarding agent-profile hook

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(agent-profiles): gate mutate controls for cloud view-only members

Reuse #1532's org-permission gating for the Agent Profiles UI. Agent
profiles are org-scoped on cloud (bearer + X-Org-Id, edit_org_settings),
so a cloud member previously saw Add/Edit/Delete/Set-active controls that
would 403 server-side — the same flash-then-403 problem #1532 fixed for
LLM profiles.

- Generalize useCanManageLlmProfiles -> useCanManageOrgProfiles (it reads
  the generic edit_org_settings permission; local users always true).
- Thread canManage through AgentProfilesManager -> Body -> Row, mirroring
  LlmProfilesManager: hide the Add button and the row actions menu for
  view-only members.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: fix stale comments surfaced by PR review

Comment-only. No behavior change.

- chat-input-actions.tsx: the pickerKind summary claimed "cloud → model
  picker (cloud has no profile surface)", contradicting the code, which
  uses the AgentProfile picker on cloud home too (#15060). Rewrite to
  match the actual cases; trim the duplicated render-site recap.
- acp-route-guard.ts / settings-nav.tsx / settings.tsx: the ACP redirect
  target moved to /settings/agents (plural) in this PR, but three
  docstrings still said /settings/agent. Update them.

* test(mock-llm-e2e): wire the active agent profile to the mock LLM

Fixes the mock-LLM e2e regression where the home composer stayed blocked
(submit disabled / launcher never ready) so conversation-launching specs
timed out. Conversations now launch from the active AGENT profile (#1571),
and `useLlmConfigured` follows that profile's `llm_profile_ref` — not the
active LLM profile. The specs seed `openhands-onboarded` and configure an
LLM profile the old way, so the seeded "default" agent profile still
pointed at a keyless LLM and the composer never unblocked.

Mirror what onboarding does for a real user: after activating the mock LLM
profile, upsert + activate the "default" agent profile referencing it. Add
a shared `ensureMockLLMAgentProfile` helper (called from ensureMockLLMProfile
and from the conversation spec, which sets up inline).

Verified locally: the full mock-llm-conversation spec passes 4/4 (real
conversation runs against the mock LLM) with this change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent-profiles): address PR #1571 review feedback

Human review (VascoSch92):
- useLlmConfigured: fall back to the active LLM profile when the active agent
  profile's llm_profile_ref is stale/absent, mirroring the launch-time fallback
  in useCreateConversation. Without this the two contradicted each other: launch
  succeeded via the fallback but the hook reported unconfigured and spuriously
  disabled the composer + banner (even inside a running conversation). Adds a
  regression test for the stale-ref scenario.
- Drop the dead launched_profile plumbing (wire parse + types + adapter map):
  it had zero readers (the home picker keys off active_agent_profile_id and the
  in-conversation picker is LLM/model by design), so the "Consumed by the
  picker" comments were misleading.
- Point the remaining /settings/agent links at /settings/agents (ACP model
  context, chat-input model state, chat error re-auth, command menu) so the
  route rename doesn't cost an extra redirect hop.

/codereview-roasted:
- Extract the triple-nested pickerKind ternary into a pure, unit-tested
  resolvePickerKind() helper.
- Document why cloud OpenHands onboarding intentionally does not repoint the
  active agent profile (persistAsProfile is local-only; cloud resolves the
  agent-profile/LLM wiring server-side).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(agent-profiles): scope the profile-launch enrichment gap (#1571 review)

- Document at buildStartConversationRequest that the profile path relies on the
  server/SDK to restore exec tools + public skills (software-agent-sdk#3967),
  and that canvas_ui + the RUNTIME_SERVICES suffix are intentionally canvas-only.
- Point the createConversation positional-args TODO at the tracked issue (#1587).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent-profiles): preserve unmodeled fields on edit-save + await profiles at launch

Two fixes from the #1571 review:

Edit-save wiped every profile field the minimal editor doesn't model
(condenser, verification, system_message_suffix, skill/MCP refs, embedded
skills, ACP session mode/timeout): the save endpoint is a whole-profile
overwrite, and the editor posted only its own fields. The save payload now
spreads the stored profile under the edited fields via a pure, kind-aware
mergeAgentProfileSaveInput — a kind switch stays a clean variant replacement
(the server's extra="forbid" union rejects mongrel payloads), and
server-managed identity (id/name/revision) is stripped. The edit fetch now
uses X-Expose-Secrets: encrypted so any skills[].mcp_tools values round-trip
as Fernet tokens instead of persisting the mask literally (same pattern as
the LLM-profile editor).

Launch raced the agent-profiles query: useCreateConversation read the hook's
maybe-unresolved data, so a send fired before the list loaded fell through to
the stale global agent_settings path — which activation (pointer-only) never
updates — and silently launched the wrong agent. The launch now awaits the
list via queryClient.ensureQueryData on the shared query key (mirroring the
LLM-profile ref validation below it), with retry: false so backends without
the surface degrade to the legacy launch immediately. The dangling-llm-ref
downgrade also logs a console.warn so the silent fallback is diagnosable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(acp): drive ACP spec through the Agent Profile editor, not the retired route

Settings → Agent is now the Agent Profile library (#1571): the standalone
/settings/agent form redirects to /settings/agents, whose editor reuses the
same embedded agent-settings-screen form. The ACP mock-llm spec and the
resetToOpenHandsAgentViaUI cleanup helper still navigated the old route and
waited on the retired agent-save-button, so they timed out — and the cleanup
helper's failure (swallowed by afterAll's try/catch) left the "default" agent
profile stuck in ACP mode, poisoning downstream specs that share the backend.

- Add openAgentProfileEditor(page, name): navigate /settings/agents, open the
  named profile's editor via its row action menu (row located by the
  profile-name span[title], mirroring activateProfileViaUI).
- Rewrite resetToOpenHandsAgentViaUI to drive the new editor (switch kind →
  OpenHands, pick an LLM profile, save via save-agent-profile-btn).
- Point ACP spec steps 1 & 2 at the editor; swap agent-save-button →
  save-agent-profile-btn.
- Verify step 1 against GET /api/agent-profiles/default (the new source of
  truth) instead of legacy /api/settings; acp_command is a shell string there
  (ts-client AgentProfile.acp_command: string | null), not a token array.

* fix(build): keep the styling core in one chunk to avoid a tv() init-order crash

This PR's new imports grew/shifted the auto-split `vendor` chunk enough that
Rolldown's size-based splitter (`maxSize`) sliced the styling core apart —
separating a HeroUI component's top-level `tv()` recipe from tailwind-variants'
core within the emitted init order. The recipe then evaluated before
tailwind-variants initialized, throwing `TypeError: s is not a function` at
module load. React Router reported "Error loading route module root-layout,
reloading page", looped, and rendered a blank page — deterministically crashing
the whole app and failing 13 mock-llm-e2e specs (npm + docker) that load the
shell.

Give the styling core (@heroui/react + tailwind-variants + tailwind-merge +
clsx) its own group that is never size-split, so it initializes as a coherent
unit before any consumer's top-level `tv()` call. Verified locally: the home
route renders (was a blank page) with zero console errors.

* test(mock-llm): make LLM-profile setup idempotent and fix stale ACP launch assertion

With the crash fixed, the app renders and a second class of failure surfaced:
specs that call `ensureMockLLMProfile` after the first one deadlocked on a stuck
"Delete Profile" modal, and the ACP spec's payload assertion checked the old
launch shape.

- ensureMockLLMProfile: create the mock LLM profile only when absent instead of
  delete-then-recreate. Once the active agent profile references it (wired right
  after, via ensureMockLLMAgentProfile — #1571), the LLMProfile FK guard rejects
  deletion; the delete-confirm modal then silently stays open and its backdrop
  blocks every later click (`add-llm-profile` timed out across files, home,
  automations, mcp, model-switch, preset-automation). The mock config is
  deterministic, so reusing an existing same-named profile is correct.
  deleteProfileIfExists is unchanged — it still works for the non-referenced
  profiles that other specs delete.
- mock-llm-acp-agent step 3: conversations now launch from the active
  AgentProfile (#1571), so the POST /api/conversations payload carries
  `agent_profile_id` and omits `agent_settings` (mutually exclusive, per
  agent-server-adapter). Assert that shape instead of the retired
  `agent_settings.agent_kind`; the ACP reply-token check still proves the ACP
  agent ran.

* ci: degrade gracefully when the linked SDK reference isn't a PR

"Resolve linked SDK PR" (mock-llm-docker-e2e.yml) greps the PR description
for OpenHands/software-agent-sdk#NNNN or .../pull/NNNN and tries to build
against that PR's branch. GitHub's "#NNNN" shorthand looks identical for
issues and PRs, so a description that links a tracking issue (e.g. #3713)
matches the same regex — and /pulls/{number} 404s for an issue number,
failing the whole job under `bash -e` instead of falling back to the
released SDK version like the "no match" branch already does.

Treat a failed PR lookup the same as "no linked PR found": log and exit 0,
leaving git_ref unset so the job falls through to the released version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(test): make ensureMockLLMProfile/AgentProfile converge, not skip

Two real e2e failures traced to test-helper bugs surfaced only once #3968
(SDK 1.32.0) let profile-launched conversations actually run:

- ensureMockLLMProfile: the earlier idempotent-reuse fix (deadlock guard
  against the LLMProfile FK constraint) skipped writing the profile's
  config entirely whenever a same-named profile already existed —
  correct for repeat calls with the SAME config, but silently ignored a
  DIFFERENT one. mock-llm-image-upload requests a vision-capable model
  ("openai/gpt-4o") to get past the mock LLM's default; when an earlier
  spec in the same CI run had already created "mock-llm" with the
  default model, the override never applied and the agent replied "the
  currently selected model does not support image understanding" —
  confirmed via the CI screenshot. Fixed by editing the existing profile
  in place (via the LLM settings UI's Edit flow, never deleting it) so
  every call converges on the requested model/apiKey/baseUrl regardless
  of what an earlier test left behind.

- ensureMockLLMAgentProfile: OpenHandsAgentProfile.skill_refs defaults to
  `[]` (none discovered) when omitted from the save payload. Workspace-
  scoped project skills are discovered independently of this and keep
  working, but a profile-launched conversation's agent never sees any
  public/preset skill (e.g. an installed automation's bundled skill)
  without an explicit skill_refs. Set it to `null` (all discovered),
  matching what a real onboarding-seeded profile effectively gets.

mock-llm-model-switch step 2's post-switch reply timeout is left
unaddressed: its trajectory hard-codes one padding turn for "the
agent-server's internal condenser/skill-analysis call before the main
loop" (a documented, historically-fragile assumption per the test's own
comment) — plausibly now off by one now that #3968 lets the agent make
additional real tool-use calls around a /model switch. Fixing this
requires an empirical trajectory-turn count from a real 1.32.0
conversation trace, which needs a CI cycle to observe correctly rather
than guessing blind.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* revert(test): drop skill_refs=null from ensureMockLLMAgentProfile

CI showed this regressed mock-llm-skills.spec.ts (project skill in
workspace/.agents/skills/), which passed before this change: fixed the
narrow preset-automation slash-command skill-activation case at the
cost of breaking a more fundamental, previously-solid #3968 validation
— a net-negative trade, not a clean win.

The shared "default" agent profile backs every spec in the suite;
widening its skill_refs to "all discovered" has global blast radius
across unrelated tests, evidently including some interaction with
project-skill discovery/activation tracking that isn't understood yet.
A fix for preset-automation's specific skill needs to be scoped to that
one profile/test, not applied to the profile every other spec shares.

Keeps the ensureMockLLMProfile edit-in-place fix (proven, isolated,
fixes mock-llm-image-upload with no observed side effects).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent-profiles): default new profiles' skill_refs to "all discovered"

OpenHandsAgentProfile.skill_refs defaults to `[]` (none) server-side when
omitted from a save payload. Neither onboarding's profile seed nor the
Settings "Add Agent Profile" editor exposes a skill_refs control, so every
newly-created profile silently gets zero public/user/project skills — a
profile-launched conversation's agent can't activate any of them (#1571
launches conversations from the active agent profile). This is exactly
the mock-llm-preset-automation regression: a slash-command-triggered
skill never activates because the "default" test profile has no
skill_refs, matching what a real user's fresh profile would also hit.

useSaveAgentProfile is the single choke point for every profile save
(onboarding seed + Settings create/edit), so default skill_refs to `null`
("all discovered") there whenever the caller hasn't set it explicitly —
matches what users actually expect (a new agent has access to their
skills unless deliberately scoped down) and requires no SDK change. The
pinned typescript-client doesn't type skill_refs on AgentProfileSaveInput
yet (SDK/wire drift), so this reaches it via an untyped merge; `in`
checks the runtime object since mergeAgentProfileSaveInput's edit-preserve
spread can carry it at runtime despite the missing type.

Re-applies the equivalent default to ensureMockLLMAgentProfile (the e2e
test helper bypasses this hook via a raw fetch) so the test suite mirrors
real behavior.

Verified: full unit suite green (3618 passed), typecheck clean,
agent-profiles-local-view.test.tsx passes unaffected (it mocks
useSaveAgentProfile at the hook boundary, so this change is invisible to
it). Locally reproduced the fix: mock-llm-preset-automation's slash-
command skill-activation test now passes; mock-llm-image-upload
(previously fixed) still passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent-profiles): self-heal LLM profile stream=true for profile-launched conversations

A profile-launched conversation (agent_profile_id) never sends
agent_settings, so PR #1474's `llm.stream = true` (buildConfiguredOpenHands
AgentSettings, agent-server-adapter.ts) never reaches it — the referenced
LLM profile's stored `stream` field (SDK default: false) is used as-is by
resolve_agent_profile/_build_openhands_settings, with no override, unlike
the legacy path.

The agent-server decides once, at conversation construction, whether to
wire the `on_token` streaming callback — based on whether any of the
agent's LLMs has stream=True at that moment — and never re-evaluates it
afterward (confirmed by reading LocalConversation.switch_llm: it swaps the
LLM but never touches _on_token). So a profile-launched conversation whose
LLM profile was never saved with stream=true gets on_token=None for its
entire lifetime. switchProfile's switch_llm call (unconditionally sending
stream: true, unchanged by this fix) then crashes the next completion with
"Streaming requires an on_token callback", since on_token can never be
(re-)wired post-construction. Confirmed via real agent-server tracebacks in
both mock-llm-e2e and mock-llm-docker-e2e CI runs.

Streaming is a pre-existing, independently-shipped feature (PR #1474) that
must not regress for legacy-launched conversations — ruling out simply
dropping switch_llm's stream:true (would silently disable streaming after
a switch for the one case that works today). And since existing users'
LLM profiles predate this fix, defaulting stream:true only at future
profile-save time (mirroring the skill_refs fix) would still crash on
their first profile-launched conversation post-deploy.

ensureLlmProfileStreams is a migration shim: at the one call site
guaranteed to run for every profile-launched conversation (already
fetching the LLM-profiles list to validate llm_profile_ref exists), check
the referenced LLM profile's full config and, if stream isn't already
true, save it with stream:true — self-healing both new and existing
profiles on first use, memoized per profile name for the session so it's
a no-op read on every subsequent launch. Mirrors the profile-duplicate
flow's exact pattern for round-tripping the encrypted secret
(getProfile(name, "encrypted") + saveProfile(..., include_secrets: true))
so the stored api_key is never clobbered. Touches neither the legacy
agent_settings path nor switch_llm — both keep working exactly as before.

Safe to delete once virtually all users are migrated, or once
resolve_agent_profile forces stream=true for OpenHands profiles upstream
(same category of fix as the skill_refs default — likely the same #3967
umbrella), whichever comes first.

Verified: full unit suite green (3620 passed, +2 new tests exercising
this exact self-heal/no-op branching), typecheck clean. Locally
reproduced the fix: mock-llm-model-switch's on_token crash no longer
occurs; preset-automation and image-upload remain passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(agent-profiles): link the skill_refs/streaming shims to their tracking issue

References OpenHands/agent-canvas#1619 (the cleanup-tracking issue for both
workarounds) and the specific upstream SDK issues, so the removal criteria
is discoverable from the code itself, not just the PR description.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent-profiles): drop skill_refs/streaming migration shims (SDK #4017 landed)

software-agent-sdk#4017 (PR #4018) fixes both gaps these shims worked
around: OpenHandsAgentProfile.skill_refs now defaults to null (all
discovered) server-side, and the agent-server forces llm.stream=true
for profile-launched conversations. Both shims are now dead code.

Validated end-to-end against the SDK branch (OH_AGENT_SERVER_LOCAL_PATH)
before removing: real HTTP round-trips confirmed skill_refs defaults to
null and the launched agent's LLM streams even though the underlying LLM
profile is stored with stream=false; the full mock-llm-skills.spec.ts and
mock-llm-profile-management.spec.ts suites pass unchanged.

Removes:
- withDefaultSkillRefs (src/hooks/mutation/use-save-agent-profile.ts)
- ensureLlmProfileStreams + its two dedicated tests
  (src/hooks/mutation/use-create-conversation.ts)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(agent-profiles): correct comments for the disabled_skills deny-list

SDK #4017 replaced the profile's skill_refs allow-list (and embedded skills)
with a disabled_skills deny-list. Canvas is already deny-list-native — the
user-level disabled_skills UI exists and the generic profile merge carries the
field automatically — so only two stale comments referencing embedded skills /
skill refs needed correcting. No functional change; the per-profile skill
picker stays out of scope for the minimal editor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(agent-profiles): drop stale skill_refs from fixtures for the deny-list

SDK #4017 replaced the profile's skill_refs allow-list (and embedded skills)
with a disabled_skills deny-list. Update the fixtures/comments that still
referenced the removed fields (they ride untyped through `as unknown` casts /
raw POST bodies, so the generic merge round-trips them regardless):
- merge-agent-profile-save-input.test.ts + agent-profiles-local-view.test.tsx:
  skill_refs -> disabled_skills, drop embedded `skills`, schema_version 3,
  ACP fixtures drop the skill field (ACP has none). Correct the stale
  exposeSecrets/mcp_tools comment (profiles are secret-free now).
- mock-llm-helpers.ts: the omitted-field comment now describes the deny-list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(agent-profiles): profile fixtures use the v1 baseline schema_version

SDK #4017 collapsed the pre-ship AgentProfile schema history to a clean v1
baseline (no v2/v3, no migrations). Update the two profile fixtures to
schema_version: 1 to match the shipped model.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent-profiles): launch the `default` profile via agent_settings; stamp the launched LLM ref

The seeded `default` agent profile is the enriched baseline that mirrors global
agent_settings, not a deliberate profile pick. Launching it via `agent_profile_id`
made the server rebuild the agent purely from the profile, dropping the canvas-only
enrichments the profile-resolution path can't carry — the `<RUNTIME_SERVICES>`
system-message suffix, the `canvas_ui` tool, and project-skill loading. Route the
well-known `default` profile through the agent_settings launch instead; named
profiles are deliberate custom configs and keep the profile path. Fixes the
mock-llm-docker-e2e automation RUNTIME_SERVICES failure.

Also from #1571 review:
- Stamp the launched OpenHands profile's `llm_profile_ref` into conversation
  metadata (not the standalone active LLM profile) so the switcher pill names the
  exact profile the conversation runs when the two differ (#1082).
- Add `retry: false` to the LLM-ref validation fetch, matching the sibling
  agent-profiles fetch, so a slow/erroring /api/profiles falls back promptly.

Hoist the well-known name to `WELL_KNOWN_DEFAULT_AGENT_PROFILE_NAME` (shared by the
launch path and onboarding). Re-onboarding intentionally overwrites `default`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent-profiles): gate the LLM-setup banner on the active agent-profile load

`useLlmConfigured` derives `isAcpAgent` and the referenced LLM from the active
agent profile but omitted that query's loading state from `isLoading`. On a cold
cache an ACP agent (which needs no key) briefly read as an unconfigured OpenHands
agent, flashing the "LLM not set up" banner until the profiles query resolved.
Thread the `useActiveAgentProfile` loading signal into the indeterminate state so
consumers render nothing until the active agent profile is known (#1571 review).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent-profiles): scope the default→agent_settings launch to OpenHands profiles

The `default`→agent_settings shortcut (which preserves <RUNTIME_SERVICES>/canvas_ui)
must not apply to an ACP `default` profile: activation is pointer-only, so global
agent_settings is stale (still OpenHands) when an ACP profile is active — routing it
via agent_settings launched the wrong agent (mock-llm-acp-agent.spec.ts step 3
expected agent_profile_id, got OpenHands agent_settings). ACP also carries no
<RUNTIME_SERVICES>/canvas_ui enrichment, so there's nothing to preserve. Gate the
shortcut on agent_kind === "openhands"; ACP defaults keep the profile path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent-profiles): address PR #1571 review findings (VascoSch92)

- Gate the default-profile agent_settings downgrade to local backends
  only; cloud always launches from the resolved agent_profile_id.
- Emit an explicit schema-default (not an omitted key) when
  tool_concurrency_limit is cleared, so edit-save actually resets it.
- Restore the tailored "Switched to {name} failed" toast via
  meta.disableToast + a dedicated onError.
- Share one AGENT_PROFILES_RETRY_OPTIONS constant across the launch
  path, redirectIfAcpActive, and useAgentProfiles so retry policy
  can't drift between call sites.
- Fix a stale comment on optimisticActiveProfile's write path.
- Self-heal a dangling llm_profile_ref in the agent-profile editor by
  validating it against the live LLM-profiles list on load.

* fix(agent-profiles): restore cloud in-conversation LLM-profile switching

resolvePickerKind hard-coded cloud conversations to the read-only
model picker, on the premise that cloud has no per-conversation
switch endpoint. That's not true: POST
/api/v1/app-conversations/{id}/switch_profile has existed since
OpenHands#14288 (2026-05-05), predating this PR, and the frontend
plumbing to call it (AgentServerConversationService.switchProfile's
cloud branch) was already implemented and just unreachable.

Cloud OpenHands conversations now resolve to the LLM-profile picker,
same as local, matching how ACP already behaves identically on both
backends. main's old SwitchProfileButton had no cloud gate either, so
this restores previously-working behavior rather than adding new
scope.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 17:22:55 +00:00
c50d049316 chore: bump typescript-client 1.32.1 + agent-server/openhands-sdk 1.33.0 (#1623)
* chore: bump typescript-client 1.32.1 + agent-server/openhands-sdk 1.33.0

- @openhands/typescript-client 1.32.0 -> 1.32.1 (package.json + lockfile)
- agent-server SDK libs (openhands-sdk/tools/workspace/agent-server) 1.32.0 -> 1.33.0 via versions.agentServer in config/defaults.json
- sweep doc/test/comment references to 1.33.0 (AGENTS.md, dev-safe.test.ts, check-sdk-version-sync.mjs, dev-safe.mjs, mock-llm-e2e.yml)
- keep the acp <0.11 transitive pin: openhands-sdk 1.33.0 still requires agent-client-protocol>=0.10.1 with no upper bound

Note: check-sdk-version-sync fails until an openhands-automation release pins openhands-sdk 1.33.0. The latest automation (1.1.3) still pins 1.32.0, so versions.automation is left at 1.1.3.

* chore: bump openhands-automation 1.1.3 -> 1.1.4 (pins SDK 1.33.0)

openhands-automation 1.1.4 is now published on PyPI and pins
openhands-sdk/openhands-workspace to 1.33.0, matching versions.agentServer.
This satisfies the check-sdk-version-sync check, which failed while
automation stayed at 1.1.3 (pinned SDK 1.32.0). Consolidates the
SDK+automation bump from #1622 into this PR alongside the ts-client bump.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Debug Agent <157206163+simonrosenberg@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 14:54:14 +00:00
Vasco Schiavo d8f7097112 test(e2e): deflake mock-LLM profile activation helper (#1580) 2026-07-08 16:17:00 +02:00
simonrosenbergandClaude Opus 4.8 ebeaca4f4d chore: bump agent-server SDK to 1.33.0 and automation to 1.1.4 (#1622)
* chore: bump agent-server SDK to 1.33.0 and automation to 1.1.4

Bump config/defaults.json pins:
- versions.agentServer 1.32.0 -> 1.33.0
- versions.automation  1.1.3 -> 1.1.4

Everything else (dev-safe.mjs, docker.yml, mock-llm workflows) reads these
from defaults.json. Updated the dev-safe.test.ts expectations and the two
concrete AGENTS.md version references to match.

The agent-client-protocol<0.11 guard stays: openhands-sdk 1.33.0 still pins
agent-client-protocol>=0.10.1 (unchanged from 1.32.0), so acp 0.11.0 would
still break the ACP client.

Blocked until openhands-automation 1.1.4 (pinned to SDK 1.33.0) publishes to
PyPI, since the sdk-version-sync check resolves the released automation's SDK
deps. Draft until then.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: sync remaining 1.32.0 version examples to 1.33.0

The drift-detection test (docs-version-sync) requires JSDoc examples in
scripts/dev-safe.mjs and scripts/check-sdk-version-sync.mjs to match the
config/defaults.json agent-server pin. Also refresh the acp-constraint
comments in mock-llm-e2e.yml and defaults.json for consistency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 11:54:38 +02:00
Juan Micheliniandopenhands e20f983773 Improve vision inspection event titles (#1573)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-07-07 17:02:30 +00:00
Hiep Leandallhands-bot 27111e154d feat: show locally-discovered plugins as a read-only Local group (#1616)
* feat: show locally-discovered plugins as a read-only Local group

* chore: Remove PR-only artifacts

---------

Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
2026-07-07 16:38:54 +00:00
Hiep Leandallhands-bot 3e40f458aa fix: enrich conversation_created PostHog event (#1614)
* fix: enrich conversation_created PostHog event

* chore: Remove PR-only artifacts

---------

Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
2026-07-07 16:25:17 +00:00
Hiep Leandallhands-bot ddcf5881d2 fix: don't gate sidebar navigation on email verification (#1613)
* fix: don't gate sidebar navigation on email verification

* chore: Remove PR-only artifacts

---------

Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
2026-07-07 23:12:50 +07:00
Hiep Leandallhands-bot 8a4be25a7a feat: add Debug with OpenHands button to failed automation runs (#1603)
* feat: add Debug with OpenHands button to failed automation runs

* chore: Remove PR-only artifacts

---------

Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
2026-07-07 22:59:12 +07:00
Hiep Le a4abde8e90 fix: render telemetry consent banner above the onboarding modal (#1602)
* fix: render telemetry consent banner above the onboarding modal

* refactor: remove unrelated file
2026-07-07 22:41:11 +07:00
Graham Neubigandopenhands c552545926 feat(mcp): add OAuth support to MCP install flow
Squash merge PR #1583.

This merge commit was created by an AI agent (OpenHands) on behalf of Graham Neubig.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-07-07 12:03:36 +02:00
Ash Clarkeandopenhands d2cfffb7ef feat: expand long command outputs inline (#1577)
* feat: expand long command outputs inline

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

* chore: add PR screenshots

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

* chore: narrow command output PR test scope

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

* fix: preserve formatted command output

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

* fix: reuse code block for expandable output

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

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-07-07 09:07:10 +02:00
bd15cea151 [codex] Show MCP server names on installed cards (#1429)
* Show MCP server names on installed cards

* test(mcp): cover stdio command-fallback in getInstalledServerTitle

---------

Co-authored-by: neubig <398875+neubig@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-07-06 18:57:07 +00:00
Hiep Le 2e63845a3f chore: bump software-agent-sdk to 1.31.1 and automation to 1.1.2 (#1609)
* chore: bump software-agent-sdk to 1.31.1 and automation to 1.1.2

* fix: pin agent-client-protocol <0.11 and sync version docs
2026-07-07 00:02:46 +07:00
Hiep Le eb47f59961 feat: reset LLM profile to active from the edit modal (#1588) 2026-07-06 22:19:55 +07:00
Vasco Schiavo 4088a23bcb fix(backends): quick-retry a transient backend health probe (#1582)
The connectivity indicator and the onboarding "backend connected" banner only
flip green once a probe succeeds. useBackendsHealth re-probes each backend only
every REFRESH_INTERVAL_MS (10s) with retry: false, so a single transient
first-probe miss -- the agent-server still warming up right after navigation, a
momentary proxy 5xx, a dropped connection -- leaves the banner stuck for a full
10s until the next scheduled refetch. That is the root cause of the flaky
mock-llm-e2e "onboarding backend health probe should report connected" timeout.

Probe with a bounded quick retry (2 x 300ms) inside the query function so a
transient miss recovers in under a second. Retrying inside the query function
rather than via React Query's retry is deliberate: success and failure are
recorded once per settled query, so a logical probe still records exactly one
outcome and the MAX_CONSECUTIVE_FAILURES disabled-cap accounting is unchanged.

Definitive auth failures (logged out, invalid/missing key) are not retried:
they are a decided server response, not a transient miss, so retrying would only
delay surfacing the correct disconnected state and its recovery UI.

Add transient-recovery and no-retry-on-auth unit tests; bump waitFor timeouts on
the failing-probe tests that now retry before settling.
2026-07-04 15:34:57 +00:00
Engel Nystandopenhands aed9a485be Show five lines for long user prompts (#1578)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-07-03 03:14:29 +00:00
Hiep Le 31ff0fe85d feat: instrument PostHog events for automation lifecycle (#1549)
* feat: instrument PostHog events for automation lifecycle

* fix: failing tests
2026-07-03 00:32:32 +07:00
Hiep Le 4470c5c3db feat: emit backend_added PostHog event on user-initiated backend adds (#1548) 2026-07-02 23:04:14 +07:00
Hiep Le b6727865b7 feat: instrument PostHog analytics for the onboarding funnel (#1547)
* feat: instrument PostHog analytics for the onboarding funnel

* test: de-flake onboarding layout probe and profile activation
2026-07-02 22:26:39 +07:00
Hiep Le c7c4166a2b fix: remove first-run analytics consent popup from onboarding (#1545) 2026-07-02 21:05:56 +07:00
Vasco Schiavo 5aeccb878e feat(chat): add /goal slash command with status banner (#1492)
* chore(deps): bump @openhands/typescript-client to 1.27.0

* test: update ACP provider/model fixtures for typescript-client 1.27.0

1.27.0 refreshed the claude-code/codex ACP registry data: provider command
versions (claude-agent-acp 0.30.0->0.44.0, codex-acp 0.15.0->0.16.0),
claude-code model ids (claude-opus-4-8->opus[1m], claude-sonnet-4-6->sonnet,
claude-haiku-4-5->haiku) plus a new well-labeled "default" option, and the
codex default (gpt-5.5/medium->gpt-5.5).

Canvas sources these lists from the client registry (closes #740), so the
source was already correct -- only the hardcoded test expectations were
stale. Also relaxed the acp-providers placeholder guard to accept the SDK's
intentional "Default (recommended)" entry.

* chore(deps): bump agent-server/openhands-sdk to 1.29.0

Align the spawned agent-server SDK release train (openhands-sdk,
openhands-tools, openhands-workspace, openhands-agent-server) with the
version @openhands/typescript-client 1.27.0 is validated against
(agent-server 1.29.0-python). Bump the coupled openhands-automation pin
to 1.0.0a12, whose SDK deps resolve to 1.29.0, to satisfy the
check-sdk-version-sync gate. minimumAgentServer compat floor unchanged.

Doc/JSDoc/test references updated to keep docs-version-sync green.

* feat(chat): add /goal slash command with status banner

/goal [--max N] <objective> drives the agent toward an objective through the agent-server goal loop: a judge grades completion each round and re-prompts until done or capped. A live banner shows per-round progress and the judge's verdict while the loop runs; once it ends, the final status renders inline in the conversation so the discussion continues below it.

* chore(chat): clarify /goal comments and rename status testid

Addresses PR review suggestions:
- note GoalStatus.max_iterations is snake_case to match the agent-server wire payload
- explain why the goal-status routing is duplicated across the main and planning WebSocket handlers
- rename the goal-status-content testid from goal-status-banner to goal-status (it also renders inline, not just in the banner)

* fix(chat): keep the live /goal banner in view as the loop advances

The active goal banner renders in the scroll stream, but in-progress goal events are filtered out of renderableEvents, so the bottom-following effect never reacted to it — the live progress banner could sit below the viewport when a goal starts in a long conversation. Feed the active goal status into that effect so it scrolls the banner into view while the user is pinned to the bottom. Adds a ChatInterface test for the behavior.

* fix(chat): hide /goal loop re-prompts from the chat

Each incomplete round, the goal loop injects the SDK FOLLOWUP_PROMPT (and RESUME_PROMPT on resume) as a user message to steer the agent; FOLLOWUP_PROMPT embeds the judge's verdict, which the goal banner already surfaces. The persisted event carries no marker distinguishing these from real user input, so they leaked into the chat as fake user turns. Hide them in shouldRenderEvent by matching the SDK prompt prefixes — a stopgap until the SDK stamps a goal-loop marker on the event.

* feat(chat): add goal stop/resume controls and require an objective

- Bare /goal with no objective now shows an error toast instead of
  silently doing nothing.
- Add Stop and Resume controls to the goal status row. Stop calls
  stopGoal and interrupts the in-flight agent turn, because the backend
  stop only cancels the loop, not the running turn. Resume calls
  resumeGoal and is hidden while a goal is already active.
- Add tests for the interceptor and the loop controls.

* fix(chat): refetch conversation history on return instead of replaying it over the socket

useConversationHistory cached the tail with staleTime: Infinity and was never
refetched, so events the agent produced while the user was on another
conversation — most visibly an active /goal loop, which keeps emitting
user + agent turns server-side — were back-filled one event at a time over the
WebSocket `since` replay. The cached tail's newest timestamp never advanced, so
every return replayed the entire post-first-load history over the socket, and
it got worse the longer the goal had been running.

Refetch the tail on return instead:
- staleTime 0 + refetchOnMount "always" so returning fetches the latest page in
  one batched REST call; keep gcTime (30m) so the cached page still renders
  instantly (no skeleton) while the refetch runs.
- refetchOnWindowFocus false so a focus refetch can't churn the socket.
- Gate the WebSocket connect on the history fetch settling (isFetching, not just
  first-load isPending) so it subscribes once from the freshest event. The
  socket bakes after_timestamp into its URL at connect time and only reconnects
  on URL change, so connecting mid-refetch would pin `since` to the stale tail.

Update the history query tests: drop the staleTime-Infinity assertion, add a
remount-refetches-the-tail test and a config test (gcTime, refetchOnMount,
refetchOnWindowFocus).
2026-07-02 15:48:43 +02:00
Vasco Schiavoandhieptl 9e787557fd feat: support LLM profiles on cloud backends (#1532)
* feat: support LLM profiles on cloud backends

Cloud backends had no access to LLM profiles: the LLM was configured through the flat cloud settings form and the chat composer showed a plain model picker. The cloud app-server already exposes the full profile machinery under /api/v1/settings/profiles, so wire agent-canvas to it.

- ProfilesService branches to a new cloud service (src/api/cloud/profiles-service.api.ts) when the active backend is cloud, mirroring how SettingsService delegates to fetchCloudSettings; the profile hooks and the settings manager UI then work transparently.
- The LLM settings route renders the profile manager for both backends.
- Chat-level switching on cloud: the composer shows the profile switcher, /model lists/switches profiles, and per-conversation switching routes through the app-server's server-resolved /app-conversations/{id}/switch_profile endpoint.

* fix: gate cloud LLM profile management on org role (owner/admin)

Cloud org members (role=member) have VIEW_ORG_SETTINGS only: they may view but not create/edit/rename/delete/activate LLM profiles, which the app-server reserves for owner/admin (EDIT_ORG_SETTINGS). The cloud profile settings page exposed every mutating control to all members — reported in PR review.

Surface the caller's role from the existing GET /api/organizations/{orgId}/me call and add useCanManageLlmProfiles() (local backends always true; cloud only for owner/admin, reusing the /me query so no extra request). The settings profile manager hides Add and the per-row actions menu (edit/rename/duplicate/delete/activate) for members, rendering a read-only list.

Per-conversation profile switching in chat stays available to members: the app-server's /app-conversations/{id}/switch_profile route is not org-permission-gated, so switching one's own conversation is a permitted usage action, distinct from managing the org's profiles.

* fix: read profile-manage permission from the server, with role fallback

Review follow-up: instead of hardcoding the role->permission mapping on the client (role === owner||admin), useCanManageLlmProfiles now reads the server-defined `permissions` from GET /api/organizations/{orgId}/me and gates on `edit_org_settings`. Falls back to the previous role check when an older app-server doesn't return `permissions`, so it keeps working against either backend version.

Backend companion (adds `permissions` to /me): OpenHands/OpenHands#15048.

* fix: enforce LLM-profile permissions server-side via the org-gated routes

Route cloud profile CRUD/activate through /api/organizations/{orgId}/profiles, which require EDIT_ORG_SETTINGS server-side — so a member's mutation is rejected with 403 even on a direct API call, not just hidden by the client gate. Falls back to the ungated per-user /api/v1/settings/profiles route only when no org is bound (legacy keys).

A shared cloudProfilesTarget() picks the base path; get/activate normalize the org shapes (llm -> config / llm_applied). Completes the 'validate on both client and server' review point alongside the client gate (companion: OpenHands/OpenHands#15048 exposes the permission on /me).

---------

Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-07-02 12:16:46 +00:00
35fe8ac4c0 fix: show agent model metadata by default (#1556)
Co-authored-by: orphis <anrahya@orphiss-MacBook-Air.local>
Co-authored-by: Vasco Schiavo <115561717+VascoSch92@users.noreply.github.com>
2026-07-02 07:47:18 +00:00
Vasco Schiavo 8a32096829 fix(chat): de-duplicate streamed intermediate agent messages (#1552)
* fix(chat): de-duplicate streamed intermediate agent messages

With `stream=true` (always on for the OpenHands agent), each agent step
streams its pre-tool-call text as a StreamingDeltaEvent and then arrives
as an intermediate ActionEvent whose `thought` is that same text. The
chat hoists the action's thought into its own message, so the leftover
streaming delta rendered the identical text a second time — every
intermediate message appeared twice (issue #1534).

`handleEventForUI` only reconciled streamed deltas for the final message
(FinishAction / agent MessageEvent). Add the symmetric case for
intermediate actions: when a non-finish ActionEvent reproduces the
current turn's streamed content, clear that content from the delta so the
text renders once via the action's hoisted thought. The delta's
`reasoning_content` is preserved (often the only carrier of reasoning, so
the collapsible "Thinking" section must survive); a delta left empty is
dropped. ThinkAction is excluded — its thought renders via its own path.

* refactor(chat): share streamed-delta reconciliation helpers

Extract the duplicated "current turn's content-bearing streaming deltas"
collection and the streamed-text match (startsWith / segments-in-order)
into `getCurrentTurnContentDeltas` and `matchStreamedSegments`, now used
by both `finalizeStreamingDeltasInPlace` and the new intermediate-action
reconciliation. No behavior change — full suite green.

* fix(chat): harden streamed-thought reconciliation (review fixes)

Adversarial review of the #1534 fix surfaced three defects, all covered
by new tests that fail without these changes:

- Whitespace mismatch: the SDK strips the action's `thought`, so streamed
  `content` with a trailing newline no longer matched and the dedupe
  silently fell through (re-triggering #1534). `matchStreamedSegments`
  now tolerates trailing-trimmed streamed text; shared with finalize.
- Cross-step collection: reconciliation joined every current-turn
  content delta, so an earlier step's still-rendered delta broke the
  match for the next step. Now only the current step's trailing delta
  run is considered (getTrailingContentDeltas).
- Reasoning duplication: keeping the delta to preserve reasoning rendered
  a second "Thinking" section when the action also carried reasoning
  (e.g. Claude extended thinking). The delta is now dropped when
  `getReasoningContent(action)` is non-empty, and kept only to supply
  reasoning the action itself lacks.

* refactor(chat): trim comments and dedupe text-block joining

Condense the explanatory comments in handle-event-for-ui.ts to their essential rationale, and collapse the duplicated getAgentMessageText and getAgentActionThoughtText bodies into one joinTextBlocks helper. Also drop the transient "(BUG n)" markers from the reconciliation test titles. Comments/refactor only — no behavior change.
2026-07-01 16:04:16 +02:00
Tim O'Farrellandopenhands 8bb5623655 test: drop should-not-exist assertions on removed code (APP-2570) (#1554)
Removes three negative-existence test assertions that have decayed into
no-ops because the testids they target no longer exist in source. Each
was added in the same PR that deleted the related component (or, for
login-cta, in the OSS-strip PR) and was useful as a regression guard at
the time. Once the deletion is several weeks old, an assertion of the
form `expect(queryByTestId("deleted-testid")).not.toBeInTheDocument()`
is always trivially true and only adds maintenance noise.

Follow-up to PR #1545 review feedback (tofarr): 'A test that makes sure
that something which was deleted six months ago does not exist. We
should probably have an agent review our tests for cases like this and
remove them.' See APP-2570 for the audit inventory and follow-ups.

* __tests__/routes/device-verify.test.tsx — drop the entire
  'keeps the device verification view OSS-only without login CTA chrome'
  test. The testid 'login-cta' has no source counterpart; the assertion
  was always trivially true. Test was added in #17 (the OSS-strip PR).

* __tests__/routes/agent-settings.test.tsx — drop the orphan
  queryByTestId('acp-credentials-save-button') assertion (and the
  obsolete explanatory comment) from the 'a single Save persists ACP
  credentials' test. The testid has no source counterpart; the rest of
  the test (the single Save flow) is unaffected.

* __tests__/components/features/conversation-panel/conversation-card.test.tsx
  — drop the entire 'should not render the llm model in the conversation
  card' test. The testid 'conversation-card-llm-model' was removed in
  6e545c54 ('Move LLM model name from conversation lists/title to chat
  input'); the inverted test added at the same time is now a no-op.

All three test files still pass. typecheck clean. No new lint issues.
The PR's own new analytics-consent-modal assertion is intentionally kept
— it is load-bearing for the release that removed the modal and matches
the same pattern that PR #1251 used for acp-credentials-save-button
(now itself a candidate for a future cleanup pass; APP-2570 tracks).

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-30 12:34:35 -06:00
d4f825c7f3 [codex] Fix OpenAI subscription device code contrast (#1531)
* Fix OpenAI subscription device code contrast

* chore: Remove PR-only artifacts

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
2026-06-30 17:58:25 +00:00
Tim O'Farrellandopenhands cfa2ba5796 fix(backends): hide edit/remove row actions when locked to cloud (#1555)
The Manage Backends modal previously rendered pencil and trash icons on
every backend row even when the deployment was started with
--lock-to-cloud. In locked mode the user should not be able to mutate
the locked backend's host/key/name or remove it outright.

backend-row.tsx now consults getLockedCloudHost() and skips rendering
both action buttons when a locked cloud host is configured. The row
identity (name + host) still renders so the user can see which backend
is locked.

Adds three focused tests to describe("BackendRow", ...):

  * renders edit and remove buttons when not locked to a cloud host
  * hides edit and remove buttons when locked via VITE_LOCK_TO_CLOUD
  * hides edit and remove buttons when locked via the
    window.__AGENT_CANVAS_LOCK_TO_CLOUD__ runtime global

Also extends afterEach with vi.unstubAllEnvs + window-global cleanup
(consistent with backend-form-modal.test.tsx) and isolates three
pre-existing edit-form tests from a local .env that sets
VITE_LOCK_TO_CLOUD, so they keep exercising edit-form behavior rather
than lock behavior.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-30 09:57:46 -06:00
74f06866ec Use libraries for local proxy and static serving (#1543)
* Use libraries for local proxy and static serving

* Fix CI for proxy library refactor

* Fix static server CI failures

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

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-30 06:07:20 -07:00
Vasco SchiavoandVascoSch92 39bb90b8ef fix(conversation): fast-poll active conversation until title is set (#1508) (#1520)
* fix(conversation): fast-poll active conversation until title is set (#1508)

The header conversation title only refreshed on the slow 30s interval
because useActiveConversation dropped to 30s as soon as conversation_url
was present — which happens before the agent asynchronously generates the
title. Add a fast-poll (3s) trigger while the title is unset and the agent
is actively executing, bounded by isExecutionActive so terminal/paused
titleless conversations don't poll forever.

* docs(conversation): trim verbose comments in title-poll change

---------

Co-authored-by: VascoSch92 <vasco@openhands.dev>
2026-06-29 13:27:03 +00:00
Hiep Le 2acb3a39fc fix: show the connected organization name in the Manage Backends modal (#1530) 2026-06-29 18:23:26 +07:00
Graham NeubigandCodex 09d56a330b [codex] Always show first-run onboarding (#1528)
* Always show first-run onboarding

* Fix first-run onboarding CI regressions

* Fix root onboarding launch navigation

---------

Co-authored-by: Codex <codex@openai.com>
2026-06-29 10:01:05 +00:00
Hiep Le 65040758ab feat: allow changing the LLM profile when editing an automation (#1527) 2026-06-29 13:24:52 +07:00
Hiep Le c399927f17 fix: remove gradient background from command menu modal (#1526) 2026-06-29 13:09:11 +07:00
Hiep Le 3f705def47 fix: render navigation options as links for Open in New Tab (#1524) 2026-06-29 04:58:39 +00:00
Hiep Le e0a483c200 fix: render Slack logo from a bundled icon instead of the removed CDN asset (#1523) 2026-06-29 11:39:50 +07:00
Graham Neubigandneubig e3c096ba83 chore: bump @openhands/extensions 0.6.0 -> 0.7.0 (#1519)
Bump to the released 0.7.0 (OpenHands/extensions#369): defaultTool
removed, every HTTP connector has openApiUrl, MCP-first ordering, 5
vendors recovered as MCP, 10 connectors dropped, strict JSON schema
added. Typecheck passes; 3444 tests pass (1 pre-existing EADDRINUSE
port-flake unrelated to the bump).

Co-authored-by: neubig <398875+neubig@users.noreply.github.com>
2026-06-27 23:13:59 -04:00
Hiep Le a8d29bced8 feat: add plugin picker to the new-conversation flow (#1482)
* feat: add plugins catalog service and usePluginsMarketplace hook

* feat: add plugin picker to the new-conversation flow
2026-06-27 19:59:14 +07:00
Hiep Le 357719c48a feat: show a conversation's attached plugins in the tools menu (#1483)
* feat: show a conversation's attached plugins in the tools menu

* fix: show enabled installed plugins in the conversation plugins view
2026-06-27 17:15:35 +07:00
Hiep Le fd69281129 feat: enable the Plugins navigation entry (#1477)
* feat: add plugins catalog service and usePluginsMarketplace hook

* feat: enable the Plugins navigation entry
2026-06-27 06:33:36 +00:00
b2ba5889d3 fix(examples): inherit acp-docker image from config/defaults.json (#1434)
* fix(examples): inherit acp-docker image from config/defaults.json

examples/acp-docker/docker-compose.yml hardcoded the agent-server image at
`1.25.0-python`. Canvas enforces `compatibility.minimumAgentServer` (1.28.0)
from the repo's single source of truth, so the example default fell below the
floor and rendered "Disconnected — requires 1.28.0 or newer" — a reviewer
following the quickstart as written never reached the feature.

examples/acp-docker was the lone in-repo file hardcoding a version instead of
inheriting from config/defaults.json (14 other files read it; check-sdk-version
-sync only validates the released PyPI package, not in-repo files).

- scripts/gen-acp-docker-env.mjs: read defaults.json, pin AGENT_SERVER_IMAGE to
  `${images.agentServer}:${versions.agentServer}-python` in examples/acp-docker
  /.env (idempotent upsert; mirrors scripts/docker-build.mjs).
- package.json: `npm run example:acp-docker:env`.
- docker-compose.yml: no-config fallback `1.25.0-python` -> `latest-python`,
  always >= the compatibility floor, so zero-config `docker compose up` never
  shows "Disconnected"; the generated .env overrides with the pinned SoT
  version for the reproducible path.
- .env.example / README.md: document both paths; correct the version narrative
  (floor is the defaults.json compatibility pin; #3510 is the deeper functional
  floor at/below it).
- __tests__/scripts/acp-docker-env-sync.test.ts: assert the generator's tag
  matches defaults.json, the pin satisfies the floor, and the compose fallback
  stays `latest-python`. Mirrors docs-version-sync.test.ts — the guard that
  makes "can't silently drift" true.

* test(examples): harden acp-docker env-sync per review

Addresses the cli-review-panel findings worth acting on (the rest were
cosmetic or matched the no-validation idiom of scripts/docker-build.mjs):

- gte() in the test guarded with parseSemver — a non-numeric pin (sha /
  pre-release) now fails the floor check loudly instead of silently
  comparing NaN. The floor check is a CI gate; its one piece of logic
  shouldn't mis-compare in silence.
- compose-fallback assertion derives the registry from config.images
  .agentServer instead of hardcoding ghcr.io/openhands/... — a registry
  change no longer false-fails a test that only cares about the latest-python
  tag.
- upsertEnvLine now has unit tests (append / replace-in-place+preserve /
  idempotent / commented-template-line / keyless-line guard), making the
  "idempotent upsert" claim defensible. It was the one untested piece of real
  logic.
- upsertEnvLine guards a keyless line (no "=") with a clear throw, instead of
  an empty key matching every line and rewriting the whole file.

* fix(examples): guard acp-docker env-sync entrypoint against undefined argv[1]

The CLI entrypoint guard called pathToFileURL(process.argv[1]) unconditionally.
process.argv[1] is undefined in some ESM contexts (e.g. importing the module for
its exports via `node --input-type=module -e "import(...)"`), so the guard threw
ERR_INVALID_ARG_TYPE at import, before any exported helper was reachable.

Short-circuit on process.argv[1] before pathToFileURL so importing the module is
side-effect-free while the CLI path is unchanged. Add a regression test that
reproduces the bare-import context and asserts a clean exit.

Addresses the review finding on #1434.

* docs(acp-docker): trim verbose comments per review

Address all-hands-bot's review suggestions on #1434:
- test header describes the current invariant, not the prior-state history
  (that narration belonged in the PR description)
- docker-compose.yml: condense the image-pin comment to the how-to-override;
  the compatibility-floor / #3510 rationale already lives in README §1 + the test
- .env.example: 7-line pin explainer down to 2

Comment-only; env-sync test still 10/10 green, prettier clean.

* Clarify ACP Docker image version guidance

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

---------

Co-authored-by: enyst <engel.nyst@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-26 23:21:32 +00:00
Hiep Leandallhands-bot 3520cf1821 feat: build the Plugins management page (browse / install / enable-disable / uninstall) (#1476)
* feat: add plugins catalog service and usePluginsMarketplace hook

* feat: build the Plugins management page (browse / install / enable-disable / uninstall)

* fix: lint

* chore: Remove PR-only artifacts

* fix: styling

---------

Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
2026-06-26 19:36:31 +00:00
Rohit Malhotraandopenhands bfd5f04109 Draft: Prepare 1.1.0 release (#1511)
* chore: prepare 1.1.0-rc.1 release candidate

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

* chore: prepare 1.1.0 release

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

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-26 19:22:55 +00:00
Hiep Leandallhands-bot 712013bd31 feat: add plugins catalog service and usePluginsMarketplace hook (#1464)
* feat: add plugins catalog service and usePluginsMarketplace hook

* chore: Remove PR-only artifacts

---------

Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
2026-06-27 01:56:54 +07:00
Hiep Le c3f20e0df5 chore: bump typescript-client 1.28.0 + agent-server/openhands-sdk 1.29.3 (#1507)
* chore: bump typescript-client 1.28.0 + agent-server/openhands-sdk 1.29.3

* chore: automation
2026-06-26 17:52:18 +00:00
37b8bc6e41 Add command-k menu (#1206)
* Add command menu

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

* Add command menu keyboard coverage

* Translate command menu strings

* Stabilize command menu sidebar action

* Stabilize command menu item definitions

* chore: add live evidence for PR #1206 under .pr/

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

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: neubig <398875+neubig@users.noreply.github.com>
Co-authored-by: Graham Neubig <gneubig@users.noreply.github.com>
2026-06-26 12:46:37 -04:00
1294b08944 fix(settings): show ACP-disabled tooltip on desktop (drop pointer-events-none) (#1499)
* fix(settings): show ACP-disabled tooltip on desktop (drop pointer-events-none)

When an ACP agent is active, the desktop settings sidebar greys out the
LLM/Condenser/Verification items and wraps them in a hover StyledTooltip
("Disabled while {agent} is active"). The tooltip never opened, so users
saw a greyed item with no explanation (reported on macOS desktop).

Root cause: the disabled link also got `pointer-events-none`, but
StyledTooltip is HeroUI's pointer-driven Tooltip — with pointer events
suppressed, onPointerEnter never fires and the tooltip can't open.
pointer-events-none wasn't needed for correctness either: onClick already
preventDefaults navigation, and tabIndex=-1 + aria-disabled cover
keyboard/AT.

Fix: keep the item visually greyed (opacity-50) but only apply
pointer-events-none when there's no disabledReason tooltip to show.

Fixes #1498

Co-authored-by: smolpaws <engel@enyst.org>

* chore: Remove PR-only artifacts

* Address ACP tooltip review comment

* Restore sidebar disabled aria comment

---------

Co-authored-by: smolpaws <engel@enyst.org>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-26 01:16:56 +00:00
3f52df2e39 feat(settings): add Cloud link to settings sidebar for cloud backends (#1453)
* feat(settings): add Cloud link to settings sidebar for cloud backends

Add a "Cloud" external link at the bottom of the Settings sidebar that
appears only when the active backend is a Cloud backend. It links to
`{cloudHost}/settings` (opens in a new tab) with an external-link icon,
giving users a quick path to their hosted account/settings page. Local
backends and the no-backend state render nothing.

- New `CloudSettingsLink` component reads the active backend and renders
  only for cloud backends, normalizing the host (trailing slash) before
  appending `/settings`.
- Added to the desktop sidebar, mobile hub, and mobile drawer (next to
  the existing backend-synced badge).
- New i18n key `SETTINGS$CLOUD_SETTINGS_LINK` (allowlisted as a brand
  name since "Cloud" is identical across locales).
- Added unit tests covering cloud/local/no-backend cases and URL building.

Screenshot of the new Cloud button in the Settings window: .pr/settings-cloud-button.png

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

* docs(pr): replace screenshot with correct Settings sidebar view

The previous screenshot captured the manage-backends overlay that
appears for a logged-out cloud backend, not the Settings sidebar.
Re-captured against a connected cloud backend so the Cloud link is
visible at the bottom of the Settings sidebar alongside the nav items.

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

* feat(settings): add cloud glyph to Cloud settings sidebar link

Render the Cloud settings link with a leading cloud icon (lucide `Cloud`)
next to the "Cloud" label, keeping the trailing external-link icon so
users can tell it opens the hosted page in a new tab. Matches the other
iconified rows in the Settings sidebar.

Update the PR screenshot to show the new two-icon layout.

* feat(settings): place Cloud link below Secrets with nav-row styling

Move the Cloud settings link out of the sidebar footer into the nav
list, directly below the Secrets entry and above the synced-settings
badge, so it sits where users expect a settings sub-page to be.

Restyle the link to match the other sidebar nav rows: reuse the shared
sidebar-layout classes (sidebarNavRowClassName + SIDEBAR_ROW_INTERACTIVE
idle, SIDEBAR_ICON_SLOT_CLASS, sidebarNavLabelClassName) so it has the
same height, padding, border-radius, transparent idle background, and
hover background as Secrets/LLM/etc. Drop the bespoke bordered card.
Still renders a leading cloud glyph, the "Cloud" label, and a trailing
external-link icon.

Apply the same placement in the mobile hub and mobile drawer.

* chore: Remove PR-only artifacts

* docs(settings): simplify CloudSettingsLink JSDoc; add PR screenshots

- Apply reviewer suggestion to trim verbose JSDoc (keep only the
  non-obvious note about why local backends are excluded).
- Add the three evidence screenshots referenced in the PR description
  to .pr/ so the Video/Screenshots links resolve on the branch.

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: neubig <398875+neubig@users.noreply.github.com>
Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
2026-06-25 21:16:21 +00:00