View Plugin — Python
A complete example of a remote view plugin in Python that adds a "Hello World" tab to the workspace dashboard.
Project Setup
mkdir hello-view-plugin && cd hello-view-plugin
python -m venv venv && source venv/bin/activate
pip install grpcio grpcio-tools
Generate Python code from the proto files:
python -m grpc_tools.protoc \
--proto_path=. \
--python_out=. \
--grpc_python_out=. \
proto/plugin.proto proto/view.proto
Full Source — main.py
import os
from concurrent import futures
import grpc
import plugin_pb2
import plugin_pb2_grpc
import view_pb2
import view_pb2_grpc
MANIFEST_HCL = """plugin "hello-view" {
version = "1.0.0"
description = "A minimal view plugin that displays Hello World."
author = "yourorg"
icon = "eye"
type = "remote"
}
"""
class HelloViewPlugin(
plugin_pb2_grpc.PluginServicer,
view_pb2_grpc.ViewProviderServicer,
):
"""Implements both Plugin and ViewProvider services."""
# --- Plugin service (required) ---
def Register(self, request, context):
return plugin_pb2.RegisterResponse(
name="hello-view",
capabilities=[plugin_pb2.VIEW_PROVIDER],
metadata={"version": "1.0.0"},
)
def Health(self, request, context):
return plugin_pb2.HealthResponse(status=plugin_pb2.SERVING)
def GetManifest(self, request, context):
return plugin_pb2.GetManifestResponse(manifest_hcl=MANIFEST_HCL)
# --- ViewProvider service ---
def ListViews(self, request, context):
return view_pb2.ListViewsResponse(
views=[
view_pb2.ViewDefinition(
name="hello",
label="Hello World",
icon="sparkles",
scope="workspace",
)
]
)
def RenderView(self, request, context):
if request.view_name != "hello":
return view_pb2.RenderViewResponse()
ws_id = request.workspace_id
hcl = f"""
component "greeting" {{
type = "text"
content = "Hello, World!"
style = "heading"
}}
component "info" {{
type = "text"
content = "This view is rendered by the hello-view plugin for workspace {ws_id}."
style = "muted"
}}
component "status" {{
type = "stat-group"
stat "uptime" {{
label = "Status"
value = "Online"
}}
stat "version" {{
label = "Version"
value = "1.0.0"
}}
}}
component "details" {{
type = "section"
title = "About This Plugin"
component "description" {{
type = "text"
content = "This is a minimal example of a VIEW_PROVIDER plugin. It uses the platform's HCL component library to render UI without writing any HTML or JavaScript."
}}
component "tip" {{
type = "alert"
message = "View plugins can compose any combination of text, stats, tables, forms, code blocks, and chat components."
level = "info"
}}
}}
"""
return view_pb2.RenderViewResponse(component_hcl=hcl)
def HandleRoute(self, request, context):
return view_pb2.RouteResponse(
status_code=404,
body=b'{"error":"not found"}',
)
def HandleStream(self, request_iterator, context):
context.abort(grpc.StatusCode.UNIMPLEMENTED, "streaming not supported")
def serve():
port = os.environ.get("PORT", "9011")
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
servicer = HelloViewPlugin()
plugin_pb2_grpc.add_PluginServicer_to_server(servicer, server)
view_pb2_grpc.add_ViewProviderServicer_to_server(servicer, server)
server.add_insecure_port(f"[::]:{port}")
server.start()
print(f"hello-view plugin listening on :{port}")
server.wait_for_termination()
if __name__ == "__main__":
serve()
Run It
python main.py
# hello-view plugin listening on :9011
Register in Workspace
workspace "my-workspace" {
plugin "hello-view" {
source = "remote://localhost:9011"
version = "1.0.0"
}
}
Start the workspace and navigate to its page. You'll see a "Hello World" tab. Click it to see the rendered view.
How It Works
- Platform calls
ListViews— discovers the"hello"view with scope"workspace" - Platform adds a tab labeled "Hello World" to the workspace page
- When the user clicks the tab, platform calls
RenderView(view_name="hello") - Your plugin returns HCL component definitions
- Platform parses the HCL and renders it to HTML using its component library
- The rendered HTML is injected into the tab content area
Adding Interactivity
View plugins can go far beyond static content. See the HCL Component Reference for:
chat— WebSocket-powered chat with the agentform— Forms that POST to your plugin's HandleRoutetable— Data tables that fetch from your plugin's API routesbutton-group— Action buttons that trigger API calls