Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e5fc3a4
perf: batch StreamingDeltaEvents so the UI keeps up with fast models …
VascoSch92 Aug 18, 2026
be584ad
fix: honor the home LLM dropdown selection over an agent profile's pi…
hieptl Aug 18, 2026
b25f9b3
docs: add DefenseClaw and testing matrix to the docs index (#16408)
marmar9615-cloud Aug 18, 2026
1916c90
fix: restore onboarding modal bottom padding and stop leftover scroll…
FraterCCCLXIII Aug 18, 2026
48038af
fix(ci): stop treating markdown as frontend evidence-bearing code (#1…
marmar9615-cloud Aug 19, 2026
49812ee
fix: clear stale urlSearchResults for non-matching HTTPS URLs in useU…
xxiaoxiong Aug 19, 2026
7806815
feat(sidebar): add getting started checklist with settings toggle (#1…
FraterCCCLXIII Aug 19, 2026
6699fc9
fix: normalize trailing slash in git remote URLs (#16536)
mikemikimike Aug 19, 2026
2738be2
fix(scripts): use 127.0.0.1 for remaining localhost service URLs (#16…
marmar9615-cloud Aug 19, 2026
d636ae6
feat: show workspace path in Files view (#16362)
HurrairaBaloch Aug 19, 2026
551e9a9
fix(backend-registry): preserve URL fragments in withBackendSelection…
chrislazar25 Aug 19, 2026
3865628
fix: combine stats.usage_to_metrics into AppConversation.metrics when…
VascoSch92 Aug 19, 2026
5f11a4e
fix: reconcile non-native tool-call streamed <function=...> XML delta…
sideeffffect Aug 19, 2026
8780efb
fix(chat): scope Cmd+Enter build shortcut to plan mode (#16703)
VascoSch92 Aug 19, 2026
7c3b242
feat(automations): install a catalog entry that ships a script bundle…
VascoSch92 Aug 19, 2026
61c18c9
chore: bump openhands-automation to 1.8.0 (#16712)
VascoSch92 Aug 19, 2026
bc915cc
docs(tests): point the router example at a test that exists (#16711)
marmar9615-cloud Aug 19, 2026
550fc28
chore: bump @openhands/extensions to 0.17.0 (#16717)
VascoSch92 Aug 19, 2026
f2dd330
feat: add LLM provider-connections UI (local agent-server) (#16616)
juanmichelini Aug 19, 2026
7a9aacb
feat: polish automations dashboard, recommended rail, and Add/Import …
FraterCCCLXIII Aug 20, 2026
d70cf83
feat(conversation): add overview panel and unified commits drawer (#1…
FraterCCCLXIII Aug 20, 2026
df5d92b
fix(skills): keep modal actions visible for long descriptions (#16115)
KXHXK Aug 20, 2026
4c3bb82
fix: unsandbox the PDF preview iframe so the built-in viewer renders …
hieptl Aug 20, 2026
28be38a
fix: do not silently persist ACP model picks to agent_settings when p…
hieptl Aug 20, 2026
4c2367a
fix: support mouse drag-to-scroll on recommended rails (#16742)
hieptl Aug 20, 2026
4a8cabc
fix(git): stop reading a remote URL's port as part of the repository …
marmar9615-cloud Aug 20, 2026
4e7d1ec
chore: bump @openhands/extensions to 0.18.0 (#16754)
VascoSch92 Aug 21, 2026
b1f0acc
feat: send template provenance for versioned setup entries (#16458)
hieptl Aug 21, 2026
a80b1bb
chore: bump @openhands/typescript-client to 1.38.1 (#16780)
VascoSch92 Aug 21, 2026
56227de
feat: instrument onboarding checklist link clicks (#16777)
hieptl Aug 21, 2026
c59a28a
fix: keep the events socket alive across refetches and bound hung han…
hieptl Aug 21, 2026
ab23be6
chore(main): release 1.15.0 (#16664)
openhands-release-bot[bot] Aug 21, 2026
880a1cf
Merge remote-tracking branch 'upstream/main' into openhands-upstream-…
Lookoff-AIMLAPI Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/scripts/check_pr_description.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
".sass",
".less",
)
# Docs carry no visual state, so a screenshot can't evidence a change to them.
DOCUMENTATION_FILE_EXTENSIONS: tuple[str, ...] = (".md", ".mdx")
FRONTEND_CONFIG_GLOBS: tuple[str, ...] = (
"tailwind.config.*",
"vite.config.*",
Expand Down Expand Up @@ -155,9 +157,11 @@ def extract_human_note(body: str) -> str:
def is_frontend_file(path: str) -> bool:
"""Return True if a changed file should be treated as frontend code."""
normalized = path.lstrip("./")
lower = normalized.lower()
if lower.endswith(DOCUMENTATION_FILE_EXTENSIONS):
return False
if any(normalized.startswith(prefix) for prefix in FRONTEND_PATH_PREFIXES):
return True
lower = normalized.lower()
if any(lower.endswith(ext) for ext in FRONTEND_FILE_EXTENSIONS):
return True
name = normalized.split("/")[-1]
Expand Down
22 changes: 22 additions & 0 deletions .github/scripts/tests/test_pr_description.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from check_pr_description import (
is_frontend_file,
touches_frontend,
extract_linked_issue_numbers,
extract_pr_type,
validate_linked_issue_ready,
Expand Down Expand Up @@ -222,3 +224,23 @@ def test_bug_fix_with_video_link_no_errors():
"""
errors = validate_bug_fix_evidence(body)
assert errors == []


def test_markdown_under_frontend_prefix_is_not_frontend():
assert not is_frontend_file("__tests__/router.md")
assert not is_frontend_file("src/notes.md")
assert not is_frontend_file("public/README.mdx")

def test_markdown_outside_frontend_prefix_still_not_frontend():
assert not is_frontend_file("docs/README.md")

def test_frontend_code_under_prefix_still_frontend():
assert is_frontend_file("src/app.tsx")
assert is_frontend_file("__tests__/routes/launch.test.tsx")
assert is_frontend_file("src/styles/main.css")

def test_docs_only_change_does_not_require_frontend_evidence():
assert not touches_frontend(["__tests__/router.md", "docs/README.md"])

def test_mixed_change_still_requires_frontend_evidence():
assert touches_frontend(["__tests__/router.md", "src/app.tsx"])
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "1.14.0"
".": "1.15.0"
}
48 changes: 48 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,54 @@ One Canvas-owned PostHog client owns telemetry and app analytics.
2. Add the function to the hook's `return` object
3. Destructure and call it from the component: `const { trackFoo } = useTracking()`

### Event dictionary: onboarding_link_clicked

One stable event for every onboarding link/CTA click. New onboarding links must
reuse this contract (extend the unions in `use-tracking.ts`), never add one-off
events per destination.

Properties (all values controlled enums or booleans — never raw destination
URLs, query params, or link text; `current_url` is the standard app-page common
property, not a destination):
- `link_id` (`OnboardingLinkId`): `configure_llm` | `start_conversation` |
`schedule_task` | `customize_agent` | `connect_mcp` | `join_slack` |
`open_docs`
- `destination_type` (`OnboardingLinkDestinationType`): `community` |
`integration` | `documentation` | `settings` | `conversation` | `automation`
- `surface` (`OnboardingLinkSurface`): `landing_checklist` |
`onboarding_modal` (reserved; no modal links are instrumented yet)
- `checklist_item` (optional): the owning checklist item's `link_id`; set on
every `landing_checklist` emission, including `open_docs` clicks
- `step_id` (optional): reserved for future onboarding-modal links
- `is_external` (boolean): whether the destination leaves the app

Instrumented CTAs (sidebar "Getting started" checklist; the row link and its
preview action CTA intentionally share one `link_id` — same destination):

| Checklist item | Row + preview action | Preview docs link |
|---|---|---|
| Add LLM API key | `configure_llm` / `settings` / internal | `open_docs` / `documentation` / external |
| Start your first chat | `start_conversation` / `conversation` / internal | `open_docs` |
| Schedule a task | `schedule_task` / `automation` / internal | `open_docs` |
| Customize your agent | `customize_agent` / `settings` / internal | `open_docs` |
| Connect an MCP integration | `connect_mcp` / `integration` / internal | `open_docs` |
| Join the OpenHands Slack | `join_slack` / `community` / external | `open_docs` |

Excluded CTAs (per the one-canonical-capture rule above):
- Onboarding-modal wizard controls (back/next/skip/close, agent cards) →
covered by `onboarding_step_viewed` / `onboarding_completed` /
`onboarding_skipped`
- Modal backend-connect CTAs and the backend form's docs links →
`backend_added` with `source: "onboarding"`
- LLM settings help links inside the embedded settings screen (shared with
non-onboarding surfaces) → setup outcome captured by `settings_saved`
- Recommended-automation cards → `prebuilt_automation_enabled`
- Checklist expand/collapse toggle and the settings visibility switch → UI
state, not destination links

Known limitation: middle-click (`auxclick`) opens are not captured; tracking
uses React `onClick` only and never prevents default navigation.

### Env vars
`VITE_POSTHOG_API_KEY` is the sole build-time PostHog key. Unconfigured source builds use staging; official release workflows set production explicitly. Precompiled consumers use runtime configuration instead.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ docker run -it --rm \
-p 8000:8000 \
-v "$HOME/.openhands:/home/openhands/.openhands" \
-v "${PROJECTS_PATH}:/projects" \
ghcr.io/openhands/agent-canvas:1.14.0 # x-release-please-version
ghcr.io/openhands/agent-canvas:1.15.0 # x-release-please-version
```

**Windows (PowerShell / Windows Terminal):** See [README.windows.md](./README.windows.md) for the equivalent commands.
Expand Down
4 changes: 2 additions & 2 deletions README.windows.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ For the main install options and overall context, see [README.md](./README.md).
- A host directory for `PROJECTS_PATH` containing the project folders you want the agent to access (create it before starting the container)

```powershell
docker pull ghcr.io/openhands/agent-canvas:1.14.0 # x-release-please-version
docker pull ghcr.io/openhands/agent-canvas:1.15.0 # x-release-please-version

$env:PROJECTS_PATH = Join-Path $HOME "projects" # directory containing your project folders
New-Item -ItemType Directory -Force -Path $env:PROJECTS_PATH, (Join-Path $env:USERPROFILE ".openhands") | Out-Null
Expand All @@ -21,7 +21,7 @@ docker run -it --rm `
-p 8000:8000 `
-v "$($env:USERPROFILE)\.openhands:/home/openhands/.openhands" `
-v "$($env:PROJECTS_PATH):/projects" `
ghcr.io/openhands/agent-canvas:1.14.0 # x-release-please-version
ghcr.io/openhands/agent-canvas:1.15.0 # x-release-please-version
```

Open [http://localhost:8000/canvas](http://localhost:8000/canvas) in your browser.
Expand Down
87 changes: 87 additions & 0 deletions __tests__/api/agent-server-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,93 @@ describe("toAppConversation", () => {
updated_at: "2026-01-01T00:00:00Z",
};

it("combines stats.usage_to_metrics into metrics when the backend doesn't set metrics directly (#16480)", () => {
const result = toAppConversation({
...baseInfo,
stats: {
usage_to_metrics: {
agent: {
model_name: "agent-model",
accumulated_cost: 1.5,
max_budget_per_task: 10,
accumulated_token_usage: {
prompt_tokens: 100,
completion_tokens: 20,
cache_read_tokens: 5,
cache_write_tokens: 1,
context_window: 8000,
per_turn_token: 120,
},
costs: [],
response_latencies: [],
token_usages: [],
},
condenser: {
model_name: "condenser-model",
accumulated_cost: 0.5,
max_budget_per_task: null,
accumulated_token_usage: {
prompt_tokens: 40,
completion_tokens: 10,
cache_read_tokens: 0,
cache_write_tokens: 0,
context_window: 4000,
per_turn_token: 50,
},
costs: [],
response_latencies: [],
token_usages: [],
},
},
},
});

expect(result.metrics).toEqual({
accumulated_cost: 2,
max_budget_per_task: 10,
accumulated_token_usage: {
prompt_tokens: 140,
completion_tokens: 30,
cache_read_tokens: 5,
cache_write_tokens: 1,
context_window: 8000,
per_turn_token: 120,
},
});
});

it("prefers backend-provided metrics over stats.usage_to_metrics when both are present", () => {
const result = toAppConversation({
...baseInfo,
metrics: { accumulated_cost: 3, max_budget_per_task: null },
stats: {
usage_to_metrics: {
agent: {
model_name: "agent-model",
accumulated_cost: 999,
max_budget_per_task: null,
accumulated_token_usage: null,
costs: [],
response_latencies: [],
token_usages: [],
},
},
},
});

expect(result.metrics?.accumulated_cost).toBe(3);
});

it("defaults metrics to a zero-cost snapshot when neither metrics nor stats are present", () => {
const result = toAppConversation({ ...baseInfo });

expect(result.metrics).toEqual({
accumulated_cost: 0,
max_budget_per_task: null,
accumulated_token_usage: null,
});
});

it("falls back to the default title when the backend returns null", () => {
const result = toAppConversation({ ...baseInfo, title: null });
expect(result.title).toBe("Conversation 372eb");
Expand Down
44 changes: 44 additions & 0 deletions __tests__/api/agent-server-conversation-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,50 @@ describe("AgentServerConversationService", () => {
expect(result.items[0]?.sandbox_status).toBe("PAUSED");
});

it("falls back to stats.usage_to_metrics when searchConversations omits metrics (#16480)", async () => {
const searchSpy = vi.fn().mockResolvedValue({
items: [
{
id: "conv-stats-only",
created_at: "2024-01-01",
updated_at: "2024-01-01",
stats: {
usage_to_metrics: {
default: {
model_name: "test-model",
accumulated_cost: 1.25,
max_budget_per_task: null,
accumulated_token_usage: {
prompt_tokens: 100,
completion_tokens: 50,
cache_read_tokens: 0,
cache_write_tokens: 0,
context_window: 8000,
per_turn_token: 150,
},
costs: [],
response_latencies: [],
token_usages: [],
},
},
},
},
],
next_page_id: null,
});
mockConversationClient.mockReturnValue({
searchConversations: searchSpy,
});

const result =
await AgentServerConversationService.searchConversations(10);

expect(result.items[0]?.metrics?.accumulated_cost).toBe(1.25);
expect(
result.items[0]?.metrics?.accumulated_token_usage?.prompt_tokens,
).toBe(100);
});

it("preserves the launched Agent Profile through the wire normalizer", async () => {
mockHttpGet.mockResolvedValue({
data: [
Expand Down
67 changes: 67 additions & 0 deletions __tests__/api/backend-registry/url-selection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,73 @@ describe("withBackendSelectionParams", () => {
`/conversations/abc?tab=files&${BACKEND_QUERY_PARAM}=local-1`,
);
});

it("keeps a fragment after existing query parameters intact and after the query", () => {
const path = withBackendSelectionParams(
"/conversations/abc?tab=files#detail",
{
backend: localBackend,
orgId: null,
},
);

expect(path).toBe(
`/conversations/abc?tab=files&${BACKEND_QUERY_PARAM}=local-1#detail`,
);
});

it("keeps a fragment on a path without query parameters after the query", () => {
const path = withBackendSelectionParams("/conversations/abc#detail", {
backend: localBackend,
orgId: null,
});

expect(path).toBe(
`/conversations/abc?${BACKEND_QUERY_PARAM}=local-1#detail`,
);
});

it("does not treat a ? inside the fragment as a query separator", () => {
const path = withBackendSelectionParams("/conversations/abc#detail?x=1", {
backend: localBackend,
orgId: null,
});

expect(path).toBe(
`/conversations/abc?${BACKEND_QUERY_PARAM}=local-1#detail?x=1`,
);
});

it("round-trips an empty fragment verbatim", () => {
const path = withBackendSelectionParams("/conversations/abc#", {
backend: localBackend,
orgId: null,
});

expect(path).toBe(`/conversations/abc?${BACKEND_QUERY_PARAM}=local-1#`);
});

it("keeps the org id and the fragment together", () => {
const path = withBackendSelectionParams("/conversations/abc#detail", {
backend: cloudBackend,
orgId: "org-7",
});

expect(path).toBe(
`/conversations/abc?${BACKEND_QUERY_PARAM}=prod&${ORG_QUERY_PARAM}=org-7#detail`,
);
});

it("keeps query data that itself contains a ?", () => {
const path = withBackendSelectionParams("/conversations/abc?next=/a?b=1", {
backend: localBackend,
orgId: null,
});

expect(path).toBe(
`/conversations/abc?next=%2Fa%3Fb%3D1&${BACKEND_QUERY_PARAM}=local-1`,
);
});
});

describe("readBackendSelectionFromUrl", () => {
Expand Down
Loading
Loading