diff --git a/automations/README.md b/automations/README.md index 09194c89..4153de10 100644 --- a/automations/README.md +++ b/automations/README.md @@ -270,12 +270,13 @@ falling back to its built-in defaults when the manifest is absent or fails admis captions it: `overview.tiles[].metric` names a host-computed value (`automations`, `needs-attention`, `total-runs`, `average-duration`), filter option values name host predicates (`status`: enabled / latest-run-failed / disabled; `trigger`: `event` matches event-triggered automations, `schedule` - everything else), `sort` values name host comparators, and `insights` captions the host's run-health - states and per-automation stats. The health precedence, the run sampling, the value formatting, the - relative-time rendering, and the filtered-empty state with its reset button are the host's - a manifest - cannot redefine them, only relabel what appears. Tile `detail` copy is plain substitution over the - metric's placeholder namespace (only the `automations` metric exposes `{{active}}`); `zeroDetail` - replaces `detail` while the value is zero. + everything else; `created_by`: `me` matches automations the signed-in user created, `others` everything + else, and the host decides where it is shown), `sort` values name host comparators, and `insights` + captions the host's run-health states and per-automation stats. The health precedence, the run + sampling, the value formatting, the relative-time rendering, and the filtered-empty state with its reset + button are the host's - a manifest cannot redefine them, only relabel what appears. Tile `detail` copy is + plain substitution over the metric's placeholder namespace (only the `automations` metric exposes + `{{active}}`); `zeroDetail` replaces `detail` while the value is zero. - **`docsUrl`** - the automations documentation link, prefix-pinned to docs.openhands.dev by schema. - **`attributes`** - the input surface of an existing Automation: which attributes can be set after creation, keyed by the runtime-model property the host sends (`name`, `prompt`, `model`, `timeout`, diff --git a/automations/index.d.ts b/automations/index.d.ts index 4f61cf58..83815f88 100644 --- a/automations/index.d.ts +++ b/automations/index.d.ts @@ -309,6 +309,7 @@ export type AutomationStatusFilterValue = | "failing" | "disabled"; export type AutomationTriggerFilterValue = "all" | "schedule" | "event"; +export type AutomationCreatedByFilterValue = "all" | "me" | "others"; /** * A filter dropdown. Values name predicates the host implements; the manifest @@ -327,9 +328,16 @@ export interface AutomationTriggerFilter { options: { value: AutomationTriggerFilterValue; label: string }[]; } +export interface AutomationCreatedByFilter { + id: "created_by"; + label: string; + options: { value: AutomationCreatedByFilterValue; label: string }[]; +} + export type AutomationDashboardFilter = | AutomationStatusFilter - | AutomationTriggerFilter; + | AutomationTriggerFilter + | AutomationCreatedByFilter; /** A comparator the host implements, named from a closed set. */ export type AutomationSortValue = "last-run" | "runs" | "name"; diff --git a/automations/interface.json b/automations/interface.json index e89e8211..796f08fd 100644 --- a/automations/interface.json +++ b/automations/interface.json @@ -71,6 +71,15 @@ { "value": "schedule", "label": "Scheduled" }, { "value": "event", "label": "Event-driven" } ] + }, + { + "id": "created_by", + "label": "Filter by creator", + "options": [ + { "value": "all", "label": "Anyone" }, + { "value": "me", "label": "Me" }, + { "value": "others", "label": "Others" } + ] } ], "sort": { diff --git a/automations/interface.schema.json b/automations/interface.schema.json index e0461c8b..77725040 100644 --- a/automations/interface.schema.json +++ b/automations/interface.schema.json @@ -86,12 +86,13 @@ "description": "The filter dropdowns of the list page, in render order. Each filter id may appear once; the host's admission checks that.", "type": "array", "minItems": 1, - "maxItems": 2, + "maxItems": 3, "uniqueItems": true, "items": { "oneOf": [ { "$ref": "#/$defs/statusFilter" }, - { "$ref": "#/$defs/triggerFilter" } + { "$ref": "#/$defs/triggerFilter" }, + { "$ref": "#/$defs/createdByFilter" } ] } }, @@ -425,6 +426,40 @@ } } }, + "createdByFilter": { + "description": "The creator filter. Like the status filter, values name host-implemented predicates: me matches automations the signed-in user created, others every other automation. The host decides where the filter is shown and may hide it where every automation is the signed-in user's.", + "type": "object", + "additionalProperties": false, + "required": ["id", "label", "options"], + "properties": { + "id": { "const": "created_by" }, + "label": { + "description": "The control's accessible name.", + "$ref": "#/$defs/copy" + }, + "options": { + "description": "Each value may appear once; the host's admission checks that.", + "type": "array", + "minItems": 2, + "maxItems": 8, + "uniqueItems": true, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["value", "label"], + "properties": { + "value": { "enum": ["all", "me", "others"] }, + "label": { "$ref": "#/$defs/copy" } + } + }, + "contains": { + "type": "object", + "required": ["value"], + "properties": { "value": { "const": "all" } } + } + } + } + }, "sort": { "description": "The sort dropdown. Values name comparators the host implements. default must be one of the declared option values; the host's admission checks that, as schemas cannot cheaply.", "type": "object", diff --git a/tests/test_interface_manifest.py b/tests/test_interface_manifest.py index 3bb7b04c..d91d9647 100644 --- a/tests/test_interface_manifest.py +++ b/tests/test_interface_manifest.py @@ -30,6 +30,12 @@ def _with(mutation): return document +def _filter(document, filter_id): + return next( + f for f in document["pages"]["list"]["filters"] if f["id"] == filter_id + ) + + def test_schema_is_a_valid_draft_2020_12_schema(): jsonschema.Draft202012Validator.check_schema(SCHEMA) @@ -94,7 +100,23 @@ def test_featured_automations_resolve_to_catalog_entries(): ), ( "a status filter without the all option", - lambda doc: doc["pages"]["list"]["filters"][0]["options"].pop(0), + lambda doc: _filter(doc, "status")["options"].pop(0), + ), + ( + "a creator filter without the all option", + lambda doc: _filter(doc, "created_by")["options"].pop(0), + ), + ( + "a creator filter value the host does not implement", + lambda doc: _filter(doc, "created_by")["options"].append( + {"value": "team", "label": "My team"} + ), + ), + ( + "more filters than the host renders", + lambda doc: doc["pages"]["list"]["filters"].append( + {**doc["pages"]["list"]["filters"][0], "label": "Filter again"} + ), ), ], )