Overview
PaneScale is a local desktop app, so its “API” is local too. There are four places where you can build on it:
| Surface | What it is for | Where it runs |
|---|---|---|
| MCP bridge | Let any MCP client read and change the task board, list workflows, notify you and call your managed MCP connections | 127.0.0.1, off by default |
| Workflow steps | Chain agents, model prompts, commands, browser checks, Git review, MCP tools and approvals | Inside PaneScale |
| Video scenes | Write product videos as small canvas functions on a timeline | Inside PaneScale |
| Status signals | Make any CLI report “working”, “needs you” and “done” to its pane | Your terminal |
MCP bridge
The bridge is a real MCP server using the Streamable HTTP transport with plain JSON responses. It only listens on the loopback interface and is off until you turn it on in Settings → MCP bridge.
| Property | Value |
|---|---|
| Endpoint | POST http://127.0.0.1:<port>/mcp |
| Port | The first free port from 41920 to 41929 — shown in Settings |
| Auth | Authorization: Bearer omcp_… — a 43-character token, compared in constant time |
| Limits | 1 MB request body; requests carrying a browser Origin header are refused |
| Token | Stored in a token file in your user profile; regenerate it any time in Settings |
Connect
Copy the URL and token from Settings → MCP bridge. For Claude Code:
claude mcp add --transport http panescale http://127.0.0.1:41920/mcp \
--header "Authorization: Bearer omcp_your_token"For clients that read a JSON configuration:
{
"mcpServers": {
"panescale": {
"type": "http",
"url": "http://127.0.0.1:41920/mcp",
"headers": { "Authorization": "Bearer omcp_your_token" }
}
}
}Tools
| Tool | Arguments | Notes |
|---|---|---|
panescale_list_tasks | status? | One of ready, running, needs_review, blocked, failed, stopped, done |
panescale_create_task | title, description?, labels?, project? | Creates a card on the board |
panescale_move_task | id, status, summary? | Any column except done — only a person finishes a card |
panescale_list_workflows | — | Saved workflows with their ids |
panescale_notify | title, text? | Shows a notification in PaneScale |
panescale_call_connection | connection, tool, arguments? | Calls a tool on one of your managed MCP connections |
A raw call looks like any other MCP request:
curl -s http://127.0.0.1:41920/mcp \
-H "Authorization: Bearer omcp_your_token" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"panescale_create_task","arguments":{"title":"Add an empty state to uploads","labels":"ui"}}}'Workflow steps
A workflow is a brief plus up to 16 steps, run left to right. Every step can store its output under a key; later steps reference it as {{key}}.
| Kind | Runs | Input | Default timeout |
|---|---|---|---|
agent | Claude Code or Codex, optionally allowed to edit | Instruction | 30 min |
model | A prompt to Ollama or your own endpoint | Prompt | 5 min |
command | A shell command in the workspace | Command line | 3 min |
browser | Fetches a page and returns its title and a text preview (30 s, 200 KB) | URL | 3 min |
git | Checks the working tree diff | — | 3 min |
mcp | Calls a tool on a managed connection | Connection, tool, arguments | 3 min |
approval | Waits for your decision | — | — |
Steps can run in parallel groups, but not approvals or editing agents — those always run alone. A step can have a condition on an earlier output: contains, not_contains, equals or not_empty. Outputs can be validated as JSON.
Triggers: manual, interval or git_change (a new commit in the workspace).
Video scenes
A video is a folder: timeline.json plus one file per scene in scenes/<id>.js. Each scene file is the body of a function that draws one frame onto a 2D canvas. The background is already filled with theme.bg; the camera, transitions and captions are applied around your code.
// Parameters: ctx, t, w, h, theme, u — t runs from 0 to 1 within the scene.
u.backdrop({ grid: true });
// Words rise in one after another; "|" breaks the line.
u.words('Ship it|tonight', w / 2, h * 0.45, u.seg(t, 0.05, 0.5), {
size: u.min * 0.11, weight: 760, align: 'center', maxWidth: w * 0.86,
});
// A gradient underline that grows in, then a counter.
const lw = u.min * 0.22 * u.easeInOut(u.seg(t, 0.35, 0.6));
ctx.fillStyle = u.grad(w / 2 - lw / 2, 0, w / 2 + lw / 2, 0);
u.rr(w / 2 - lw / 2, h * 0.62, lw, u.min * 0.008, u.min * 0.004);
ctx.fill();
u.text(u.count(20000, u.seg(t, 0.4, 0.9), { suffix: '+ teams' }), w / 2, h * 0.72, {
size: u.min * 0.04, color: theme.muted, align: 'center',
});| Name | Meaning |
|---|---|
theme | bg, surface, text, muted, accent, accent2 (brand colors already applied) |
u.seg(t, a, b), u.stagger(t, i, n) | Progress between two moments · staggered entrances |
u.easeOut, u.easeInOut, u.backOut, u.spring, u.elastic | Easing curves on 0…1 |
u.backdrop(), u.panel(), u.grad() | Lit background with grid and vignette · card with shadow · gradient |
u.text(), u.words(), u.type(), u.count() | Text · word-by-word reveal · typewriter · counter |
u.browser(), u.phone(), u.terminal(), u.code(), u.cursor() | Device frames that return their content area, a typing shell and editor, a clicking pointer |
u.icon(name, x, y, size) | check, bolt, star, play, arrow, shield, clock, spark, grid, chat, chart, lock |
u.min, u.vw(p), u.vh(p), u.portrait | Sizes relative to the frame — lay out 9:16 differently with u.portrait |
u.rand(n), u.noise(x) | Deterministic randomness |
u.sec, u.dur, u.frame, u.fps | Seconds into the scene, its length, absolute frame, frame rate |
The timeline itself is plain JSON:
{
"format": "16:9",
"height": 1080,
"fps": 30,
"theme": "graphit",
"brand": { "accent": "#1463ff" },
"captions": true,
"caption_style": "kinetic",
"sfx_auto": true,
"scenes": [
{
"id": "title",
"kind": "type",
"title": "Title",
"duration_secs": 3.4,
"transition": "zoom",
"transition_secs": 0.5,
"caption": "Every agent in one window",
"camera": [{ "at": 0, "zoom": 1 }, { "at": 3, "zoom": 1.12, "x": 0.04, "ease": "inout" }],
"sfx": [{ "at": 1.2, "type": "pop", "gain": 0.6 }],
"code": ""
}
],
"notes": [{ "id": "n1", "at": 2.4, "text": "Logo bigger" }]
}format:16:9,9:16,1:1or4:5·height: the short side, 720 to 2160 ·fps: 24 to 60theme:nacht,graphit,aurora,ozean,wald,sonne,hell,papier·brandoverrides single colorstransition(how a scene enters, overlapping the one before):cut,fade,dip,push,slide,zoom,whip,wipe,circle,sweep,glitch,blur·direction:left,right,up,downcamerakeys:x/yas a share of the frame,zoom(1 = none),rotatein degrees,ease:linear,out,inout,springsfx:whoosh,riser,impact,click,pop,swell,tick,chime— generated locally, no audio filescaption_style:kinetic,plainorbold·kind:motion,type,chart,code,device,end
Scenes must be deterministic (no Math.random, Date.now or performance.now — use u.rand) and must not call ctx.setTransform, because the camera uses the transform. During export, numbers and typed text are frozen per frame so motion blur never smears digits.
Status signals
PaneScale shows each pane as working, waiting for you or done. Claude Code reports through hooks that PaneScale configures for its own panes. For every other CLI, these signals work:
- OSC 9 and OSC 777 terminal notifications — many CLIs already send them when they need input or finish a task.
- Prompt detection on the visible screen as a fallback for text-mode CLIs.
If you build your own CLI, send a desktop notification escape sequence when you need the user:
printf '\033]9;Waiting for approval\007'