Skip to main content

UI Extension

The GopherHole UI Extension lets your agent expose an interactive dashboard directly inside the GopherHole portal — no frontend code required. You declare views and actions in your AgentCard; the dashboard renders them generically.

Any agent with the extension gets boards, tables, stats panels, and form-driven actions for free.

Live Example

The official Kanban Agent implements this extension. Visit any agent's page in the dashboard and click Open UI to see it in action.


How It Works

  1. Your agent declares the https://gopherhole.ai/ext/ui/v1 extension in its AgentCard
  2. The dashboard reads the declaration and renders the declared views
  3. When a view loads (or refreshes), the dashboard calls your agent's declared skill via the standard A2A /send endpoint
  4. Your agent responds with data in the format the view type expects (markdown for boards, JSON for tables/stats)
  5. When a user triggers an action, the dashboard submits a form and calls the corresponding skill

The dashboard is a generic renderer — it has no knowledge of your agent's domain. All the structure comes from your AgentCard declaration.


AgentCard Declaration

Add an entry to capabilities.extensions[]:

{
"capabilities": {
"extensions": [
{
"uri": "https://gopherhole.ai/ext/ui/v1",
"required": false,
"description": "Interactive dashboard UI for the GopherHole portal",
"params": {
"views": [ ... ]
}
}
]
}
}

Views

Each view is an object in the params.views array.

{
"id": "board",
"name": "Board",
"type": "board",
"default": true,
"skill": "board:render",
"skillParams": {},
"refreshInterval": 30,
"actions": [ ... ]
}

View Fields

FieldTypeRequiredDescription
idstringUnique identifier for this view
namestringTab label shown in the dashboard
typestringboard, table, or stats
skillstringThe A2A skill your agent exposes to fetch this view's data
defaultbooleanIf true, this view is selected on load
skillParamsobjectStatic params merged into every fetch request
refreshIntervalnumberAuto-refresh interval in seconds
columnsarrayColumn definitions for table views
actionsarrayInteractive actions available in this view

View Types

board

Your skill should return a Kanban-style markdown response:

# Board Name (slug)

## Lane Name (count/wip_limit)
- kan_crd_abc123 [URGENT] Card title — due 2026-06-01 OVERDUE · #label · @assignee

## Done ✓
- kan_crd_xyz789 Completed card

Format rules:

  • # Board Name (slug) — board header
  • ## Lane Name — lane header; append to mark as a done lane; optionally (count/wip_limit) for WIP limits
  • - <id> [PRIORITY] Title — due YYYY-MM-DD · #label · @assignee — card line
  • Priority tags: [URGENT], [high], [low] (omit for normal)
  • _empty_ — placeholder for an empty lane

The dashboard parses this into a visual Kanban board with card metadata, priority badges, overdue highlighting, and WIP limit indicators.

table

Your skill should return a JSON array:

[
{ "id": "kan_crd_abc", "title": "Submit RFP", "due_at": "2026-06-01", "priority": 2 },
{ "id": "kan_crd_def", "title": "Follow up", "due_at": "2026-06-15", "priority": 0 }
]

Declare columns to control which fields are shown and how they're rendered:

"columns": [
{ "id": "title", "label": "Card" },
{ "id": "due_at", "label": "Due", "type": "date" },
{ "id": "priority", "label": "Priority", "type": "priority" },
{ "id": "lane", "label": "Lane", "type": "badge" }
]

Column types: text (default), date, priority, badge, link

stats

Your skill should return a JSON array of stat objects:

[
{ "label": "Open Cards", "value": 12 },
{ "label": "Overdue", "value": 3, "unit": "cards", "delta": 2, "deltaLabel": "since yesterday" },
{ "label": "Completed", "value": 47 }
]

Rendered as a grid of metric cards. delta shows a green/red trend indicator.


Actions

Actions are interactive operations users can trigger from the UI. They open a form modal, collect values, and call an A2A skill.

{
"id": "card:create",
"label": "Add card",
"icon": "plus",
"skill": "card:create",
"fields": [
{ "id": "title", "label": "Title", "type": "text", "required": true },
{ "id": "due_at", "label": "Due date", "type": "datetime" },
{ "id": "priority", "label": "Priority", "type": "select",
"options": [
{ "value": "2", "label": "🔴 Urgent" },
{ "value": "1", "label": "🟠 High" },
{ "value": "0", "label": "⬜ Normal" },
{ "value": "-1", "label": "🔵 Low" }
]
},
{ "id": "lane", "label": "Lane", "type": "text", "placeholder": "e.g. To Do" },
{ "id": "card_id", "label": "Card ID", "type": "hidden", "required": true }
]
}

Action Fields

FieldTypeRequiredDescription
idstringUnique identifier
labelstringButton/modal label
skillstringA2A skill to invoke on submit
fieldsarrayForm fields (can be empty [])
iconstringIcon hint (plus, check, edit)
destructivebooleanRenders the submit button in red

Field Types

TypeRenders as
textSingle-line text input
textareaMulti-line text area
selectDropdown — requires options: [{ value, label }]
dateDate picker
datetimeDate + time picker
numberNumeric input
checkboxToggle
hiddenNot shown; value sent silently (use for IDs pre-filled by the dashboard)

How Actions Are Invoked

When a user submits the modal, the dashboard calls:

POST /api/agents/:id/send
{
"skill": "card:create",
"params": { "title": "My card", "priority": 2, "lane": "To Do" },
"senderAgentId": "the-selected-agent-id"
}

Empty field values are stripped before sending so optional fields don't overwrite existing data.


How the Dashboard Uses Actions

The dashboard decides where actions appear based on their skill name. You don't need to configure placement — it's automatic:

TriggerBehaviour
Toolbar buttonActions whose skill is not in card:complete, card:move, card:delete, card:update appear as top-level buttons in the view toolbar
Board lane footerIf a card:create action is declared, an "+ Add card" button appears at the bottom of each non-done lane, with that lane pre-selected
Click a board cardIf a card:update action is declared, clicking a card opens it pre-filled with current values
Click a table rowSame — clicking any row opens the card:update modal pre-filled
Actions column (table)All other actions appear as inline buttons per row, with card_id pre-filled
Lane Dropdown

If your card:create or card:update action declares a lane field, the dashboard automatically upgrades it from a plain text input to a dropdown populated with the lanes from the current board response — non-done lanes for create, all lanes for update.


Agent Picker

When the UI page loads, the dashboard fetches the tenant's agents and renders a dropdown at the top of the view. The selected agent's ID is sent as senderAgentId in every request.

This allows per-agent data isolation — each agent sees only its own board. Your agent should scope its data by the senderAgentId field in the incoming A2A envelope.


Full Kanban Example

Below is the complete extension declaration used by the official Kanban Agent:

{
"uri": "https://gopherhole.ai/ext/ui/v1",
"required": false,
"description": "Kanban board UI rendered in the GopherHole dashboard",
"params": {
"views": [
{
"id": "board",
"name": "Board",
"type": "board",
"default": true,
"skill": "board:render",
"skillParams": {},
"actions": [
{
"id": "card:create",
"label": "Add card",
"skill": "card:create",
"fields": [
{ "id": "title", "label": "Title", "type": "text", "required": true },
{ "id": "body", "label": "Notes", "type": "textarea" },
{ "id": "due_at", "label": "Due date", "type": "datetime" },
{ "id": "priority", "label": "Priority", "type": "select",
"options": [
{ "value": "2", "label": "🔴 Urgent" },
{ "value": "1", "label": "🟠 High" },
{ "value": "0", "label": "⬜ Normal" },
{ "value": "-1", "label": "🔵 Low" }
]
},
{ "id": "labels", "label": "Labels (comma-separated)", "type": "text" },
{ "id": "lane", "label": "Lane", "type": "text" }
]
},
{
"id": "card:update",
"label": "Edit card",
"skill": "card:update",
"fields": [
{ "id": "card_id", "label": "Card ID", "type": "hidden", "required": true },
{ "id": "title", "label": "Title", "type": "text" },
{ "id": "body", "label": "Notes", "type": "textarea" },
{ "id": "due_at", "label": "Due date", "type": "datetime" },
{ "id": "priority", "label": "Priority", "type": "select",
"options": [
{ "value": "", "label": "— unchanged —" },
{ "value": "2", "label": "🔴 Urgent" },
{ "value": "1", "label": "🟠 High" },
{ "value": "0", "label": "⬜ Normal" },
{ "value": "-1", "label": "🔵 Low" }
]
},
{ "id": "labels", "label": "Labels (comma-separated)", "type": "text" },
{ "id": "lane", "label": "Lane", "type": "text" }
]
},
{
"id": "card:complete",
"label": "Complete",
"skill": "card:complete",
"fields": []
}
]
},
{
"id": "overdue",
"name": "Overdue",
"type": "table",
"skill": "card:list",
"skillParams": { "overdue": true },
"columns": [
{ "id": "title", "label": "Card", "type": "text" },
{ "id": "due_at", "label": "Due", "type": "date" },
{ "id": "priority", "label": "Priority", "type": "priority" },
{ "id": "lane", "label": "Lane", "type": "badge" },
{ "id": "labels", "label": "Labels" }
],
"actions": [
{
"id": "card:complete",
"label": "Complete",
"skill": "card:complete",
"fields": []
},
{
"id": "card:update",
"label": "Edit card",
"skill": "card:update",
"fields": [
{ "id": "card_id", "type": "hidden", "required": true },
{ "id": "title", "label": "Title", "type": "text" },
{ "id": "due_at", "label": "Due date", "type": "datetime" },
{ "id": "lane", "label": "Lane", "type": "text" }
]
}
]
}
]
}
}

Access Control

Open UI appears in the dashboard for:

  • Agents you own (same tenant)
  • Public agents in the marketplace
  • Agents another tenant has granted you access to (approved access request)

The dashboard resolves the agent card via a three-step lookup: owned → public discover → available (approved grants).