* feat(telemetry): add posthog events for install, conversation creation, and prebuilt automation
- canvas_install already fires pre-consent via telemetry.ts
- Add conversation_created to NPM library telemetry (trackEvent) in use-create-conversation.ts onSuccess
- Add prebuilt_automation_enabled event in recommended-automations-launcher.tsx when a catalog automation is successfully launched
- Add prebuilt_automation_enabled event in automations-list.tsx and automation-detail.tsx handleToggle when enabling (not disabling)
- All consent-gated events route through telemetry.ts to PostHog prod via POSTHOG_PROD_KEY baked in by build:lib
Co-authored-by: openhands <openhands@all-hands.dev>
* refactor: align posthog tracking with existing useTracking hook pattern
- Add trackPrebuiltAutomationEnabled to useTracking hook (typed, named,
includes commonProperties like all other tracked events)
- Remove duplicate conversation_created trackEvent call in
use-create-conversation (trackConversationCreated already covers it)
- Replace all trackEvent(telemetry.ts) calls in automation routes and
recommended-automations-launcher with useTracking hook functions
- Add trackPrebuiltAutomationEnabled to useCallback dep array in launcher
Co-authored-by: openhands <openhands@all-hands.dev>
* refactor: implement dual-system PostHog tracking (Option C)
System 1 — telemetry.ts (anonymous, library-level):
- canvas_install and canvas_new_session remain in telemetry.ts
- trackEvent() stays as the public library API (lib/index.ts export)
- Rule: never import trackEvent in app routes/components
System 2 — useTracking hook (identified, app-level):
- Wire posthog_client_key from VITE_POSTHOG_CLIENT_KEY env var in
OptionService.getConfig() so PostHogProvider mounts when key is set
- Document VITE_POSTHOG_CLIENT_KEY in .env.sample
- Add 6 typed tracking functions to useTracking:
trackInitialQuerySubmitted, trackUserMessageSent,
trackDownloadVsCodeButtonClicked, trackSettingsSaved,
trackMcpConfigUpdated, trackDownloadTrajectoryButtonClicked
- All include commonProperties (current_url, user_email) automatically
- Migrate all 6 raw usePostHog()/posthog.capture() call sites:
chat-interface.tsx, conversation-card.tsx, settings-form.tsx,
use-save-settings.ts, use-download-conversation.ts
- Rule: never call posthog.capture() directly from components
Consent unification:
- AnalyticsConsentFormModal now calls setTelemetryConsent() after saving
user_consents_to_analytics so both systems honour the same decision
Documentation:
- AGENTS.md: tracking architecture section with boundary rules,
system ownership table, and instructions for adding new events
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: treat null consent as opted-out in useSyncPostHogConsent
PostHog defaults to capturing enabled on init. The previous hook only
called opt_out_capturing() when user_consents_to_analytics was explicitly
false, leaving a window where events fired while settings were loading
(null) or on first visit before the user had made a decision.
Changes:
- null and false both route to opt_out_capturing() — only an explicit
true opts PostHog in
- Remove hasSyncedRef: the hook now reacts to every settings change,
which correctly handles consent granted in another tab or after the
initial load. handleCaptureConsent is idempotent so repeated calls
are safe.
- Update comment to document the three-state consent model
Co-authored-by: openhands <openhands@all-hands.dev>
* test: add unit tests for tracking and consent changes
New test files:
- use-sync-posthog-consent.test.ts (7 tests)
Covers the null/false/true consent states and reactive re-sync
behaviour. Specifically tests the bug fix: null now opts out.
- use-tracking.test.ts (17 tests)
One test per tracking function. Verifies correct event names,
property mapping (snake_case / SCREAMING_SNAKE_CASE), and that
commonProperties (current_url, user_email) are included.
Also covers git_user_email fallback and null user_email.
- analytics-consent-form-modal.test.tsx (5 tests)
Verifies setTelemetryConsent is called with 'granted'/'denied'
matching the form checkbox state on submission.
- use-save-settings.test.ts (4 tests)
Covers trackMcpConfigUpdated: fires on new config, skips on
absent config, skips on same object reference, handles zero counts.
- use-download-conversation.test.ts (3 tests)
Covers trackDownloadTrajectoryButtonClicked: called on download,
called before the API request, still called even when API rejects.
Updated test files:
- option-service.test.ts
Two new cases for posthog_client_key: returns key from env var
when set, returns null when env var is absent.
- chat-interface.test.tsx
Added vi.mock('#/hooks/use-tracking') to prevent test noise.
New 'Tracking' describe block: verifies trackInitialQuerySubmitted
fires on first message and trackUserMessageSent on follow-ups.
- conversation-card.test.tsx
Added vi.mock('#/hooks/use-tracking') guard.
- settings-form.test.tsx
Added vi.mock('#/hooks/use-tracking') guard.
New test: verifies trackSettingsSaved is called with LLM_API_KEY_SET
and SEARCH_API_KEY_SET on form submission.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix(tests): resolve two CI failures in tracking tests
use-tracking.test.ts: COMMON.current_url was hardcoded as
'http://localhost/' but jsdom in CI runs at 'http://localhost:3000/'.
Change COMMON from a module-level const to a let populated in beforeEach
via window.location.href so it always matches the environment.
chat-interface.test.tsx: trackUserMessageSent was never called (0 times)
because totalEvents is derived from uiEvents via useFilteredEvents, not
from the events array. The test was setting events but leaving uiEvents
empty, so totalEvents stayed 0 and the component took the
trackInitialQuerySubmitted branch instead. Seed uiEvents with a minimal
MessageEvent (has llm_message.role + content, which isMessageEvent and
shouldRenderAgentServerEvent both accept) so totalEvents = 1.
Co-authored-by: openhands <openhands@all-hands.dev>
* style: fix prettier formatting in option-service getConfig
?? null was split onto its own line but the full expression fits
within the 80-char printWidth (79 chars), so prettier requires them
joined on one line.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix(tests): resolve 4 TypeScript type errors in test files
analytics-consent-form-modal.test.tsx:
Spread of unknown[] is not valid — TS requires a tuple type or rest
param. Replace (...args: unknown[]) => mock(...args) with a typed
single-arg forwarder: (consent: string) => mock(consent).
use-save-settings.test.ts:
MCPConfig requires shttp_servers (added in a later migration).
Add shttp_servers: [] to all three fixture objects (full config,
sharedConfig reference-equality test, and zero-count test).
Co-authored-by: openhands <openhands@all-hands.dev>
* fix(tests): type setTelemetryConsentMock to accept a string arg
vi.fn(() => Promise.resolve()) infers zero parameters, so calling
it with a string argument fails TS2554. Add _consent: string to
the implementation so the inferred mock signature matches the call.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix(tracking): add explicit consent guard to useTracking
All tracking functions now require posthog to be initialized AND
user_consents_to_analytics === true before calling posthog.capture().
Previously, events were blocked only via PostHog's opt-out mechanism
(set by useSyncPostHogConsent), leaving three gaps:
1. posthog undefined (no VITE_POSTHOG_CLIENT_KEY) - silent no-op
2. Desync between useSyncPostHogConsent and useTracking can cause
events to fire or be dropped depending on hook ordering
3. No self-documenting enforcement of the documented requirement
that System 2 requires user_consents_to_analytics === true
The new internal track() helper short-circuits when either condition
is unmet, so each tracking function is self-contained and the consent
contract is explicit in the code that enforces it.
Tests: beforeEach now includes user_consents_to_analytics: true; posthog
mock is made variable so the undefined case can be tested; new 'consent
gate' describe block covers all blocked cases (false, null, undefined
settings, no posthog).
Co-authored-by: openhands <openhands@all-hands.dev>
* feat: add usePostHogIdentify hook for cloud-mode PostHog identity
Calls posthog.identify(userId, { email }) for cloud users who have
granted analytics consent. Required because PostHogProvider is
initialized with person_profiles='identified_only', which silently
drops all events until identify() has been called.
Identity lifecycle:
- consent === true + userId present → posthog.identify()
- consent === false (explicit denial) → posthog.reset()
- userId becomes null after identify (logout) → posthog.reset()
- consent === null / settings loading → no-op
userId sourced from useCloudCurrentUserId() (cloud mode only).
Local mode is skipped — no stable server-issued user ID available.
Co-authored-by: openhands <openhands@all-hands.dev>
---------
Co-authored-by: openhands <openhands@all-hands.dev>
* Restore choose-agent as the first onboarding step.
Put agent selection back ahead of backend setup so new users pick an agent before connecting a server, and keep the step-0 Next button right-aligned when Back is hidden.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add onboarding step preview via query param.
Support `?previewOnboardingStep=0-3` to open a specific slide for design review without marking onboarding complete, mounting from the root layout when the param is present.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Redesign onboarding Say Hello step input and footer.
Use the chat-style send control in a raised input container, replace the launch button with Close, and add trailing punctuation to the default hello message across locales.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Scroll recommended automations grid in onboarding Say Hello.
Keep the section heading fixed while only the workflow cards scroll, and use a lighter raised surface on automation tiles for contrast against the modal panel.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Make onboarding backend Next save and align the footer.
Remove the separate Save button, left-align Back with Next on the right, and submit the backend form through Next so a successful connection test advances the flow.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Disable backdrop dismiss on onboarding modal only.
Add a closeOnBackdropClick option to ModalBackdrop and turn it off for new-user setup so outside clicks no longer skip onboarding while other modals keep the default behavior.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Collapse onboarding backend fields when connected and refresh copy.
Hide the configuration form behind a Show configuration toggle once the health probe succeeds, and explain that users can register multiple agent backends.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Left-align Back on the onboarding LLM step footer.
Use justify-between so Back and Next match the layout on the other onboarding steps.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Avoid skeleton flicker during onboarding LLM step transition.
Keep SDK settings content visible on background refetches so clicking Next from LLM setup no longer flashes the loading skeleton.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Polish onboarding footer spacing and suppress setup save toast.
Match the Say Hello footer top/bottom spacing and disable the generic settings-saved toast while advancing through the startup modal LLM step.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Refine final onboarding step actions and scrolling.
Hide Skip on the final slide, add an OR divider in Say Hello, and keep the recommendation heading/description inside the same scrollable area as the automation cards.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Remove extra spacing before onboarding recommendations.
Drop the top margin above the recommended automations block on the Say Hello step so it aligns tightly with the updated scrollable recommendations layout.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor: remove unrelated file
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
* fix(chat): polish pending user message sending and error UX
Move sending/error status below the bubble, left-align the sending label,
and use theme error colors with a compact retry button. Add hover stop
control on in-flight messages so users can dismiss pending sends.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(chat): improve pending send UX and truncate long user messages
Clamp sent user bubbles to three lines with a bottom gradient, pill-style
View more hint (fast fade on hover), and click-to-expand via an overlay
control. Move the in-flight stop control inside the bubble and use the
stop icon.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(chat): refine pending send UX and add dev preview helper
Polish sending/error pending bubbles: adaptive stop button placement, filled stop icon, full-opacity sending state, Thinking-matched status text, and user message spacing. Add dev-only preview seeding via ?previewPendingChat= for mock review.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(chat): restore cancelled sends and stabilize pending bubble layout
Return stopped message text to the input when empty, reserve a fixed stop-button column to prevent text shift, and tighten user bubble vertical padding to balance line-height spacing.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(chat): add flowing gradient to sending status text
Reuse the workbench kanban gradient-flow animation on the pending send label for a subtle live-activity shimmer.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(chat): polish pending send shimmer, stop control, and preview
Add a reusable TextShimmer for the sending label, overlay the stop button
on the bubble with clearer hover feedback, align sending status spacing with
Thinking, and seed a multiline sending case in the dev preview.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
* feat: use agent server APIs for settings persistence
- Replace localStorage with HTTP API for settings storage
- Use `X-Expose-Secrets: encrypted` header for GET /api/settings
to receive encrypted secrets (not exposing raw values)
- Use `secrets_encrypted: true` in start conversation payload
- Add `getSettingsForConversation()` to build encrypted settings
payload for conversation start endpoint
- Update secrets service to use /api/settings/secrets endpoints
- Add mock handlers for settings and secrets API endpoints
- Update tests for new API-based settings flow
This integrates with software-agent-sdk PR #3060
(feat/encrypted-secrets-in-transit) which adds server-side
encryption support for secrets in transit.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: update test mocks for encrypted settings API and add OH_SECRET_KEY support
- Update use-create-conversation-metadata.test.ts to mock getSettingsForConversation()
which is now called by buildStartConversationRequestWithEncryptedSettings
- Skip flaky onOpen websocket test that times out intermittently in CI
- Add OH_SECRET_KEY environment variable support in dev-safe.mjs:
- Uses default key for local development
- Can be overridden via OH_SECRET_KEY environment variable
- Logs secret key source at startup
Co-authored-by: openhands <openhands@all-hands.dev>
* docs: update AGENTS.md for settings API and OH_SECRET_KEY
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: update secrets service to use agent-server API routes
Changes:
- Update SecretsService to use /api/settings/secrets endpoints instead of /api/v1/secrets
- Simplify secrets-service.types.ts to remove unused pagination types
- Update use-get-secrets hook to do client-side filtering (agent-server doesn't support pagination)
- Update mock handlers to only use agent-server API routes
- Update secrets-settings test to mock getSecrets instead of searchSecrets
- Remove pageSize option from useSearchSecrets since agent-server doesn't paginate
The agent-server API routes (per SDK PR #3060):
- GET /api/settings/secrets - List secrets (names/descriptions only)
- GET /api/settings/secrets/{name} - Get secret value
- PUT /api/settings/secrets - Upsert secret
- DELETE /api/settings/secrets/{name} - Delete secret
Co-authored-by: openhands <openhands@all-hands.dev>
* docs: update AGENTS.md for secrets API routes
- Document the agent-server secrets CRUD routes in MSW handlers list
- Update git provider token persistence note to reflect server-side storage
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: update secret name validation to match agent-server requirements
- Change pattern from '^\S*$' (no whitespace) to '^[a-zA-Z][a-zA-Z0-9_]{0,63}$'
- Add title prop to SettingsInput component for validation error messages
- Secret names must: start with letter, contain only letters/numbers/underscores, be 1-64 chars
Co-authored-by: openhands <openhands@all-hands.dev>
* feat: include custom secrets in conversation requests via LookupSecret
Custom secrets configured in Settings > Secrets are now automatically
included in conversation start requests. Instead of exposing secret values
to the frontend, we use LookupSecret entries that point to the agent-server
endpoint /api/settings/secrets/{name}. The agent-server fetches the actual
values at runtime.
Changes:
- Add LookupSecret interface to agent-server-adapter.ts
- Add customSecrets option to StartConversationOptions
- Build LookupSecret entries for each custom secret in buildStartConversationRequest
- Update buildStartConversationRequestWithEncryptedSettings to fetch and include
custom secrets list from SecretsService.getSecrets()
- Include X-Session-API-Key header in LookupSecret when configured
This ensures secrets never touch the frontend in plaintext while still
making them available to conversations.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: address review comments - no localStorage fallback, retry logic, SDK docs
Review feedback addressed:
1. secrets-service.ts: Server storage MUST succeed before updating localStorage
- addGitProvider now stores to server FIRST, only updates localStorage on success
- createSecret/updateSecret/deleteSecret now throw on failure (no silent returns)
- Added retry logic with exponential backoff for all API calls
2. settings-service.api.ts: No silent fallback for encrypted settings
- getSettingsForConversation now throws if encrypted fetch fails
- Conversations should not start with broken/redacted credentials
- Added retry logic with exponential backoff
3. AGENTS.md: Document SDK dependency
- Settings persistence APIs require SDK PR #3060
- Until released, npm run dev defaults to main branch
- Documented git provider storage design (server + localStorage)
4. dev-safe.mjs: Default to SDK main branch
- Added DEFAULT_GIT_REF='main' constant
- npm run dev now uses main until settings APIs are released
- TODO comment to update once released
Note: Git provider tokens still use localStorage for frontend git API calls
(repo search, branches), but MUST succeed on server first.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: update server secret when only host changes
When updating just the host (empty token), the server secret's description
must also be updated to keep metadata in sync. Previously, only localStorage
was updated, violating the 'server storage must succeed first' principle.
Now the host-only update path also calls createSecret() to update the
server secret's description before updating localStorage.
Co-authored-by: openhands <openhands@all-hands.dev>
---------
Co-authored-by: openhands <openhands@all-hands.dev>