These message types extend the procMessenger WebSocket protocol and are implemented exclusively by the branchShredder application. All messages follow the standard procMessenger envelope (see the procMessenger
Protocol.mdfor the full envelope schema and shared message types).
When branchShredder connects to a procMessenger server it registers with the following capabilities:
edit_story llm_chat query_nodes find_nodes get_node update_node
system_prompt system viewport viewport_snapshot viewport_info viewport_tap
This document covers the viewport remote-control additions (viewport,
viewport_snapshot, viewport_info, viewport_tap). All other
capabilities (edit_story, llm_chat, query_nodes, find_nodes,
get_node, update_node, system_prompt, system) are described in the
procMessenger shared protocol document.
branchShredder exposes three message types that let any connected client (e.g. a mobile app) remotely control the graph viewport and receive PNG screenshots in response.
Execute a sequential pipeline of viewport commands. The pipeline may move the camera, zoom, centre on a node, and capture a PNG snapshot — all in a single round-trip.
Request (any client → branchShredder):
{
"id": "uuid-v4",
"type": "viewport",
"source": "mobileApp",
"target": "branchShredder",
"timestamp": "2026-04-30T12:00:00Z",
"flags": {},
"payload": {
"commands": [
{"Move": [10, -15]},
{"zoom": 0.98},
{"viewport": "Render", "output": "WebSocket"}
]
}
}Response — with image (branchShredder → requester):
{
"type": "viewport",
"payload": {
"status": "complete",
"image": "<base64-encoded PNG>",
"format": "png",
"viewportState": {
"x": 0.0,
"y": 0.0,
"zoom": 1.0
}
}
}Response — without image (no Render command in pipeline):
{
"type": "viewport",
"payload": {
"status": "complete",
"viewportState": {
"x": 0.0,
"y": 0.0,
"zoom": 1.0
}
}
}Response — error:
{
"type": "viewport",
"payload": {
"status": "error",
"error": "No viewport is available"
}
}Each element in commands is an object. Multiple keys may appear in one
element; they are processed in insertion order.
| Key | Value type | Description |
|---|---|---|
Move |
[dx, dy] |
Pan the viewport by dx, dy scene-coordinate units. Positive X moves right; positive Y moves down. |
zoom |
number |
Multiply the current zoom level by this factor. Values < 1 zoom out, > 1 zoom in. (e.g. 0.98 = zoom out 2%) |
center |
[x, y] |
Centre the viewport on the absolute scene position [x, y]. |
center_node |
string |
Centre the viewport on the node whose id matches this string. Searches subnets recursively. |
viewport |
"Render" |
Capture the current viewport as a PNG. This triggers the image response. Must be last in the pipeline if an image is desired. |
output |
"WebSocket" |
Declares where to deliver the rendered image. Currently only "WebSocket" is supported. |
width |
number |
Maximum output image width in pixels. Aspect ratio is preserved. |
height |
number |
Maximum output image height in pixels. Aspect ratio is preserved. |
viewportStateis always included in everyviewportresponse (with or without aRendercommand), giving the final centre position and zoom after all pipeline commands have executed. See Viewport State below.
Pan right, zoom out 2%, render:
[
{"Move": [10, -15]},
{"zoom": 0.98},
{"viewport": "Render", "output": "WebSocket"}
]Centre on a known node and capture at 1280 × 720:
[
{"center_node": "abc123"},
{"width": 1280, "height": 720},
{"viewport": "Render", "output": "WebSocket"}
]Move, zoom in, then render at full viewport resolution:
[
{"Move": [50, 0]},
{"zoom": 1.1},
{"viewport": "Render"}
]Convenience shorthand for "centre on a node → capture → return PNG".
Equivalent to composing a viewport pipeline manually.
Request (any client → branchShredder):
{
"id": "uuid-v4",
"type": "viewport_snapshot",
"source": "mobileApp",
"target": "branchShredder",
"timestamp": "2026-04-30T12:00:00Z",
"flags": {},
"payload": {
"nodeId": "abc123",
"zoom": 1.5,
"width": 1280,
"height": 720
}
}All payload fields are optional:
| Field | Type | Description |
|---|---|---|
nodeId |
string | ID of the node to centre on before capturing. Omit to capture the current viewport as-is. |
zoom |
number | Absolute zoom scale factor to apply before capturing (replaces current zoom). |
width |
number | Maximum output image width in pixels. |
height |
number | Maximum output image height in pixels. |
Response — success:
{
"type": "viewport_snapshot",
"payload": {
"status": "complete",
"image": "<base64-encoded PNG>",
"format": "png",
"nodeId": "abc123",
"viewportState": {
"x": 0.0,
"y": 0.0,
"zoom": 1.5
}
}
}nodeId is null when no node was requested. viewportState always reflects the
final centre and zoom after the snapshot was taken.
Response — error:
{
"type": "viewport_snapshot",
"payload": {
"status": "error",
"error": "No viewport is available"
}
}Query the full list of available viewport commands and their descriptions. Useful for auto-discovery by remote clients.
Request (any client → branchShredder):
{
"id": "uuid-v4",
"type": "viewport_info",
"source": "mobileApp",
"target": "branchShredder",
"timestamp": "2026-04-30T12:00:00Z",
"flags": {},
"payload": {}
}Response:
{
"type": "viewport_info",
"payload": {
"status": "complete",
"commands": {
"Move": {
"description": "Pan the viewport by [dx, dy] offset in scene coordinates.",
"value": "[number, number]",
"example": {"Move": [10, -15]}
},
"zoom": {
"description": "Multiply the current zoom level by the given factor. Values < 1 zoom out, values > 1 zoom in (e.g. 0.98 = zoom out 2%).",
"value": "number",
"example": {"zoom": 0.98}
},
"center": {
"description": "Center the viewport on the given absolute [x, y] scene position.",
"value": "[number, number]",
"example": {"center": [0, 0]}
},
"center_node": {
"description": "Center the viewport on the node with the given node ID.",
"value": "string (nodeId)",
"example": {"center_node": "abc123"}
},
"viewport": {
"description": "Control viewport rendering. \"Render\" captures a PNG snapshot of the current viewport.",
"values": ["Render"],
"example": {"viewport": "Render"}
},
"output": {
"description": "Set the output destination for a rendered image.",
"values": ["WebSocket"],
"example": {"output": "WebSocket"}
},
"width": {
"description": "Maximum output image width in pixels (aspect ratio preserved).",
"value": "number",
"example": {"width": 1280}
},
"height": {
"description": "Maximum output image height in pixels (aspect ratio preserved).",
"value": "number",
"example": {"height": 720}
}
},
"messageTypes": {
"viewport": "Execute a command pipeline. Send a list of command objects; include {\"viewport\": \"Render\"} to receive a PNG in the response.",
"viewport_snapshot": "Convenience shorthand: optionally centre on nodeId, then capture and return a PNG.",
"viewport_info": "Return this command reference."
},
"pipelineExample": [
{"Move": [10, -15]},
{"zoom": 0.98},
{"viewport": "Render", "output": "WebSocket"}
]
}
}Every response from viewport, viewport_snapshot, and viewport_tap
includes a top-level viewportState object so that the remote client can
stay in sync with the current camera state without polling.
| Field | Type | Description |
|---|---|---|
x |
number |
Scene-coordinate X of the viewport centre. |
y |
number |
Scene-coordinate Y of the viewport centre. |
zoom |
number |
Current uniform zoom scale factor. 1.0 = default 1:1. Values > 1 are zoomed in; values < 1 are zoomed out. |
pixelWidth |
number |
Current native width of the viewport widget in pixels. Use this as imageWidth when sending a viewport_tap. |
pixelHeight |
number |
Current native height of the viewport widget in pixels. Use this as imageHeight when sending a viewport_tap. |
"viewportState": {
"x": 142.5,
"y": -80.0,
"zoom": 1.25,
"pixelWidth": 900,
"pixelHeight": 600
}Tap-coordinate workflow: capture a snapshot (sending no
width/heightto get the full native resolution), readpixelWidth/pixelHeightfrom the returnedviewportState, display the image, then send those same values asimageWidth/imageHeightin aviewport_tapmessage.
Select the node at a tapped pixel position within a previously delivered
viewport image. branchShredder scales the tap coordinates back to viewport
widget space, hit-tests the scene, selects the node (the inspector and
sidebar update exactly as though it was clicked with a mouse), and returns
the node's data in the same shape as get_node.
Request (any client → branchShredder):
{
"id": "uuid-v4",
"type": "viewport_tap",
"source": "mobileApp",
"target": "branchShredder",
"timestamp": "2026-04-30T12:00:00Z",
"flags": {},
"payload": {
"x": 320,
"y": 240,
"imageWidth": 900,
"imageHeight": 600
}
}| Field | Required | Description |
|---|---|---|
x |
yes | Pixel X of the tap within the displayed image. |
y |
yes | Pixel Y of the tap within the displayed image. |
imageWidth |
yes | Actual pixel width of the image that was displayed. Use viewportState.pixelWidth from the snapshot response for a 1:1 match when no scaling was requested. |
imageHeight |
yes | Actual pixel height of the image that was displayed. Use viewportState.pixelHeight similarly. |
Response — node found:
{
"type": "viewport_tap",
"payload": {
"status": "complete",
"node": {
"nodeId": "abc123",
"name": "Intro Scene",
"type": "Info",
"content": "Markdown content of the node",
"stageNotes": "Director notes",
"selectedCharacters": ["Alice", "Bob"]
},
"image": "<base64-encoded PNG>",
"format": "png",
"viewportState": {
"x": 142.5,
"y": -80.0,
"zoom": 1.25,
"pixelWidth": 900,
"pixelHeight": 600
}
}
}Response — tap landed on empty space (node is null):
{
"type": "viewport_tap",
"payload": {
"status": "complete",
"node": null,
"image": "<base64-encoded PNG>",
"format": "png",
"viewportState": { "x": 0.0, "y": 0.0, "zoom": 1.0, "pixelWidth": 900, "pixelHeight": 600 }
}
}The image is always returned — it reflects the viewport state after the tap
(with the newly selected node highlighted, or all nodes deselected when the
tap hit empty space). The node field is null when the tap hit empty space.
Response — error:
{
"type": "viewport_tap",
"payload": {
"status": "error",
"error": "x, y, imageWidth, and imageHeight are required"
}
}- Send a
viewport_snapshot(nowidth/heightto get native resolution). - Read
viewportState.pixelWidthandviewportState.pixelHeightfrom the response. - Display the image at any size in your WebView.
- When the user taps, scale the tap back to native image coordinates:
native_x = tap_x * (pixelWidth / displayedWidth) native_y = tap_y * (pixelHeight / displayedHeight) - Send
viewport_tapwithimageWidth: pixelWidth,imageHeight: pixelHeight,x: native_x,y: native_y.
All PNG images returned over WebSocket are base64-encoded and carried in
the "image" field of the response payload. The "format" field is always
"png".
Decoding example (JavaScript):
const bytes = Uint8Array.from(atob(payload.image), c => c.charCodeAt(0));
const blob = new Blob([bytes], { type: "image/png" });
const url = URL.createObjectURL(blob);Decoding example (Python):
import base64
png_bytes = base64.b64decode(payload["image"])The full capability list that branchShredder advertises in its register
message:
[
"edit_story",
"llm_chat",
"query_nodes",
"find_nodes",
"get_node",
"update_node",
"system_prompt",
"system",
"viewport",
"viewport_snapshot",
"viewport_info",
"viewport_tap"
]