Unreal Engine gives game makers a level to build in, actors that live there, Blueprints to wire them together and a debugger to watch them run. Portal is that for AI agents: the canvas is the level, every agent, terminal and document is a tile, and typed wires connect them.
Tiles are the actors; wires are the Blueprint. Each pin has a type, so a wire only connects what makes sense — a finished command, text, a document or an image — and values arrive in order. Filters and review gates sit on the wire itself, and every agent or chat node runs on the same harness.
Every request captured in full; a breakpoint holds the next one before it leaves.
Unreal Engine and Blueprint are trademarks of Epic Games, Inc. The comparison is an analogy; Portal isn’t affiliated with Epic.
Plots · 2D and 3D
Ask for a chart. Get an instrument.
Ask to “compare Q3 revenue and cost by region” and a table arrives — then stands up as a 3D chart you can orbit. Drag the price: revenue follows, cost stays put. In 2D or 3D, every plot is a native surface with pan, zoom, a crosshair and a legend, and it keeps its exact data.
Portal gathers the skills your tools already have — Claude Code, Codex, Cursor, Gemini, OpenCode and more — into one library of SKILL.md folders, read in place. Filter them from any chat, see whose each one is, grant or forbid them per scope, read a learned skill before it joins, and keep the ones that measurably cut failures.
One harness is mounted into every agent you run. Agents work where you work; anything that changes your world comes back to you first.
One harness — rules, skills, tools and memory — is mounted into every agent you run. The agents work where you work, and anything that changes your world comes back to you as a proposal.
4agent runtimes, one kind of tile
6scopes in the harness cascade
11skill folders read in place
3tool effects: read, staged, mutating
01 · COMPARED
One workspace, every model.
Single-vendor apps choose the model family, the agent and the surface for you. Portal lets you combine them — and keeps them sharing one harness.
The Claude app is built around Claude, the ChatGPT app around OpenAI’s models, Warp around the terminal and its own agent, Muse around boards for thinking. Portal is built around the combination: any model, any coding agent, and the terminals and canvas they work in — with one set of rules, skills, tools and memory for all of them.
Each of these apps is excellent at what it is built around. Portal is built around the combination: any model, any agent CLI, and the terminals and canvas they work in — sharing one harness.
Every one of these tools is good at what it is built for. The difference is scope — how many models, agents and surfaces live in one place, and whether they share what they know.
Portal
Warp
Claude app
ChatGPT app
Muse · Allume
Built around
agents, models, terminals and a canvas
the terminal and Warp’s agent
chat, Cowork and Claude Code
chat, Work and Codex
boards for thinking
Models
any: DeepSeek, GLM and MiMo built in; any OpenAI-compatible endpoint; local models; Claude through Claude Code
OpenAI, Anthropic, Google, xAI and hosted open models, for Warp’s agent
Claude
OpenAI’s
connects to AI tools through MCP
Coding agents
Claude Code, Codex, OpenCode and your own CLI, side by side
Warp’s agent, locally and in the cloud
Claude Code
Codex
—
Terminal
durable terminals; every command a typed record
blocks with command, output, exit code and duration
Blocks, history and an agent in the terminal — kept. Portal adds what surrounds them: the agent CLIs you already use as first-class tiles, terminals that outlive the app, and one harness every agent reads.
From the Claude app
Claude Code runs here as a tile with its own login, beside Codex and OpenCode, reading the same rules, skills and memory — while chats on the same canvas can use other models.
From the ChatGPT app
Codex runs here too, next to Claude Code, and any OpenAI-compatible endpoint — OpenAI’s included — can back a chat, a review or a second opinion.
From Muse
A calm canvas for thinking, and Portal’s canvas is one too — it also runs things: terminals, agents, live figures, plots and proofs, side by side with your notes.
02 · TERMINALS
Terminals that remember.
Every terminal is owned by a daemon, not a window. Commands become typed records, risk shows up before Enter, and a failure is one keystroke from its fix.
A small per-user terminal daemon owns every PTY; the tile on the canvas is only a reconnecting client. Close the tile, quit the app or crash it — the shell keeps running, and the next launch attaches to the same session and replays what it missed.
Closing a tile — or the whole app — only detaches the client. The daemon-owned process keeps running; reopening attaches to the same session and replays the missed output, resizes and commands.
When this happens
You get
Close a tile or switch boards
The process keeps running and is recovered on the next launch.
Quit or crash the app
The PTY stays live; the next launch reconnects to the same PID and journal cursor.
The socket drops
The tile says it is disconnected; Reconnect catches up from the last applied event.
The daemon or machine stops
A kernel PTY can’t survive that. The journal becomes an honest archived transcript — it never claims the old process is alive.
For zsh, bash and fish, Portal adds temporary shell hooks that chain to your own configuration and mark where each command starts and ends. The daemon sees those marks even while the app is closed, so every finished command becomes a durable value — one you can wire, filter and search. Warp made blocks familiar; here a block is also a record that outlives the app and can be wired straight to an agent.
zsh · ~/work/appCommandResult
A~/work/app % pytest -q tests/api
C..F.
FAILED tests/api/test_auth.py::test_refresh
1 failed, 3 passed in 2.41s
Dexit 1
commandpytest -q tests/apicwd~/work/appexit_code1duration_s2.41objectspytest case × 4risklow
The shell marks the prompt (A), the output (C) and the finish (D). What arrives on the other side is a typed record, not a screen of text.
Objects are recognised in the output — files and Git states, containers, processes, pods, pytest cases, deployments, URLs. environment is a safe host, folder and shell identity, never a dump of your variables.
Blocks, not scrollback. Each command folds into a line with its exit, time and output count.Compared with last time. ls artifacts, run twice: one new file since the previous run.
Risk is classified low, medium, high or critical from the command and its environment. It routes approvals; it is not a sandbox. Shells without reliable hooks keep durable PTYs and transcripts, but never get invented exit codes.
The prompt suggests the likely next command in grey. For commands that change the world — rm, git push, kubectl, terraform — a preflight line says what would happen, on which host, before you commit to it. Commands that failed here before say so as you type.
cat · ~/work/appprod-nl
anton@prod-nl /work % git reset --hard origin/mainmake test
preflightgit reset --hard origin/main
Discard uncommitted work · moves to origin/main · on prod-nl
A predicted command waits in grey; a destructive one gets a preflight line naming its effect and the host it will run on.
The next command, in grey. The host and its latency ride along: nl-vps · 240 ms.Preflight, on the line itself. “Discard uncommitted work · origin/main.”
Warned before Enter. “often fails · 2 of 2” — the prompt remembers which pushes were rejected here, as you type.
⌥E opens the latest failure as a side sheet — the top source, diagnostics, failing tests — with three ways forward: ask an AI with the evidence attached, create an agent mission, or open the raw block. If something fixed this error here before, the block says so and offers to replay it.
A failed block is a starting point, not a dead end: ask what usually fixes it, capture the fix as a reusable card, or open a mission for an agent with the failure attached.
Fixed in the loop. An agent reads the failing build, patches the file and reruns it until it is green.
Incident Lens. cargo test, exit 101: the diagnostic, the failing test, and what to do next.The usual fix. “fix: git checkout -- src/x.cpp” rides next to the error it fixed before.Fixed before. Replay the steps that worked, from terminal memory.A fixer beside the failure. A one-hunk patch for a compile error, ready to apply.
Draw a wire from a terminal to an agent or chat and each finished command travels as a typed record, in order. Filter by exit status, objects, host or risk; high-risk records wait as approval cards; text routed into a terminal is pasted, never run.
Wires carry typed values. A Terminal → Agent wire takes each finished command in order, drops what the filter doesn't want, and stages anything high-risk as a card you approve. Text routed into a terminal is pasted, never run.
Value
Can flow to
CommandResult
Telegram chat · AI · Agent · Markdown artifact
Text
Telegram chat · Terminal · AI · Agent
Artifact
AI · Agent
Image
Agent
Pipelines on the board. ⌥L lifts ps aux | grep | wc onto the canvas, one stage per tile, with line counts.
Each wire keeps its own cursor, advanced only when the destination accepts. If a crash lands between a remote side effect and its acknowledgement, the wire returns as uncertain — you choose Retry or Treat as delivered — rather than risking a duplicate.
SSH hosts open as ordinary terminal tiles. First contact shows the server’s fingerprint to compare; forwarded ports become chips you can open; transfers show inline; a dropped link reconnects and restores the session. The same harness and memory follow you onto the host.
Trust on first use — visibly. Compare the SHA256 fingerprint before connecting.Ports as chips. localhost:3000 and :5173 from dev-linux, one click to open.Transfers, inline. An upload, and a stalled download said plainly.Reconnecting, not gone. The session is restored where it was.
Connect a Vagabond host and every coding agent Portal launches can create machines — containers, Firecracker microVMs, QEMU/KVM VMs or full Linux desktops. Each one appears on the canvas beside the agent that made it. The key stays in your OS keychain, and every side effect is written to a compute journal, attributed to its owner.
Machines are terminals too: each one opens as a tile, carries the same harness, and can be forked, raced against a sibling, or grown into a fleet.
Fork a live microVM. Copies resume with the same files and running processes.Access is asked for. An agent requests push rights for one hour, on one machine.A fleet is one tile. 48 containers and 6 desktops, with status and budget.Connect, verify, use. Three steps; the key goes to your keychain.
33 s200 containers, workspace uploaded once
1.7 sone command answered across all 200
8 sone microVM forked into 16, processes running
5 s4 desktops with live thumbnails
Measured end to end through the agent tools on a single-node lab host.
Twenty machines, one request. An agent asks for a fleet, you approve, it runs — then the machines are gone.03 · CODING AGENTS
Every coding agent, one kind of tile.
Claude Code, Codex, OpenCode or any CLI you name. Portal drives each through its own structured interface, so you read tool calls instead of scrollback — and every one of them gets the same harness.
The New agent dialog picks a runtime, a folder and a model. Creating the tile has no side effects — the process starts with your first prompt, under its own login and its own model — and Portal reads each runtime’s structured output instead of scraping its screen. The Claude app runs Claude Code and the ChatGPT app runs Codex; here both run side by side — with OpenCode and your own CLI — and every one of them gets the same harness.
Claude Code guards before
A persistent session, resumed rather than restarted.
One structured run per prompt, sandboxed to the workspace.
Driven as
codex exec --json --sandbox workspace-write
Harness
CODEX_HOME/AGENTS.md · a memory server for each run
Tools
memory and machine servers
OpenCode guards before
JSON events from each run, any provider/model it supports.
Driven as
opencode run --format json
Harness
OPENCODE_CONFIG · plugin with the known fix on failure
Tools
memory server
Custom CLI your argv
Your own agent, wrapper or internal tool.
Driven as
your command + an argument template: {prompt} · {session}
Started
with execvp — never through a shell
Good for
an internal agent, a wrapper script, a model you host
Creating an agent tile has no side effects — the process starts with your first prompt. Portal reads the runtime's structured output, so tool calls and edits arrive as rows, not screen-scraped text.
Claude Code. Command claude; model sonnet, opus or an id.OpenCode. Model as provider/model.Custom CLI. /usr/bin/env with {prompt} as the argument template.
Every step arrives as a row — read a file, searched, edited, ran a command, fetched a page — and the run folds into a one-line summary with failures counted. Expand a command to see its exact input and output. When the runtime asks permission, the question is a card: Allow or Deny, naming the path or command.
Release engineer · sonnet · $0.04Claude Code
Audit the release path and verify the test gates.
I’ll inspect the build contract, run the focused checks, and report only actionable release risks.
Zoom out and every tile reduces to its state — WORKING, NEEDS APPROVAL, FAILED, WAITING FOR YOU. ⌘J walks what needs you in order: host failures, then waiting input, then failed commands. Each card names the host, folder, actor, the exact request, its risk and a short tail of output.
Board · zoomed out⌘J
Release engineer
claude code
WORKINGNEEDS APPROVAL · Write✓ gates green
docs
codex
WORKING · edit
lint
opencode
✓ 0 warnings
zsh
prod-nl
WAITING FOR YOU
migrate
custom cli
FAILED · exit 2
zsh
dev-linux
RUNNING
QA
claude code
WORKINGNEEDS APPROVAL · Bash✓ 12 checks
ci
microVM · node:22
RUNNING
⌘J: host failure → waiting input → failed commandevery tile, reduced to its state
Zoomed out, a tile becomes its status. The ones that need you are amber, and ⌘J takes you to each in order.
The attention card. cwd, actor, command, risk and the exact request — Approve, Deny or open the agent.A hundred tiles at a glance. Claude Code and OpenCode terminals, each reduced to its state. A recorded fixture.
A mission gives an agent a goal and a contract — time, cost and command budgets; how many edits, files and attempts it may use; which commands are allowed — pinned above the conversation with a Stop button. Finished commands join a Mission Evidence ledger: actor, session, environment, risk, exit code, duration class and a command digest, never the prompts, replies, commands or transcripts themselves.
The contract, pinned. “Fix the failing terminal outline test” — edits 2/8 · files 2/5 · attempts 2/3 · allowed: cargo test.Content-free evidence. Counts, costs and request sizes; exported from ⌘K.
Wires connect agents to each other, to chats and to documents. A report can fan out to a researcher and a skeptic; one agent’s answer can become another’s prompt. Whatever crosses a wire is typed, ordered and, when it matters, reviewed.
Rules, facts, skills, tools, profiles and memory live in plain files. Portal resolves them per agent, mounts them into each runtime the way it natively reads configuration — and learns from what happens.
Every agent resolves the same chain: Everyone, then the repo it works in, then its Board, Channel and Station, and finally the agent itself. Each narrower scope can grant or forbid. The Harness screen shows what reached an agent, the scope each item came through, and about how many tokens ride every turn.
Every agent resolves the same chain. A repo joins right after Everyone; each narrower scope can grant or forbid. The Harness screen shows exactly what reached the agent and the scope it came through.
What it gets. One agent’s rules, skills and tools, each with the scope that granted it.
Your library is a folder of rules, skills, custom tools and profiles — and Portal also reads the folders your other tools already use, editing them in place. Change a file in any editor and it reloads within about 1.5 s. Nothing is deleted; removed files move to .trash/.
your library
agent/
rules/<id>.md # always on
skills/<id>/SKILL.md # + scripts/
tools/<id>.md # custom tools
profiles/<id>.md # roles
.trash/ # never deleted
a repo
<repo>/
CLAUDE.md, AGENTS.md # their own CLIs
rules/*.md # this repo's agents
tools/ # id@my-repo
policy.yaml # restricts
CLAUDE.md, AGENTS.md, .claude/, .agents/skills, .codex/skills and the repo’s harness folder
Claude Code · Codex
Read, not copied. ~/.claude/CLAUDE.md, exactly as Claude Code reads it.A lint for your library. A skill with no description gets a ring and a one-line fix.
A fact is one line of text. Trailing brackets give it structure — where it applies, what it forbids and how to check it — so every place that edits facts can write them: the kit editor, settings.yaml, a repo’s policy.yaml. Agents read each fact together with its latest result.
facts
We use pnpm, never npm[guard:`npm install*` `npm ci*`]stopped 3 · ran anyway 1Deploys freeze on Fridays[when:branch=release/*, repo=github.com/acme/app]sent only on release/* in acme/appThe suite runs under a minute[probe:`make test`] [expect:under 60s]checked 2h agoThe API listens on 8443[probe:`grep -q 8443 config/app.yml`] [rests:config/app.yml]its check failed — verify before relying on it
The same four lines, with what Portal records beside each.
guard:Command patterns — globs, /regex/ or a prefix — refused for agents, checked per simple command, past sudo and env.
when:os, host, repo, branch, path, runtime. Sent only where every condition holds.
probe: expect:A check run daily, when a file it rests on changes, or on Check facts now: exit N, contains, under Ns.
rests:Files the fact depends on. When one changes, the check runs again and stale answers say so.
A repo’s own probes wait until you allow them once or trust the repo. Probes run with your rights — keep them to read-only checks.
One rule, enforced the strongest way each runtime allows. Claude Code’s PreToolUse hook denies the call and cites the rule; OpenCode’s plugin refuses it; an API chat’s terminal proposal fails. Codex and other CLIs have no hook before a command runs, so Portal notices it in the terminal and counts it as run anyway — the harness report lists each rule’s stops and misses.
A guard is enforced where the runtime allows it. Codex and other CLIs have no hook before a command runs, so Portal notices the command afterwards and records it — the harness report lists each rule's stops and misses.
Nothing to install per agent. Each runtime receives the harness through the switches it already understands, and every shell carries a live link to its tile’s bundle — change a scope, and the next claude or codex typed in that same shell gets it, with no restart.
Nothing to install per agent. Each runtime receives the harness through the switches it already understands; your own config.toml, CLAUDE.md and AGENTS.md stay yours.
Remote hosts get the same bundle over SSH, synced once per content hash. Vagabond machines install it with the command that starts their shell, so later edits reach the next launch.
Every agent action carries the effects its plan declares — editing the canvas, creating tiles, typing into terminals. In Review, an action runs on its own only when you’ve allowed that agent every effect it has. After 20 accepted in a row, Portal offers to let the agent do that kind of thing without asking; one discard takes it back, and Keep asking waits for 40 before offering again.
Release engineer · creating tiles20 accepted in a row
Each accepted action of this kind counts.
Let Release engineer create tiles without asking? AllowKeep asking
✕ One discard — back to asking.
Autonomy is granted per agent and per kind of effect, and taken back the moment you disagree.
One action, reviewed. Approve checked or Discard — with a reason that goes back to the agent.
Every Portal terminal — local, over SSH or on a machine — gives its agents four memory tools. Memory follows the repo, named by its origin remote, so a laptop and a remote box share it. remember never writes: a proposal waits under Proposed until a person accepts it, and unreviewed ones expire after 14 days.
recall, why and fix_lookup answer from what Portal remembers for this repo and machine. remember never writes memory — it proposes, and every answer is marked as evidence, not instructions.
Proposed, not remembered. Agents’ observations wait for review, with where they came from.Remember a decision. From any terminal block’s menu.
Every finished command joins a log. Once a week — or when you ask — the evidence becomes edits you review: a guard for a “never” rule that ran anyway, a fact for an error that keeps coming back, stop sending a skill nobody opened in 30 days, remove a fact whose check fails. Profiles can be tried against each other by bad-turn rate and cost, and nothing is called before 20 turns an arm.
The retro, as edits. “Stop sending this skill to every agent: unused for 30 days.” Apply or dismiss.What this agent gets. Rules, skills, tools and the exact text, with Look for harness edits.
An agent’s spawner sets its limits: autonomy, approval floor and budgets can only be as loose as the parent’s.
Repos restrict until trusted
A repo’s policy.yaml can forbid but not grant — no tools, model, provider or capabilities — until you trust the repo.
Budgets are shared
A cost ceiling belongs to the scope that sets it: the app’s budget counts every chat, a board’s the chats on that board.
05 · SKILLS
Skills: written once, used by every agent.
A skill is a folder with a SKILL.md — the format Claude Code, Codex and every Agent Skills tool already read. Portal finds the ones you have, proposes new ones from what you run, measures whether they help, and delivers them the way each agent expects.
Standard frontmatter — name and description — so the same folder works in Claude Code, Codex and any tool that reads Agent Skills. Portal-only keys live under metadata:, and keys it doesn’t model — allowed-tools, license, your own — are never rewritten.
skills/release-check/SKILL.md
---name: release-check
description: Before tagging a release, run the
suite, confirm the changelog and build.
---
1. `pnpm test` — stop on the first failure.
2. Check that CHANGELOG.md names this version.
3. `pnpm build`, then attach the bundle report.
layout
skills/release-check/
SKILL.md
scripts/ # helpers it may run
references/ # docs it may read
assets/ # files it may copy# a default-off flag under metadata:# → available only where a scope turns it on
Portal reads skills from the folders your tools already use and merges them into one library. For the same id the narrower source wins — the repo over Portal, Portal over the other tools — and a repo’s skills get qualified ids like deploy@my-repo, so two repos never collide.
Only these folders are read — never a recursive walk — with a 256 KB per-file and 2,000-item per-source cap. Editing a read-only item saves your copy, which then wins; removing one moves it to .trash/.
Open the Skills panel beside any chat and the whole library is there: filter by name and see whose each skill is. The skills chip in the composer keeps the count in view. Each skill can be granted or forbidden per scope — everyone, a repo, a board, one agent — and the Harness screen shows which scope an agent got it through.
The Skills panel. 39 skills, one filter, and whose each one is.
Always in view. The composer’s skills chip.The skill, as a file. ~/.claude/skills/pdf/SKILL.md — on for everyone, never opened yet.
A sequence you run three times or more that mostly ends well is proposed as a skill, with its steps as a trace and the repo files it goes with. A command you run with varying words is proposed as a tool with parameters. The eye opens the exact file it would write; Add to library writes it, and dismissing silences it.
When a failure turns into a pass, Portal captures what changed as a fix — When, Diagnose, Fix, Verify. Accept skill grants it to every agent on the board, Memory only keeps it as a known fix, Discard drops it. Accepting is the only way a fix becomes a skill.
Fix captured. vim src/app.cpp, make build — Accept skill, Memory only, or Discard.And recalled. The next time the error appears, the fix is one click away.
For each skill an agent had, Portal compares failures with the skill opened and without it — API chats’ turns, CLI agents’ command results reported by their hooks. With eight or more on each side, a two-proportion test says whether it goes with fewer failures or more. In an API chat’s always-on context, up to two short skills that fit the repo ride whole; one that goes with more failures is sent only by name, last.
release-check · failure rate
With the skill10%
Without it50%
fewer failures with it (10% vs 50%, 40 runs)
A skill earns its place by outcome, not by being installed.
Declarative *.skill.json files compose registered commands into one ⌘K entry. They can’t call a binary, a URL or a provider directly, and on load Portal derives their effects from the commands they use — so a manifest can’t hide risk by under-declaring. Limits are deliberately small: 16 arguments, 16 steps, no recursion.
Tools with a clear line between looking and doing.
Agents get the canvas as a tool server, your library tools, memory and machines. Reads run at once; changes arrive as proposals; the strongest authority is always visibly armed.
Coding agents reach the canvas through an MCP server of about 55 verbs, each declared read-only, staged or mutating. Agent tiles running Claude Code get the canvas tools pre-allowed; anything that would change your canvas arrives as an Accept / Discard card.
Terminal proposals stage the text and never press Enter.
Verbs
stage_terminal · open_terminal with a command
A proposal in the chat. Draw a diagram · Dependency flow — 2 nodes, 1 connection. ✕ or ✓.Editing a live figure. “Make it more complex” — the Thomas attractor joins Lorenz and Clifford.
In a chat, a fenced tool block becomes a typed proposal. Parsing alone never executes model output: auto-approved actions run through the same planner and executor as ⌘K, everything else waits as a card, and unknown tools or malformed calls stay plain text.
A kind: shell tool binds its arguments into environment variables — never parsed as shell — and runs by its declared effect. API chats call library tools with run_tool; CLI agents see them as tools of the memory server.
A kind: shell tool binds its arguments into environment variables — never parsed as shell — and runs according to its effect. A staged change shows its diffstat before you apply it, here or on the host it came from.
tools/check.md
---name: check
description: Run the test suite
kind: shell
effect:read_onlyrun: make check
---
Runs the whole suite.
The tool, as a file. Exactly what any editor shows.
An AI chat can operate the whole workspace through bounded Lua — only when you arm it, per tile. Pause and Stop are always on its bar. One turn is bounded to 12 continuations and a response to four calls, and a restart never silently restores automatic authority: saved modes come back as Review.
–Read the whole workspace
–Capture the visible viewport
–Change layout, boards, artifacts
–Type into terminals
–Send to providers and Telegram
–Start agents, drive browsers
Pick a mode to see what it allows. ✓ runs on its own · ? asks each time · – never.
Something it built. “Sculpt the invisible” — a live wave surface with its own controls.Steps, folded. “Ran a canvas script” — each step with its run time.
Each step is an explicit Lua block that opens with an intent comment — its title on the card. Changes go through transactions checked against the world revision, so a stale plan is rejected instead of hitting the wrong tile, and every applied plan returns a receipt you can undo. The Lua runs in a fresh, bounded VM with no filesystem, process or network access.
step.lua
-- intent: Add a release checklist beside the build-- read the world, then apply one transaction:-- artifact.create "Release checklist" at 120, 160-- tile.resize that checklist to 520 × 360-- → a receipt you can undo; a stale revision is refused
⌘K holds built-in actions, native tools, installed skills and recent commands, and every entry follows one pipeline. Provider requests created by a command stay staged in the chat’s exact-payload review — approving a plan doesn’t send them.
⌘K is the control plane. Planning never changes anything; execution is the single mutation boundary, and reversible plans record their own undo.
The vagabond MCP server, served from Portal’s own binary, gives agents machines: exec with real exit codes, logs that wait instead of polling, git-aware upload and download, clone, desktops driven by element name, and fleets. Full per-machine results never enter the agent’s context — they’re written to the workspace, and the answer names the path.
create_machine
exec
logs
upload · download
clone
create_desktop
screenshot
desktop_input
fleet_create
fleet_run
fleet_map
race
07 · CANVAS
Build it like a Blueprint.
The canvas is Portal’s level. Chats, terminals, agents, figures and files are tiles on one native, GPU-rendered board, wired together like Blueprints. Ask it for a picture of an idea and you get a live instrument — equation, plot and controls — then branch it, check it and keep what matters.
Every tile has typed ports. Drag from one to another and you have a wire: a document fans out to a researcher and a skeptic, one agent’s answer becomes the next one’s prompt, a terminal hands each finished command to an agent. Values arrive in order, filters and review gates sit on the wire, and text routed into a terminal is pasted, never run.
One document, two agents. report.md wired to a Researcher and a Skeptic.Claude into OpenCode. One agent’s output, wired into the next one’s input.
Press ⌘/ and say what you want to see — “black hole lensing”, “how vaccines build herd immunity”, “E = mc²”. The answer arrives inline as a native studio: the governing equation, figures and live controls, never a flat image. Bookmark it and it joins your gallery, searchable from ⌘K.
A live explainer. Drag the mass; energy, TNT equivalent and homes powered update as you move.Open it large. The same figure, maximized — still live, still exact.
Plots are native surfaces, not pictures — in 2D and 3D. Pan, zoom, read a crosshair, toggle a series from the legend, orbit a 3D chart or switch it to a table — and the exact data stays with the plot. Lines, scatter, bars and shaded bands; neon, scientific or minimal styles; linear, log, symlog and time axes, with calendar labels and SI units (1.2k · 3.4M).
Plots are native surfaces, not images: pan, zoom, read a crosshair, toggle legend entries — and the exact data stays with the plot.
Math is typeset by the app itself — in chats, figures and studios — so it stays vector-crisp at every zoom. Inline and display math, colour, cancellations, arrows, stacked limits and your own macros; chemistry with \ce{}, units with siunitx and the physics package. Wide equations break like typeset ones, and a half-streamed formula shows a placeholder instead of an error.
LaTeX
% Schrödinger, with the physics package
i\hbar\,\pdv{\psi}{t} = -\frac{\hbar^2}{2m}\nabla^2\psi + V\psi
% chemistry and units
\ce{2H2 + O2 -> 2H2O} \qquad g = \qty{9.81}{m/s^2}
% bra-ket, colour, cancellation, arrows
\bra{\phi}\hat{H}\ket{\psi} \qquad
\color{teal}{E} = mc^{2} \xrightarrow{\,1\,\mathrm{g}\,} \qty{21.5}{kt}
Inline and display. 2 + 2 = 4, E = mc² and ∫₀¹ x² dx = 1/3 — no blur, no raster.
Every answer has a ⋯ menu. Branch opens a sibling conversation beside the original — wired to it, with the context frozen at that turn — so you can take a different direction without losing the first. Race runs a second answer to the same turn side by side. Nothing forks unless you ask: an ordinary Send never doubles provider traffic.
Branches are spatial: the fork opens beside the original, wired to it, with the context frozen at the turn you picked. An ordinary Send never doubles provider traffic — branching is always explicit.
One answer, two paths. The branch opens on the right. The ⋯ menu: Copy, Evidence, Retry, Branch, New chat about this, Keep as a note, Select, Remember, Save as fact.
The same menu keeps things. Remember records a decision; Save as fact turns a passage into a fact; Add as skill (K) bottles a procedure; Keep as a note drops it on the canvas. Each opens a capture card that asks the scope — this chat or the whole board — and grants nothing until you save. Saved facts join the harness, so every agent in that scope reads them.
Save as fact⌘↵ to save
c² is the exchange rate: one gram, fully converted, is about 21.5 kt of TNT.
This chatWhole board
Only this conversation will read it.Every agent on this board will read it.
CancelSave
The capture card says out loud who will read what you save — before you save it.
Evidence opens beside any passage: its sources, the checks that back it, the assumptions written down and the open questions — with plain flags like No source and Not checked. Challenge passage sends it back as a new turn that must weigh that evidence and say whether the conclusion should change.
Evidence, per passage. Sources · Checks · Assumptions · Open questions — and Challenge passage.
Boards of tiles, conversations with any model, and answers you can use: tables that recompute as you drag a value, derivations checked step by step, figures you can steer.
Tables that recompute. Drag a value; the answer recalculates on your machine.
Every step checked. A wrong step is caught before you see it.
Drop anything on the canvas or into a chat. Portal says exactly what a model will receive, turns pictures into vision tasks, lets agents see the screen they are changing — and leaves your originals where they are.
Drag files anywhere on the canvas and a ring of verbs opens at the drop point: Preview makes linked file tiles, Compact keeps small file objects, AI chat starts a conversation with the files in its draft, and over an existing chat Attach takes the top petal. Files are linked originals, not copies, and one drop takes up to 128 paths.
Enter runs the top petal, arrows pick a direction, Tab walks the ring. Dropping never sends anything on its own — it only offers.The offer, in place. A PDF dropped beside a research chat.
An image or a visual diff becomes a vision task. Text-only models are marked in the model registry: a PDF sends its text with an explicit note that the image was left out, and an image asks you to pick a vision-capable model while keeping your draft.
No silent failures and no automatic OCR: Portal says what the model can and cannot see before anything is sent.
Photos, asked about. Four images and a document in one message.Images beside the scrollback. Images the session produces ride in a rail beside its output.
In Canvas Operator, capture_viewport reads the real rendered frame, encodes a bounded PNG in memory and attaches it to that one call — captures are never saved. On a machine’s desktop, Point lets you circle a region and send it to the agent with a sentence.
Point at the problem. “The Pay button overlaps the total here — fix the layout.”A desktop, live. 640×400, driven by Claude Code during a refactor.
Hold ⌥H and hum — the pitch becomes the seed of a song. Answers can be live figures and simulations you steer rather than pictures, and video, drawing studios and music share the same canvas. Full-screen GPU scenes are on the way.
Hum a hook. Your melody becomes the seed of a song.09 · MODELS
Any model. Nothing to relearn.
Chats talk to model APIs; agents bring their own. All of them see the same rules, skills, tools and memory — so switching models never means starting over.
DeepSeek, Xiaomi MiMo and Zhipu GLM are built in. Any OpenAI-compatible endpoint works with your key — OpenAI itself, or a local server such as Ollama or LM Studio on loopback. Claude joins through Claude Code; Codex and OpenCode bring their own models. Where a single-vendor app picks the model family for you, here each chat picks its own — and can change its mind mid-thread.
API models answer in chats; agent CLIs run in terminals. Both get the same rules, skills, tools and memory.
The model picker. Built-in providers with context size and price for each model.
Pick a model per chat and switch mid-thread without losing the history, let a second model cross-examine the first, or put several on the same question.
Switch mid-thread. The conversation stays where it was.
One model checks another. A second opinion before you rely on it.
Challenge a passage. See what backs a claim; send it for review.
Several on one question. Answers side by side, scored when reality resolves.
10 · SETTINGS
Configure everything.
Looks, canvas, terminals, models, agents, memory — every part of Portal is a setting you can change in the app or in one plain-text file. The UI is fully configurable; the text is the source of truth.
⌘, opens Settings as a side sheet with a lens for each part of the app — Appearance, Canvas, Terminal, Media, Music, AI, Models, Sources, Chat, Hosts, Vagabond and Memory. Changes apply live and save themselves; Reset returns to the defaults.
Change the whole look, live. Neon to Claude, Synthwave to Observatory — colour scheme and world switch while you keep working.
Appearance. Colour schemes, colours, layout and motion.Canvas. Worlds, grid and guides, docking, Board Time Machine, wire flow.Effects. GPU visuals, HDR glow, tile edges, frosted glass, depth, zoom.
Everything the Settings screen shows lives in one plain-text file, settings.yaml — one section per area, one key per line. Edit it in any editor and Portal reloads it; save from the app and your comments, order and unrelated keys stay exactly as you wrote them. Any key can also be set from the environment — for one machine, a CI run or a demo.
Everything the Settings screen shows is plain text in one file, and every key can be overridden from the environment — for a machine, a CI run or a demo.
Live or next launch. Most changes apply at once; a few — window size, fonts, worker pools — on the next launch. The reference says which.
Safe by construction. Values outside safe bounds are clamped; malformed ones are ignored, never guessed.
No lost edits. Two changes to the same field pause the save and show both — Use file or Keep edits.
No secrets in the file. Provider keys stay in the environment or the OS keychain.
The settings reference documents every key — its range, its default and whether it applies live. Canvas and AI carry the most; the long tail covers windows, motion, capture, notifications and more.
Documented keys per section
411 keys · 26 sections
canvas74
ai71
terminal47
music38
vagabond25
harness24
chat19
missions16
behavior14
tiles13
ui12
suno10
14 more48
Every one of these is a line in settings.yaml and an environment variable.
Each chat carries its own controls: a model picker grouped by provider with context size and price, quick settings such as temperature and history, a Session panel with progress, outputs, context and export (PDF, JSON, compact), and a Skills drawer to grant any skill in the library. Terminal tiles have theirs: look, font, typography, colours, cursor, rendering, harness and command template.
Per-chat controls. Model picker, quick settings, Session panel and the Skills drawer — on a Synthwave canvas.
Models by provider. DeepSeek, Xiaomi MiMo, Zhipu GLM and OpenAI, with context and price.Skills and Session. Grant a skill; watch progress and outputs; export.Terminal settings. Look, font size, typography, colours, cursor, rendering.
11 · DIAGNOSTICS
See everything it sends.
The Inspector is Portal’s diagnostic page: every model request, shell command and tool call, captured in full with timing, size and cost — and a breakpoint to stop a prompt before it leaves.
⌘I opens Portal’s diagnostic page. Its Traffic view is a capture of everything the app says to the outside world and everything that comes back: every model request with its streamed reply, every shell command with its transcript, every tool call — in full, in order, with timing, size and cost. The tap sits at the transport, not the UI, so nothing is missed; API keys are never part of a frame.
Inspector · Traffic⌘I
AIdeepseek-chat · Compare my options6.9 s · 143 chunks
shellmake build5.8 s
toolcanvas.new_note · Research0.1 s
AIglm-5.3-flash · emc2streaming · 88
shellpytest -q tests/apiexit 1
toolcreate_plot · Signups since beta0.2 s
AImimo-v2.5 · ResearchHTTP 429
7 frames2 failed↑ 18.4 KB ↓ 212 KB6,112 tokens$0.004p50 1.2 s · p95 6.9 s
Violet for models, gold for the shell, teal for tools. Failed frames turn red; the strip sums whatever the filter shows.
The Traffic view. Requests and replies stream in as they happen, beside the conversation that made them.
Nothing off the record. Every request, streaming live.
One log for everything. App, terminals, AI and renderer in one stream.
A row opens its dissector. For a model request: session, purpose, endpoint, status, time to first token, total time, tokens in, out and reasoning, tokens per second, cost and size — then the request as its messages and the reply as it streamed. For a command: folder, who ran it, exit code, time and the whole transcript. For a tool call: the arguments and the receipt, or the refusal.
Hold the next request from a chat’s ⋯ menu, or with a rule. It stops before it leaves, and the Held tab shows it as layers — identity, each skill or harness block, the live wired context, every message — each with a switch and its token weight, beside the model, temperature and a live cost estimate. If nobody answers, the original goes after a timeout, so an unattended agent never freezes.
Held · deepseek-chat · Researchwaiting for you
Identity180
Skill · release-check1,240
Harness · facts410
Wired context · terminal3,900
Messages · 122,310
≈ 8,040 tokens · $0.0019≈ 4,140 tokens · $0.0010
Send edit ⌘↵Send originalDropSave as rule
A request stopped before it left: switch a layer off, watch the estimate fall, send the edit — or save it as a rule.
Rule action
Does
hold
parks the request at the breakpoint
drop
never sends it
replace_model
swaps the model — route a session to a local model while iterating
max_tokens
caps the reply
strip_layer
lifts out a layer: pasted receipts, live context, memory, history, or a skill by name
Type to filter, or use tokens: the wire, the session, the model, the tool, the purpose, the outcome, an exit code, a token or cost threshold, a latency. Tags narrow further. Follow session shows one conversation’s requests in order, the way you follow a TCP stream; Copy as curl reproduces a request with the key read from your environment.
Every interactive run writes a capture file beside its log — one frame per line, complete bodies included — so you can open it with jq, attach it to a bug report or diff two runs. The next launch restores the previous run’s last 400 frames, so the Inspector opens onto history.
capture-<run>.jsonl
{"v":1,"id":12,"kind":"ai","ttfb":0.412,"took":6.91,
"session":"Compare my options","purpose":"turn",
"label":"deepseek-chat","endpoint":"https://api.deepseek.com/chat/completions",
"request":"{\"model\":\"deepseek-chat\",\"messages\":[…]}","response":"…",
"status":"HTTP 200","failed":false,"out":8123,"in":2210,"chunks":143,
"tin":1890,"tout":412,"treason":-1,"cost":0.00081,"exit":-1}
12 · REFERENCE
Keys, and honest limits.
The shortcuts worth learning first, and the edges we’d rather you hear from us.