@openhands/extensions 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/custom-codereview-guide.md +31 -0
- package/.claude-plugin/marketplace.json +9 -0
- package/.codex-plugin/marketplace.json +9 -0
- package/.github/workflows/build-vscode-app.yml +3 -3
- package/.github/workflows/deprecation-check.yml +1 -1
- package/.github/workflows/pypi-publish.yml +2 -2
- package/.github/workflows/tests.yml +1 -1
- package/.plugin/marketplace.json +9 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +7 -2
- package/README.md +3 -2
- package/automations/bundle-index.js +5 -5
- package/automations/catalog/github-issue-to-pr/manifest.json +2 -2
- package/automations/catalog/github-issue-triage/manifest.json +2 -2
- package/automations/catalog/github-pr-reviewer/manifest.json +2 -2
- package/marketplaces/openhands-extensions.json +9 -0
- package/package.json +1 -1
- package/plugins/openhands/skills/openhands-automation/README.md +6 -4
- package/plugins/openhands/skills/openhands-automation/SKILL.md +103 -21
- package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +79 -12
- package/plugins/openhands/skills/openhands-sdk/SKILL.md +2 -1
- package/plugins/qa-changes/README.md +4 -0
- package/plugins/qa-changes/skills/qa-changes/README.md +3 -1
- package/plugins/qa-changes/skills/qa-changes/SKILL.md +42 -0
- package/plugins/vulnerability-remediation/skills/github/README.md +5 -1
- package/plugins/vulnerability-remediation/skills/github/SKILL.md +27 -0
- package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +51 -4
- package/pyproject.toml +1 -1
- package/python/openhands_extensions/_version.py +1 -1
- package/scripts/sync_openhands_sdk_skill.py +16 -13
- package/skills/azure-devops/README.md +3 -1
- package/skills/azure-devops/SKILL.md +4 -0
- package/skills/bitbucket/README.md +3 -1
- package/skills/bitbucket/SKILL.md +4 -2
- package/skills/bitbucket-cloud/README.md +2 -0
- package/skills/bitbucket-cloud/SKILL.md +2 -0
- package/skills/bitbucket-data-center/README.md +2 -0
- package/skills/bitbucket-data-center/SKILL.md +2 -0
- package/skills/canvas-extension-api/SKILL.md +4 -1
- package/skills/canvas-extension-api/references/native-design.md +250 -0
- package/skills/github/README.md +5 -1
- package/skills/github/SKILL.md +27 -0
- package/skills/github/scripts/agent_conversation.py +51 -4
- package/skills/github-issue-to-pr/scripts/agent_conversation.py +51 -4
- package/skills/github-issue-triage/scripts/agent_conversation.py +51 -4
- package/skills/github-pr-reviewer/README.md +5 -0
- package/skills/github-pr-reviewer/SKILL.md +12 -0
- package/skills/github-pr-reviewer/scripts/agent_conversation.py +51 -4
- package/skills/github-pr-reviewer/scripts/main.py +20 -6
- package/skills/github-pr-reviewer/scripts/worker.py +77 -15
- package/skills/github-pr-reviewer/tests/test_main.py +23 -0
- package/skills/gitlab/README.md +3 -1
- package/skills/gitlab/SKILL.md +4 -0
- package/skills/index.js +25 -13
- package/skills/linear/README.md +2 -0
- package/skills/linear/SKILL.md +6 -2
- package/skills/openhands-automation/README.md +6 -4
- package/skills/openhands-automation/SKILL.md +103 -21
- package/skills/openhands-automation/references/custom-automation.md +79 -12
- package/skills/openhands-sdk/SKILL.md +2 -1
- package/skills/qa-changes/README.md +3 -1
- package/skills/qa-changes/SKILL.md +42 -0
- package/skills/simplified-technical-english/.claude-plugin/plugin.json +15 -0
- package/skills/simplified-technical-english/.codex-plugin/plugin.json +15 -0
- package/skills/simplified-technical-english/.plugin/plugin.json +15 -0
- package/skills/simplified-technical-english/README.md +21 -0
- package/skills/simplified-technical-english/SKILL.md +65 -0
- package/tests/fixtures/automations/github-pr-reviewer.json +9 -9
- package/tests/test_agent_conversation_dispatch.py +130 -0
- package/tests/test_github_reviewer_delivery.py +279 -2
|
@@ -32,6 +32,11 @@ _CLOUD_START_GRACE_SECONDS = 10 * 60
|
|
|
32
32
|
# How long to wait for a paused conversation's sandbox to come back up.
|
|
33
33
|
_CLOUD_RESUME_TIMEOUT_SECONDS = 120
|
|
34
34
|
_CLOUD_POLL_SECONDS = 3
|
|
35
|
+
# How many times a matched delivery whose conversation ended in ERROR is sent
|
|
36
|
+
# again. The delivery key never changes for a stable subject, so without a
|
|
37
|
+
# retry its work is stranded; without a bound, a conversation that fails the
|
|
38
|
+
# same way every time would be re-run on every scan.
|
|
39
|
+
_MAX_ERROR_RETRIES = 2
|
|
35
40
|
|
|
36
41
|
|
|
37
42
|
def _register_tools() -> None:
|
|
@@ -204,14 +209,23 @@ class AgentConversationDispatcher:
|
|
|
204
209
|
|
|
205
210
|
same_delivery = record.get("delivery") == delivery
|
|
206
211
|
same_head = bool(head) and record.get("head") == head
|
|
212
|
+
# Error retries count against one delivery; a new delivery starts over.
|
|
213
|
+
error_retries = int(record.get("error_retries") or 0) if same_delivery else 0
|
|
214
|
+
can_retry = error_retries < _MAX_ERROR_RETRIES
|
|
207
215
|
|
|
208
216
|
if self._cloud:
|
|
209
217
|
disposition, conversation_id = self._deliver_cloud(
|
|
210
|
-
record,
|
|
218
|
+
record,
|
|
219
|
+
conversation_id,
|
|
220
|
+
subject,
|
|
221
|
+
prompt,
|
|
222
|
+
same_delivery,
|
|
223
|
+
same_head,
|
|
224
|
+
can_retry,
|
|
211
225
|
)
|
|
212
226
|
else:
|
|
213
227
|
disposition = self._deliver_local(
|
|
214
|
-
conversation_id, prompt, same_delivery, same_head
|
|
228
|
+
conversation_id, prompt, same_delivery, same_head, can_retry
|
|
215
229
|
)
|
|
216
230
|
|
|
217
231
|
if disposition in ("deduplicated", "in_progress"):
|
|
@@ -228,6 +242,10 @@ class AgentConversationDispatcher:
|
|
|
228
242
|
}
|
|
229
243
|
if disposition == "created" and self._cloud:
|
|
230
244
|
new_record["started_at"] = time.time()
|
|
245
|
+
if disposition == "retried":
|
|
246
|
+
error_retries += 1
|
|
247
|
+
if error_retries:
|
|
248
|
+
new_record["error_retries"] = error_retries
|
|
231
249
|
_kv_request(state_key, "PUT", new_record)
|
|
232
250
|
return {
|
|
233
251
|
"disposition": disposition,
|
|
@@ -235,7 +253,14 @@ class AgentConversationDispatcher:
|
|
|
235
253
|
}
|
|
236
254
|
|
|
237
255
|
def _deliver_cloud(
|
|
238
|
-
self,
|
|
256
|
+
self,
|
|
257
|
+
record,
|
|
258
|
+
conversation_id,
|
|
259
|
+
subject,
|
|
260
|
+
prompt,
|
|
261
|
+
same_delivery,
|
|
262
|
+
same_head,
|
|
263
|
+
can_retry,
|
|
239
264
|
):
|
|
240
265
|
"""Deliver through the OpenHands API; return the disposition and the id.
|
|
241
266
|
|
|
@@ -272,11 +297,21 @@ class AgentConversationDispatcher:
|
|
|
272
297
|
if status in ("idle", "paused"):
|
|
273
298
|
self._cloud.run(conversation)
|
|
274
299
|
return "resumed", current_id
|
|
300
|
+
if status == "error" and can_retry:
|
|
301
|
+
# The delivery matched but its conversation died before the work
|
|
302
|
+
# finished, so send the revision again rather than strand it.
|
|
303
|
+
# The API reports an execution status only while the sandbox
|
|
304
|
+
# runs, so an errored conversation whose sandbox was already
|
|
305
|
+
# paused is not seen here and keeps reporting deduplicated.
|
|
306
|
+
self._cloud.send(conversation, prompt)
|
|
307
|
+
return "retried", current_id
|
|
275
308
|
return "deduplicated", current_id
|
|
276
309
|
self._cloud.send(conversation, prompt)
|
|
277
310
|
return "resumed", current_id
|
|
278
311
|
|
|
279
|
-
def _deliver_local(
|
|
312
|
+
def _deliver_local(
|
|
313
|
+
self, conversation_id, prompt, same_delivery, same_head, can_retry
|
|
314
|
+
) -> str:
|
|
280
315
|
if self._workspace is None:
|
|
281
316
|
raise RuntimeError("AgentConversationDispatcher must be used as a context")
|
|
282
317
|
|
|
@@ -321,6 +356,18 @@ class AgentConversationDispatcher:
|
|
|
321
356
|
):
|
|
322
357
|
conversation.update_secrets(self._secrets)
|
|
323
358
|
conversation.run(blocking=False)
|
|
359
|
+
elif (
|
|
360
|
+
conversation.state.execution_status
|
|
361
|
+
== ConversationExecutionStatus.ERROR
|
|
362
|
+
and can_retry
|
|
363
|
+
):
|
|
364
|
+
# The delivery matched but its conversation died before
|
|
365
|
+
# the work finished, so send the revision again rather
|
|
366
|
+
# than strand it.
|
|
367
|
+
conversation.update_secrets(self._secrets)
|
|
368
|
+
conversation.send_message(prompt)
|
|
369
|
+
conversation.run(blocking=False)
|
|
370
|
+
disposition = "retried"
|
|
324
371
|
else:
|
|
325
372
|
disposition = "deduplicated"
|
|
326
373
|
else:
|
package/pyproject.toml
CHANGED
|
@@ -18,7 +18,7 @@ from importlib.metadata import PackageNotFoundError, version
|
|
|
18
18
|
#: Kept in lock-step with pyproject.toml/package.json by the version test.
|
|
19
19
|
#: release-please only rewrites a semver found on the annotated line itself, so
|
|
20
20
|
#: the annotation must stay on the assignment; on its own line it is a no-op.
|
|
21
|
-
_FALLBACK_VERSION = "0.
|
|
21
|
+
_FALLBACK_VERSION = "0.29.0" # x-release-please-version
|
|
22
22
|
|
|
23
23
|
try:
|
|
24
24
|
__version__: str = version("openhands-extensions")
|
|
@@ -25,6 +25,7 @@ from pathlib import Path
|
|
|
25
25
|
|
|
26
26
|
SKILL_PATH = Path(__file__).resolve().parent.parent / "skills" / "openhands-sdk" / "SKILL.md"
|
|
27
27
|
LLMS_TXT_URL = "https://docs.openhands.dev/llms.txt"
|
|
28
|
+
SDK_URL_PREFIX = "https://docs.openhands.dev/sdk/"
|
|
28
29
|
GH_API = "https://api.github.com/repos/OpenHands/software-agent-sdk/contents/examples"
|
|
29
30
|
GH_RAW = "https://raw.githubusercontent.com/OpenHands/software-agent-sdk/main"
|
|
30
31
|
GH_BROWSE = "https://github.com/OpenHands/software-agent-sdk/blob/main/examples"
|
|
@@ -90,18 +91,20 @@ def _fetch_or(url: str, fallback, **kw):
|
|
|
90
91
|
# -- llms.txt parsing --
|
|
91
92
|
|
|
92
93
|
def parse_sdk_entries(llms_txt: str) -> list[dict]:
|
|
93
|
-
"""Extract entries
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
94
|
+
"""Extract entries for the OpenHands Software Agent SDK.
|
|
95
|
+
|
|
96
|
+
The docs index (llms.txt) no longer groups entries under an
|
|
97
|
+
"OpenHands Software Agent SDK" heading, so select entries by their
|
|
98
|
+
canonical URL prefix instead. Sort by title (case-insensitively) to
|
|
99
|
+
keep the generated skill stable across runs.
|
|
100
|
+
"""
|
|
101
|
+
entries = [
|
|
102
|
+
m.groupdict()
|
|
103
|
+
for line in llms_txt.splitlines()
|
|
104
|
+
if (m := _ENTRY_RE.match(line.strip()))
|
|
105
|
+
and m.group("url").startswith(SDK_URL_PREFIX)
|
|
106
|
+
]
|
|
107
|
+
return sorted(entries, key=lambda e: e["title"].lower())
|
|
105
108
|
|
|
106
109
|
|
|
107
110
|
# -- content builders --
|
|
@@ -123,7 +126,7 @@ def build_classes_table(entries: list[dict]) -> str:
|
|
|
123
126
|
def build_api_refs(entries: list[dict]) -> str:
|
|
124
127
|
return ", ".join(
|
|
125
128
|
f"[`{e['title']}`]({e['url']})"
|
|
126
|
-
for e in entries if "/api-reference/" in e["url"]
|
|
129
|
+
for e in entries if "/sdk/api-reference/" in e["url"]
|
|
127
130
|
)
|
|
128
131
|
|
|
129
132
|
|
|
@@ -11,6 +11,8 @@ This skill is activated by the following keywords:
|
|
|
11
11
|
|
|
12
12
|
## Details
|
|
13
13
|
|
|
14
|
+
Use authenticated Azure DevOps MCP tools first when they are available in the agent's environment. Detect the connection through tool availability rather than a particular server name. Fall back to the documented `AZURE_DEVOPS_TOKEN` and direct API flow when those tools are unavailable or raw API or `curl` access is explicitly needed.
|
|
15
|
+
|
|
14
16
|
You have access to an environment variable, `AZURE_DEVOPS_TOKEN`, which allows you to interact with
|
|
15
17
|
the Azure DevOps API.
|
|
16
18
|
|
|
@@ -52,4 +54,4 @@ Common API endpoints:
|
|
|
52
54
|
- List repositories: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories?api-version=7.1`
|
|
53
55
|
- Get repository details: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}?api-version=7.1`
|
|
54
56
|
- List pull requests: `https://dev.azure.com/{organization}/{project}/_apis/git/pullrequests?api-version=7.1`
|
|
55
|
-
- Create pull request: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}/pullrequests?api-version=7.1` (POST)
|
|
57
|
+
- Create pull request: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}/pullrequests?api-version=7.1` (POST)
|
|
@@ -6,6 +6,10 @@ triggers:
|
|
|
6
6
|
- azure
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## Connection selection
|
|
10
|
+
|
|
11
|
+
Use authenticated Azure DevOps MCP tools first when they are available in the agent's environment. Detect this by tool availability, without depending on a particular MCP server name. Use the token and direct API instructions below only when Azure DevOps MCP tools are unavailable or when raw API or `curl` access is explicitly needed.
|
|
12
|
+
|
|
9
13
|
You have access to an environment variable, `AZURE_DEVOPS_TOKEN`, which allows you to interact with
|
|
10
14
|
the Azure DevOps API.
|
|
11
15
|
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
Bitbucket integration **hub** skill. It detects whether the repository is on Bitbucket
|
|
4
4
|
Cloud or Bitbucket Data Center and directs the agent to the matching detailed skill.
|
|
5
5
|
|
|
6
|
+
Authenticated Bitbucket MCP tools are preferred when available, detected generically by tool availability rather than a particular server name. The hub still routes to Cloud or Data Center, and the documented token and direct API paths remain the fallback when MCP tools are unavailable or raw API or `curl` access is explicitly needed.
|
|
7
|
+
|
|
6
8
|
## Triggers
|
|
7
9
|
|
|
8
10
|
This skill is activated by the following keywords:
|
|
@@ -30,4 +32,4 @@ when the words "data center" never appear. It then:
|
|
|
30
32
|
The hub also carries a small quick-reference table as a fallback in case the detailed skill
|
|
31
33
|
cannot be loaded.
|
|
32
34
|
|
|
33
|
-
See [`SKILL.md`](SKILL.md) for the full content.
|
|
35
|
+
See [`SKILL.md`](SKILL.md) for the full content.
|
|
@@ -5,6 +5,8 @@ triggers:
|
|
|
5
5
|
- bitbucket
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
Use authenticated Bitbucket MCP tools first when they are available in the agent's environment. Detect this by tool availability, without depending on a particular MCP server name. Still identify Cloud versus Data Center and load the matching detailed skill. Use the token and direct API instructions only when Bitbucket MCP tools are unavailable or when raw API or `curl` access is explicitly needed.
|
|
9
|
+
|
|
8
10
|
You are working with **Bitbucket**, which ships as two distinct products that behave
|
|
9
11
|
differently:
|
|
10
12
|
|
|
@@ -19,7 +21,7 @@ detailed skill for full instructions.
|
|
|
19
21
|
|
|
20
22
|
## Step 1 — Detect which Bitbucket you are on
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
First use the repository host or available Bitbucket MCP tool context to identify the product. For the token fallback, check which token environment variable is present. Environment variable names are
|
|
23
25
|
case-sensitive, so look for it case-insensitively:
|
|
24
26
|
|
|
25
27
|
```bash
|
|
@@ -53,4 +55,4 @@ Bitbucket API (not a web browser) and always use the listed PR tool to open a pu
|
|
|
53
55
|
| Host | `bitbucket.org` | self-hosted domain |
|
|
54
56
|
| REST API base | `https://api.bitbucket.org/2.0` | `https://<host>/rest/api/1.0` |
|
|
55
57
|
| Repository identifier | `workspace/repo_slug` | `PROJECT/repo_slug` (project key) |
|
|
56
|
-
| Pull request tool | `create_bitbucket_pr` | `create_bitbucket_data_center_pr` |
|
|
58
|
+
| Pull request tool | `create_bitbucket_pr` | `create_bitbucket_data_center_pr` |
|
|
@@ -11,5 +11,7 @@ it detects a Bitbucket Cloud environment (i.e. `BITBUCKET_TOKEN` is set).
|
|
|
11
11
|
|
|
12
12
|
## Details
|
|
13
13
|
|
|
14
|
+
Use authenticated Bitbucket Cloud MCP tools first when they are available, detected by tool availability rather than a particular server name. Fall back to the documented `BITBUCKET_TOKEN` and direct REST API flow when those tools are unavailable or raw API or `curl` access is explicitly needed.
|
|
15
|
+
|
|
14
16
|
See [`SKILL.md`](SKILL.md) for the full content, including authenticated git remote
|
|
15
17
|
construction and pull request instructions.
|
|
@@ -3,6 +3,8 @@ name: bitbucket-cloud
|
|
|
3
3
|
description: Bitbucket Cloud (bitbucket.org) specifics — authenticate with BITBUCKET_TOKEN, use the REST API v2, workspace/repo_slug repositories, and the create_bitbucket_pr tool. Loaded on demand by the bitbucket skill once a Cloud environment is detected.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
Use authenticated Bitbucket Cloud MCP tools first when they are available in the agent's environment. Detect this by tool availability, without depending on a particular MCP server name. Use the token and direct REST API instructions below only when those tools are unavailable or when raw API or `curl` access is explicitly needed.
|
|
7
|
+
|
|
6
8
|
You are working with **Bitbucket Cloud** (`bitbucket.org`). You have access to an
|
|
7
9
|
environment variable, `BITBUCKET_TOKEN`, which allows you to interact with the Bitbucket
|
|
8
10
|
Cloud API.
|
|
@@ -12,6 +12,8 @@ it detects a Bitbucket Data Center environment (i.e. `BITBUCKET_DATA_CENTER_TOKE
|
|
|
12
12
|
|
|
13
13
|
## Details
|
|
14
14
|
|
|
15
|
+
Use authenticated Bitbucket Data Center MCP tools first when they are available, detected by tool availability rather than a particular server name. Fall back to the documented `BITBUCKET_DATA_CENTER_TOKEN` and direct REST API flow when those tools are unavailable or raw API or `curl` access is explicitly needed.
|
|
16
|
+
|
|
15
17
|
See [`SKILL.md`](SKILL.md) for the full content, including authenticated `scm/` git remote
|
|
16
18
|
construction and pull request instructions. Note the key differences from Bitbucket Cloud:
|
|
17
19
|
the REST API is version `1.0` under `https://{domain}/rest/api/1.0`, repositories are
|
|
@@ -3,6 +3,8 @@ name: bitbucket-data-center
|
|
|
3
3
|
description: Bitbucket Data Center (self-hosted Bitbucket Server) specifics — authenticate with BITBUCKET_DATA_CENTER_TOKEN, use the REST API 1.0, PROJECT/repo_slug repositories, scm/ git remotes, and the create_bitbucket_data_center_pr tool. Loaded on demand by the bitbucket skill once a Data Center environment is detected.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
Use authenticated Bitbucket Data Center MCP tools first when they are available in the agent's environment. Detect this by tool availability, without depending on a particular MCP server name. Use the token and direct REST API instructions below only when those tools are unavailable or when raw API or `curl` access is explicitly needed.
|
|
7
|
+
|
|
6
8
|
You are working with **Bitbucket Data Center** (self-hosted Bitbucket Server). You have
|
|
7
9
|
access to an environment variable, `BITBUCKET_DATA_CENTER_TOKEN`, which contains a basic
|
|
8
10
|
auth token in the format `username:your-token` that allows you to interact with the git
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: canvas-extension-api
|
|
3
|
-
description: This skill should be used when the user asks to "create an OpenHands App", "scaffold a Canvas App", "build an app with the Canvas Extensions API", "build an Agent Canvas App", "bundle a single-file Canvas App", "build a Sidecar-backed Canvas App", "add onboarding for an App service", "add a custom interface to Agent Canvas", "validate an OpenHands App", or mentions Canvas Apps, Agent Canvas extensions, Blob-importable app bundles, registerPage, app pages, Sidecars, or canvas extension packages.
|
|
3
|
+
description: This skill should be used when the user asks to "create an OpenHands App", "scaffold a Canvas App", "build an app with the Canvas Extensions API", "build an Agent Canvas App", "bundle a single-file Canvas App", "build a Sidecar-backed Canvas App", "add onboarding for an App service", "add a custom interface to Agent Canvas", "validate an OpenHands App", "make a Canvas App look native", "match Agent Canvas styles", "style a canvas extension", "reuse OpenHands design tokens", "add native Tailwind classes to an app", or mentions Canvas Apps, Agent Canvas extensions, Blob-importable app bundles, registerPage, app pages, Sidecars, or canvas extension packages.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Canvas Extensions API
|
|
@@ -195,6 +195,8 @@ Assume activation, mounting, and disposal may happen repeatedly during hot enabl
|
|
|
195
195
|
|
|
196
196
|
Render within the supplied `container`; do not replace unrelated Canvas DOM. Scope CSS under an app-specific root class. Prefer Canvas CSS variables with sensible fallbacks rather than copying host implementation classes.
|
|
197
197
|
|
|
198
|
+
Read `references/native-design.md` before styling an App page. It covers the core rule (reuse tokens, not host internals), the three implementation paths (dependency-free, Tailwind-bundled, existing CSS), the Tailwind v4 theme mapping to host variables, layout and density conventions, copy-ready component recipes, the raw scoped CSS alternative, and a mount-safe style helper. Treat it as the authoritative design reference for Apps.
|
|
199
|
+
|
|
198
200
|
Provide:
|
|
199
201
|
|
|
200
202
|
- semantic structure and headings inside the supplied container. Canvas already owns the outer `<main aria-label={page title}>`, so do not add a nested `main` or duplicate page landmark label;
|
|
@@ -281,5 +283,6 @@ Summarize:
|
|
|
281
283
|
- `references/testing-and-installation.md` - test strategy, manual Canvas workflow, and installation coordinates.
|
|
282
284
|
- `references/packaging-recipes.md` - one-file Vite, CSS, assets, Workers, and WASM rules.
|
|
283
285
|
- `references/backend-safety.md` - authenticated requests, command safety, persistence, and prerequisite onboarding.
|
|
286
|
+
- `references/native-design.md` - native Agent Canvas styling: token reuse, Tailwind mapping, layout, component recipes, scoped CSS, and style injection.
|
|
284
287
|
- `references/acceptance-checklist.md` - automated checks and the local install/enable/reload lifecycle.
|
|
285
288
|
- `scripts/validate-extension.mjs` - dependency-free static artifact validator for an App package directory.
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# Native OpenHands App design
|
|
2
|
+
|
|
3
|
+
Build Apps for Agent Canvas that visually belong in the host while remaining a self-contained, safe extension package. Use this reference together with the main `SKILL.md` workflow before changing an App's UI.
|
|
4
|
+
|
|
5
|
+
## Core rule: reuse tokens, not host internals
|
|
6
|
+
|
|
7
|
+
Treat an App bundle as an independent frontend. Do not import Agent Canvas React components, utility modules, CSS files, font files, Tailwind configuration, aliases, or private DOM classes. The host imports the App as a self-contained Blob ESM module, so those imports will fail or create fragile coupling.
|
|
8
|
+
|
|
9
|
+
Reuse the host's stable CSS custom properties instead. Scope every selector under a unique App root class and provide literal fallbacks so the page stays legible outside a current Canvas build. Tailwind utility names such as `bg-base` or `text-contrast` are **not globally available** to an App: they work only after the App's own Tailwind build maps them to the host variables.
|
|
10
|
+
|
|
11
|
+
Use semantic names (`surface`, `foreground`, `border`, `primary`) rather than copying cool-grey hex values.
|
|
12
|
+
|
|
13
|
+
## Choose the implementation path
|
|
14
|
+
|
|
15
|
+
Choose the smallest path that matches the App package:
|
|
16
|
+
|
|
17
|
+
| App implementation | Recommended styling approach |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| Dependency-free JavaScript | Inject one scoped CSS string from the entrypoint. Use host custom properties directly. |
|
|
20
|
+
| React/TypeScript with Tailwind | Add an App-local Tailwind theme mapping to the host variables, then compile every class into the single `extension.js` artifact. |
|
|
21
|
+
| Existing CSS module or CSS-in-JS setup | Define the semantic colors and component recipes in the App's own scoped stylesheet. |
|
|
22
|
+
|
|
23
|
+
Never expect the host Tailwind compiler to scan or compile App source. Never use a CDN Tailwind runtime. Never attach App styles to `:root`, `html`, `body`, or global element selectors.
|
|
24
|
+
|
|
25
|
+
## Tailwind setup for a bundled App
|
|
26
|
+
|
|
27
|
+
For Tailwind v4, place an App-local mapping in the App stylesheet. This lets the App use Canvas-like classes while preserving portability:
|
|
28
|
+
|
|
29
|
+
```css
|
|
30
|
+
@import "tailwindcss";
|
|
31
|
+
|
|
32
|
+
@theme inline {
|
|
33
|
+
--color-base: var(--oh-color-base);
|
|
34
|
+
--color-base-secondary: var(--oh-color-base-secondary);
|
|
35
|
+
--color-surface: var(--oh-surface);
|
|
36
|
+
--color-surface-raised: var(--oh-surface-raised);
|
|
37
|
+
--color-foreground: var(--oh-foreground);
|
|
38
|
+
--color-contrast: var(--oh-contrast);
|
|
39
|
+
--color-muted: var(--oh-muted);
|
|
40
|
+
--color-tertiary-alt: var(--oh-text-dim);
|
|
41
|
+
--color-border: var(--oh-border);
|
|
42
|
+
--color-border-subtle: var(--oh-border-subtle);
|
|
43
|
+
--color-focus: var(--oh-focus);
|
|
44
|
+
--color-interactive-hover: var(--oh-interactive-hover);
|
|
45
|
+
--color-primary: var(--oh-color-primary);
|
|
46
|
+
--color-on-primary: var(--oh-accent-foreground);
|
|
47
|
+
--color-danger: var(--oh-danger);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Compile this stylesheet into `extension.js`; do not leave a linked CSS asset. For an App without Tailwind, use the matching raw CSS below instead. Use utility recipes only after adding this mapping.
|
|
52
|
+
|
|
53
|
+
## Layout and density
|
|
54
|
+
|
|
55
|
+
Let Canvas own the outer page and scrolling region. Do not add a second page shell, `h-screen`, global background, sidebar, or top navigation. Start with `min-h-full`, use `p-4 sm:p-6 lg:p-8`, and constrain dense content with `mx-auto w-full max-w-5xl`. Use `gap-4` for related controls, `gap-6` within sections, and `gap-8` between major sections. Use `rounded-lg` for controls and `rounded-xl` for cards and messages.
|
|
56
|
+
|
|
57
|
+
Avoid gradients, glass effects, oversize headings, floating shadows, dense borders, and hard-coded dark greys. The native visual language is restrained: dark layered surfaces, cool-grey borders, warm primary actions, direct typography, and short transitions.
|
|
58
|
+
|
|
59
|
+
## Interaction and state checklist
|
|
60
|
+
|
|
61
|
+
- Apply `text-contrast` to headings and key labels; use `text-muted` or `text-tertiary-alt` for supporting copy.
|
|
62
|
+
- Use the primary recipe for one dominant action. Use a bordered secondary action for alternatives. Do not make every button primary.
|
|
63
|
+
- Use a 36px (`h-9`) control height and `rounded-lg` for input, select, and standard button controls.
|
|
64
|
+
- Use `focus-visible` rather than removing outlines. Preserve disabled affordance with `disabled:cursor-not-allowed disabled:opacity-60`.
|
|
65
|
+
- Animate only background, border, box-shadow, opacity, or transforms and include `motion-reduce:transition-none`.
|
|
66
|
+
- Scope icon sizing and color within the App. Do not assume HeroUI, Lucide, or host icon packages exist.
|
|
67
|
+
- Use `textContent`, framework escaping, or safe DOM creation for data from requests. Never inject response text with `innerHTML`.
|
|
68
|
+
|
|
69
|
+
## Semantic token map
|
|
70
|
+
|
|
71
|
+
| Intent | Host variable | Fallback |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| Deep page base | `--oh-color-base` | `#0B0E14` |
|
|
74
|
+
| Standard panel | `--oh-color-base-secondary` | `#21252F` |
|
|
75
|
+
| Raised surface | `--oh-surface-raised` | `#2C313F` |
|
|
76
|
+
| Main text | `--oh-contrast` | `#FFFFFF` |
|
|
77
|
+
| Standard text | `--oh-foreground` | `#EEF2F7` |
|
|
78
|
+
| Secondary text | `--oh-muted` | `#A3B0C4` |
|
|
79
|
+
| Muted placeholder text | `--oh-text-dim` | `#7E8A9E` |
|
|
80
|
+
| Standard border | `--oh-border` | `#4B5468` |
|
|
81
|
+
| Subtle divider | `--oh-border-subtle` | `#383F50` |
|
|
82
|
+
| Focus ring | `--oh-focus` | `#FFFFFF` |
|
|
83
|
+
| Primary surface | `--oh-color-primary` | `#C9B974` |
|
|
84
|
+
| Primary text | `--oh-accent-foreground` | `#0B0E14` |
|
|
85
|
+
| Danger | `--oh-danger` | `#E76A5E` |
|
|
86
|
+
| Success | `--oh-success` | `#A5E75E` |
|
|
87
|
+
|
|
88
|
+
## Copy-ready Tailwind recipes
|
|
89
|
+
|
|
90
|
+
Use these recipes only after mapping the semantic colors in the App-local Tailwind configuration above.
|
|
91
|
+
|
|
92
|
+
### Content shell
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
<div className="min-h-full p-4 sm:p-6 lg:p-8 text-foreground">
|
|
96
|
+
<div className="mx-auto flex w-full max-w-5xl flex-col gap-8">...</div>
|
|
97
|
+
</div>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Do not use `h-screen`, `min-h-screen`, a host-like global background, or a nested `<main>`; Canvas supplies the page landmark and scroll context.
|
|
101
|
+
|
|
102
|
+
### Page heading
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
<header className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
|
106
|
+
<div className="min-w-0">
|
|
107
|
+
<h1 className="text-xl font-semibold tracking-tight text-contrast">Page title</h1>
|
|
108
|
+
<p className="mt-1 text-sm text-muted">Short supporting description.</p>
|
|
109
|
+
</div>
|
|
110
|
+
</header>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Card or section
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
<section className="rounded-xl border border-border bg-base-secondary p-4 sm:p-6">
|
|
117
|
+
<h2 className="text-base font-medium text-contrast">Section title</h2>
|
|
118
|
+
<p className="mt-1 text-sm text-muted">Explain the section briefly.</p>
|
|
119
|
+
<div className="mt-5 flex flex-col gap-4">...</div>
|
|
120
|
+
</section>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Use `border-border-subtle` for dividers inside an existing card. Avoid default card shadows.
|
|
124
|
+
|
|
125
|
+
### Controls and buttons
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
const control =
|
|
129
|
+
"h-9 min-h-9 rounded-lg transition-[background-color,border-color,box-shadow,opacity] duration-75 motion-reduce:transition-none";
|
|
130
|
+
|
|
131
|
+
const field = `${control} w-full min-w-0 border border-border bg-base-secondary px-3 text-sm text-contrast placeholder:text-tertiary-alt focus:border-contrast/40 focus:ring-1 focus:ring-contrast/20 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60`;
|
|
132
|
+
|
|
133
|
+
const primaryButton = `${control} inline-flex w-fit items-center justify-center gap-2 bg-primary px-3 text-sm font-normal text-on-primary hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-30`;
|
|
134
|
+
|
|
135
|
+
const secondaryButton = `${control} inline-flex w-fit items-center justify-center gap-2 border border-border bg-base-secondary px-3 text-sm font-normal text-contrast hover:bg-surface-raised disabled:cursor-not-allowed disabled:opacity-30`;
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Use `label` plus `htmlFor`, keep field hints in `text-xs text-muted`, and express validation with `aria-invalid`, a `border-red-500`, and a linked `role="alert"` message using `text-xs text-red-400`.
|
|
139
|
+
|
|
140
|
+
### Toolbar and responsive grid
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
144
|
+
<div className="min-w-0">...</div>
|
|
145
|
+
<div className="flex flex-wrap items-center gap-2">...</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">...</div>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Informational, error, and empty state
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
<div className="rounded-xl border border-border bg-base-secondary p-6 text-center">
|
|
155
|
+
<h2 className="text-base font-medium text-contrast">Nothing here yet</h2>
|
|
156
|
+
<p className="mt-2 text-sm text-muted">Describe the next useful action.</p>
|
|
157
|
+
<button className={`${primaryButton} mt-4`}>Create item</button>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div role="alert" className="rounded-xl border border-red-500/40 bg-red-500/10 px-4 py-3 text-sm text-red-200">
|
|
161
|
+
The request could not be completed.
|
|
162
|
+
</div>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Use `animate-pulse` with `motion-reduce:animate-none` only for short loading placeholders; reserve space with shapes matching loaded cards or rows.
|
|
166
|
+
|
|
167
|
+
### Compact tabs
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
<div role="tablist" aria-label="View options" className="flex w-fit gap-1 rounded-lg border border-border bg-base-secondary p-1">
|
|
171
|
+
<button role="tab" aria-selected="true" className="rounded-md bg-interactive-hover px-3 py-1.5 text-sm text-contrast">Overview</button>
|
|
172
|
+
<button role="tab" aria-selected="false" className="rounded-md px-3 py-1.5 text-sm text-muted hover:bg-surface-raised hover:text-contrast">Activity</button>
|
|
173
|
+
</div>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Implement keyboard tab behavior when tabs change content; use regular buttons for simple filters.
|
|
177
|
+
|
|
178
|
+
## Raw scoped CSS alternative
|
|
179
|
+
|
|
180
|
+
Use this approach for dependency-free Apps. Apply it only after inserting an App root such as `.acme-dashboard`.
|
|
181
|
+
|
|
182
|
+
```css
|
|
183
|
+
.acme-dashboard {
|
|
184
|
+
color: var(--oh-foreground, #EEF2F7);
|
|
185
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
186
|
+
}
|
|
187
|
+
.acme-dashboard .card {
|
|
188
|
+
border: 1px solid var(--oh-border, #4B5468);
|
|
189
|
+
border-radius: 12px;
|
|
190
|
+
background: var(--oh-color-base-secondary, #21252F);
|
|
191
|
+
padding: 1.5rem;
|
|
192
|
+
}
|
|
193
|
+
.acme-dashboard .field,
|
|
194
|
+
.acme-dashboard .button {
|
|
195
|
+
min-height: 2.25rem;
|
|
196
|
+
border-radius: 8px;
|
|
197
|
+
transition: background-color 75ms, border-color 75ms, box-shadow 75ms, opacity 75ms;
|
|
198
|
+
}
|
|
199
|
+
.acme-dashboard .field {
|
|
200
|
+
width: 100%; border: 1px solid var(--oh-border, #4B5468);
|
|
201
|
+
background: var(--oh-color-base-secondary, #21252F);
|
|
202
|
+
color: var(--oh-contrast, #FFF); padding: 0 0.75rem;
|
|
203
|
+
}
|
|
204
|
+
.acme-dashboard .field:focus-visible,
|
|
205
|
+
.acme-dashboard .button:focus-visible {
|
|
206
|
+
outline: none; border-color: color-mix(in srgb, var(--oh-contrast, #FFF) 40%, transparent);
|
|
207
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--oh-contrast, #FFF) 20%, transparent);
|
|
208
|
+
}
|
|
209
|
+
.acme-dashboard .button--primary {
|
|
210
|
+
border: 0; background: var(--oh-color-primary, #C9B974);
|
|
211
|
+
color: var(--oh-accent-foreground, #0B0E14); padding: 0 0.75rem;
|
|
212
|
+
}
|
|
213
|
+
@media (prefers-reduced-motion: reduce) {
|
|
214
|
+
.acme-dashboard .field, .acme-dashboard .button { transition: none; }
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Mount-safe injected style helper
|
|
219
|
+
|
|
220
|
+
Use one marker per mounted App root and remove the node during cleanup.
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
function installStyles(css, marker) {
|
|
224
|
+
const style = document.createElement("style");
|
|
225
|
+
style.dataset.canvasAppStyle = marker;
|
|
226
|
+
style.textContent = css;
|
|
227
|
+
document.head.append(style);
|
|
228
|
+
return () => style.remove();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// During mount:
|
|
232
|
+
const disposeStyles = installStyles(STYLES, "acme-dashboard");
|
|
233
|
+
const root = document.createElement("div");
|
|
234
|
+
root.className = "acme-dashboard";
|
|
235
|
+
container.append(root);
|
|
236
|
+
return () => {
|
|
237
|
+
disposeStyles();
|
|
238
|
+
root.remove();
|
|
239
|
+
};
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
If more than one page can mount concurrently, ref-count the marker or inject its stylesheet once in `activate` and remove it in the activation disposer.
|
|
243
|
+
|
|
244
|
+
## Verify before delivery
|
|
245
|
+
|
|
246
|
+
- Confirm extension markup uses a unique root class and no App CSS leaks outside it.
|
|
247
|
+
- Confirm styles survive a real Canvas mount, route remount, disable/re-enable cycle, and host dark surface backgrounds.
|
|
248
|
+
- Confirm `extension.js` has no unresolved CSS import, bare package import, extra CSS chunk, or Node global.
|
|
249
|
+
- Run the Canvas extension static validator plus the App's lint, tests, and build.
|
|
250
|
+
- Report whether the App uses direct host variables or an App-local Tailwind mapping, and list the tested states.
|
package/skills/github/README.md
CHANGED
|
@@ -11,6 +11,8 @@ This skill is activated by the following keywords:
|
|
|
11
11
|
|
|
12
12
|
## Details
|
|
13
13
|
|
|
14
|
+
Use authenticated GitHub MCP tools first when they are available in the agent's environment. Detect the connection through tool availability rather than a particular server name. Fall back to the documented `GITHUB_TOKEN` and direct API flow when those tools are unavailable or raw API or `curl` access is explicitly needed.
|
|
15
|
+
|
|
14
16
|
You have access to an environment variable, `GITHUB_TOKEN`, which allows you to interact with
|
|
15
17
|
the GitHub API.
|
|
16
18
|
|
|
@@ -39,4 +41,6 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
|
|
|
39
41
|
```bash
|
|
40
42
|
git remote -v && git branch # to find the current org, repo and branch
|
|
41
43
|
git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
|
|
42
|
-
```
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
When asked to merge a pull request, the skill uses GitHub's async merge API (`PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge-async`, then poll the returned UUID). It supports stacked PRs and merge queues. Docs: https://docs.github.com/en/rest/pulls/pulls#merge-a-pull-request-asynchronously
|
package/skills/github/SKILL.md
CHANGED
|
@@ -5,6 +5,10 @@ triggers:
|
|
|
5
5
|
- github
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## Connection selection
|
|
9
|
+
|
|
10
|
+
Use authenticated GitHub MCP tools first when they are available in the agent's environment. Detect this by tool availability, without depending on a particular MCP server name. Use the token and direct API instructions below only when GitHub MCP tools are unavailable or when raw API or `curl` access is explicitly needed.
|
|
11
|
+
|
|
8
12
|
You have access to an environment variable, `GITHUB_TOKEN`, which allows you to interact with
|
|
9
13
|
the GitHub API.
|
|
10
14
|
|
|
@@ -37,6 +41,29 @@ git remote -v && git branch # to find the current org, repo and branch
|
|
|
37
41
|
git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
|
|
38
42
|
```
|
|
39
43
|
|
|
44
|
+
## Merging Pull Requests (async merge API)
|
|
45
|
+
|
|
46
|
+
IMPORTANT:
|
|
47
|
+
Only merge if the user asks you to, without ambiguity.
|
|
48
|
+
|
|
49
|
+
GitHub released an async merge API in October 2026; it is now the
|
|
50
|
+
recommended way to merge PRs programmatically, and the only merge API that supports stacked PRs. It can also add a PR to a merge queue.
|
|
51
|
+
Prefer it over `PUT .../pulls/{n}/merge` and the GraphQL `mergePullRequest` mutation.
|
|
52
|
+
A GitHub MCP merge tool may still use the older synchronous endpoint; for stacked PRs or merge queues, use the calls below.
|
|
53
|
+
Docs: https://docs.github.com/en/rest/pulls/pulls#merge-a-pull-request-asynchronously
|
|
54
|
+
|
|
55
|
+
1. Request the merge (`merge_action`: `default` uses the merge queue if the branch has one, else merges directly; or `direct_merge` / `merge_queue`). `merge_method`, `commit_title` and `commit_message` apply only to direct merges:
|
|
56
|
+
```bash
|
|
57
|
+
gh api -X PUT repos/<OWNER>/<REPO>/pulls/<PR_NUMBER>/merge-async -f merge_method=squash -f sha=<HEAD_SHA>
|
|
58
|
+
```
|
|
59
|
+
2. A `202` returns `details.uuid`; poll it every few seconds until `status` is no longer `pending`:
|
|
60
|
+
```bash
|
|
61
|
+
gh api repos/<OWNER>/<REPO>/pulls/<PR_NUMBER>/merge-async/<UUID>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Final `status` is `merged`, `enqueued` (added to the merge queue, NOT merged yet), or `failed` (see `details.message`).
|
|
65
|
+
A `200` on the PUT means the PR is already merged or queued; a `409` means a merge request is already pending (its UUID is returned).
|
|
66
|
+
|
|
40
67
|
## Handling Review Comments
|
|
41
68
|
|
|
42
69
|
- Critically evaluate each review comment before acting on it. Not all feedback is worth implementing:
|