IngressLabs navigation lockup Compact IngressLabs lockup with radial ingress mark and wordmark. IngressLabs IngressLabs

Portal

The Unreal Engine
for AI agents.

Every agent, every model, one harness. Claude Code, Codex, OpenCode and your own CLI are tiles on one canvas — wired together like Blueprints.

The idea

An engine for agents.

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.

exit ≠ 0high risk → youzsh · ~/appTERMINALCommandResultscreenshot.pngIMAGEImagereport.mdDOCUMENTArtifactClaude CodeAGENTharnessCommandResultTextImageTextOpenCodeAGENTharnessTextResearcherAI CHATharnessArtifactTextSkepticAI CHATharnessArtifactTextCommandResultTextArtifactImageharnessrules · skills · tools · memory, mounted into each
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.
  • LevelCanvas

    One native, GPU-rendered board. Zoom from every session at a glance to a single keystroke.

  • ActorsTiles

    Terminals, coding agents, chats, documents, plots and machines — each one a tile.

  • BlueprintsWires

    Typed pins, values in order, filters and review gates on the wire itself.

  • ComponentsSkills & tools

    SKILL.md folders and native tools that every agent picks up.

  • Game ModeHarness

    The rules every agent plays by, across six scopes that grant or forbid.

  • Rendering backendsModels

    Claude through Claude Code, GPT through Codex, DeepSeek, GLM, MiMo or a local model — swap the backend, keep the graph.

  • Config .ini filessettings.yaml

    Every setting is plain text; the Settings screen edits the same file.

  • Blueprint debuggerInspector

    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.

  • 3D bars you can orbit
  • table ⇄ chart
  • live sliders
  • log · symlog · time axes
  • agents plot with the same schema
Plots in the guide →
Skills management

Every skill.
One library.

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.

The composer with its skills chip showing 40.
  • 11 skill folders read in place
  • narrowest source wins
  • grant or forbid per scope
  • learned from what you run
  • measured by outcome
Skills in the guide →

Under the hood.

One harness is mounted into every agent you run. Agents work where you work; anything that changes your world comes back to you first.

Harnessone set for every agentRules & factsguards · conditions · checksSkillsSKILL.md foldersToolscanvas · library · memoryMemoryrecall · fixes · proposalsClaude Codepersistent sessionCodexcodex exec --jsonOpenCodeopencode runYour CLIargv templateAPI chatsDeepSeek · GLM · MiMo · …Durable terminalsoutlive the windowSSH hostssame harness, syncedMachinescontainers · microVMs · desktopsCanvaschats, figures, boardsYouAccept · Discard · Allowmountedwork inproposesreview edits it learns
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.

One app per vendor — or one workspace#

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.

Warpits own agentterminalClaude appClaude modelsClaude CodeChatGPT appOpenAI modelsCodexMuse · Allumeboards for thinkingone vendor per app · context re-explained in eachPortal — one workspaceModelsDeepSeekGLMMiMoOpenAI-compatiblelocalAgentsClaude CodeCodexOpenCodeyour CLISurfacesdurable terminalscanvaschatsmachinesOne harnessrules · facts · skills · tools · memory — mounted into every agent
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.

Side by side#

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.

PortalWarpClaude appChatGPT appMuse · Allume
Built aroundagents, models, terminals and a canvasthe terminal and Warp’s agentchat, Cowork and Claude Codechat, Work and Codexboards for thinking
Modelsany: DeepSeek, GLM and MiMo built in; any OpenAI-compatible endpoint; local models; Claude through Claude CodeOpenAI, Anthropic, Google, xAI and hosted open models, for Warp’s agentClaudeOpenAI’sconnects to AI tools through MCP
Coding agentsClaude Code, Codex, OpenCode and your own CLI, side by sideWarp’s agent, locally and in the cloudClaude CodeCodex—
Terminaldurable terminals; every command a typed recordblocks with command, output, exit code and durationthrough Claude Codethrough Codex—
Canvasinfinite GPU canvas: chats, terminals, agents, figures, files—artifacts beside the chatcanvas for writing and codenested boards: ink, text, images, PDFs, links
Shared across agentsone harness — rules, facts, skills, tools, memory — mounted into every runtimeWarp Drive: shared workflows and MCP serversprojects, skills and connectors, for Claudeprojects, memory and plugins, for ChatGPT—

From each product’s public pages, September 2026. Muse is now called Allume.

Coming from another tool#

From Warp

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.

They outlive the window#

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.

Terminal tilein the Portal windowtermdowns every PTYshell · jobkeeps runningJournalappend-only JSONL, fsyncedUnix socketPTYevery eventattachedwindow closedreopened · replayingsame PID, same session
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 happensYou get
Close a tile or switch boardsThe process keeps running and is recovered on the next launch.
Quit or crash the appThe PTY stays live; the next launch reconnects to the same PID and journal cursor.
The socket dropsThe tile says it is disconnected; Reconnect catches up from the last applied event.
The daemon or machine stopsA kernel PTY can’t survive that. The journal becomes an honest archived transcript — it never claims the old process is alive.
  • socket 0600 · state 0700
  • peer credentials checked
  • fsynced JSONL journal
  • frames ≤ 16 MiB

Every command becomes a record#

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/api cwd~/work/app exit_code1 duration_s2.41 objectspytest case × 4 risklow
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.
CommandResult
CommandResult {
  event_id, session_id,
  command, cwd, environment, shell,
  exit_code, duration_s,
  transcript,
  objects[], risk,
  started_at_ms, finished_at_ms
}

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.

Know before you press Enter#

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.

When something breaks#

⌥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.

exit 1pytest -q tests/apiIncident Lens⌥E on the failed blockUsual fixwhat worked here beforeFix cardWhen · Diagnose · Fix · VerifyAgent missionhand it to an agent, with the evidence
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.

Commands flow to agents#

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.

Terminalevery CommandResultFilterexit · objects · host · riskReview high riskan approval card firstAgent · AI chatin event ordernon-matching events are consumedhigh and critical wait for youcursor moves on acceptancewire consentreview high risk · defaultreview every command
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.
ValueCan flow to
CommandResultTelegram chat · AI · Agent · Markdown artifact
TextTelegram chat · Terminal · AI · Agent
ArtifactAI · Agent
ImageAgent
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.

Remote hosts, same terminal#

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.

Machines on demand#

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.

Agent“give me eight of these”Vagabondclone 1–8 · race · fleets to 64Containersfastest to startFirecracker microVMshardware isolationQEMU VMsfull systemsDesktopsa screen the agent can point atfleets above 16 wait for your Approvethe host enforces a machine-hour budget
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.

Claude Code, Codex, OpenCode — or yours#

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.

Driven as
claude · stream-json · --resume
Harness
--append-system-prompt · --plugin-dir · .mcp.json + hooks
Tools
canvas tools pre-allowed on agent tiles

Codex guards after

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
Your prompttyped in the tileAdapterper runtimeCLI processits own auth, its own modelRowsmessages · tools · diffsJSON events, not scraped textstarted by the first prompt
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.

Tool calls you can read#

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.
▤Read CMakeLists.txt
⌕Searched for “WARNINGS_AS_ERRORS”
✎Edited release_gate.cpp+2 −1
›cmake --build build && ctest --test-dir build20/20 passed
×Ran a commandfailed
✎Wrote src/release_gate.cppallowed
Allow Write?
src/release_gate.cpp
✕ Deny✓ Allow
Rows, not scrollback. A permission request pauses the run as a card; your answer goes straight back to the runtime.
The run, folded. “Ran 3 commands, 1 search, read 1 file, edited 1 file, fetched 1 page · 1 failed.”
Permission as a card. Allow Write? src/release_gate.cpp — Deny or Allow.

What needs you, first#

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.

Missions with limits and evidence#

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.

Agents that work together#

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.

Tiles and wires on the canvas →
04 · HARNESS

One harness for every agent.

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.

Scopes: who gets what#

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.

Everyoneyour whole libraryRepothe repo’s policy — restricts until trustedBoardgranted · forbiddenChannelgranted · forbiddenStationgranted · forbiddenAgentthis one agentWhat api-tests getsruleWe use pnpm, never npmfactThe suite runs under a minuteskillrelease-checktooltail-logsskillsql — off by defaultskilldeploy — forbidden at Station● gets it ○ doesn't — blocked by a scope
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.

Plain files, where you keep them#

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
SourceFolderRead natively by
Portalyour library folder—
Claude Code~/.claude — CLAUDE.md, skills/, commands/, agents/Claude Code
Codex~/.codex — AGENTS.md, skills/Codex
Agent Skills~/.agents/skillsCodex
Other tools~/.cursor · ~/.github · ~/.copilot · ~/.gemini · ~/.opencode · ~/.factory · ~/.warptheir own tool
RepoCLAUDE.md, AGENTS.md, .claude/, .agents/skills, .codex/skills and the repo’s harness folderClaude 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.

Facts that carry directives#

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.

Where a guard holds#

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.

npm install lodashan agent's next commandClaude CodePreToolUse hookOpenCodepluginAPI chatterminal proposalCodex · other CLIsno pre-command hook✕ Denied — cites the rule✕ Denied — cites the rule✕ Denied — cites the rulerunsno hook before itnoticedcounted: ran anywayWe use pnpm, never npm [guard: `npm install*` `npm ci*`]
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.

How it reaches each runtime#

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.

Your libraryrules · skills · tools · profilesThe reporules · tools · policy.yamlHarness bundleper scope, per tileClaude Code--append-system-prompt · --plugin-dir · .mcp.json + hooksCodexCODEX_HOME/AGENTS.md · a memory server for the runOpenCodeOPENCODE_CONFIG · pluginSSH hostssame bundle, every skill as content · re-sync ~2 sVagabond machinesinstalled by the launch commandshell → live link → bundleEvery shell links to its tile's bundle.Change a scope and the next claude or codexin that same shell gets it — 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.

Autonomy is earned#

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.

One memory for every 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.

Claude Codeplugin .mcp.json + hooksCodexmemory server for the runOpenCodeOPENCODE_CONFIGAny shellrecall · remember commandsMemory toolsrecallwhyfix_lookuprememberMemoryper repo, named by its origin remoteProposeda person reviews · expires in 14 daysAccept
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.
  • signed per terminal
  • notes printed by scripts show as unverified
  • stale answers warn when a file changed

A harness that learns#

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.

Policy that can’t be quietly loosened#

Spawned agents inherit a ceiling

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.

A skill is a folder#

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

The skills you already have#

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.

Portal library~/.claude/skills~/.codex/skills~/.agents/skills~/.cursor/skills~/.github/skills~/.copilot/skills~/.gemini/skills~/.opencode/skills~/.factory/skills~/.warp/skillsrepo: .claude · .agents · harnessOne libraryedited in place · reloads ~1.5 ssame id: repo › Portal › other toolsClaude Codesession plugin, --plugin-dirCodexAGENTS.md index of each SKILL.mdAPI chats~8 KB index · skill_read opens oneSSH hostsevery skill as content
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/.

One library, managed from any chat#

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.

Learned from what you ran#

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.

git pullpnpm installpnpm buildpnpm testgit statusvim src/app.ts pnpm installpnpm buildpnpm testgit commit -am "fix" pnpm installpnpm buildpnpm testgit push
Proposed skill · build-and-test 3 runs · 3 ok
  1. pnpm install
  2. pnpm build
  3. pnpm test
◉ Read it firstAdd to libraryDismiss
The same three steps, three times, all green: that becomes a proposal — never a silent addition.

Later, when an agent runs a step of a learned skill without the steps before it, its hook’s note says which ones were skipped.

Fixes become skills#

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.

Measured by outcome#

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.

Delivered the way each agent reads them#

RuntimeHow skills arrive
Claude CodeA session plugin via --plugin-dir, loaded lazily beside your own skills. Ones it already reads from ~/.claude aren’t sent twice.
CodexCODEX_HOME/AGENTS.md: your own file first, then Portal’s rules and an index naming each SKILL.md.
API chatsAn index capped around 8 KB; skill_read opens a body when it’s needed.
SSH hostsThe same bundle with every skill as content — including your ~/.claude skills, since the host has none.
Vagabond machinesInstalled by the command that starts the shell.

Command skills for ⌘K#

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.

project-room.skill.json
{
  "id": "skill.project_room",
  "title": "Create project room",
  "effects": ["canvas_write", "tile_create", "terminal_spawn"],
  "arguments": [
    {"id": "name", "label": "Project name"},
    {"id": "brief", "label": "Brief"}
  ],
  "steps": [
    {"command": "canvas.new_board", "arguments": {"name": "${name}"}},
    {"command": "canvas.new_note", "arguments": {"title": "${name} brief", "content": "${brief}"}},
    {"command": "canvas.new_terminal"}
  ]
}
06 · TOOLS

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.

The canvas is a tool server#

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.

Look read-only

Runs at once and changes nothing.

Verbs
read_snapshot · logs · events · capture · screenshot · record · compare · terminal.read_output

Propose Accept / Discard

Becomes a card you accept or discard.

Verbs
create_diagram · create_plot · create_surface · create_figure · create_note · open_terminal

Type you run it

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.

Proposals, not side effects#

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.

proposal
{
  "tool": "create_diagram",
  "title": "Request flow",
  "nodes": [
    {"id": "client", "label": "Client", "x": 0.15, "y": 0.5},
    {"id": "api", "label": "API", "x": 0.5, "y": 0.5},
    {"id": "db", "label": "Database", "x": 0.85, "y": 0.5}
  ],
  "edges": [
    {"from": "client", "to": "api", "label": "HTTPS"},
    {"from": "api", "to": "db", "label": "query"}
  ]
}
Draw a diagram · Request flow
3 nodes · 2 connections
DiscardAccept
nothing changes until you accept
The model wrote a proposal; the canvas changed only after Accept, through the same planner as ⌘K.

Library tools, run by their effect#

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.

run_toolfrom any agentread_onlystagedmutatingruns at oncescratch worktreepatch +12 −3you applygit apply --3waywaits · Proposedyou acceptthen it runsresultback to the agent
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_only
run: make check
---
Runs the whole suite.
The tool, as a file. Exactly what any editor shows.

Canvas Operator, visibly armed#

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.

Transactions with receipts#

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

One pipeline for every command#

⌘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.

Searchfuzzy · focus · recencyArgumentstext · choices · tilesPlantyped, no side effectsReviewdeclared effectsExecutethe only mutationUndoinverse plan⌘K — built-in actions, tools, installed skills and recent commands in one place
⌘K is the control plane. Planning never changes anything; execution is the single mutation boundary, and reversible plans record their own undo.

Machines as tools#

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.

Tiles and wires, like Blueprints#

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.
What each type can reach →

Ask the canvas#

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 you can work with#

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).

Service latency604530150051015202530minutedeployp95 · 31 msp95p50
Plots are native surfaces, not images: pan, zoom, read a crosshair, toggle legend entries — and the exact data stays with the plot.
create_plot
{
  "tool": "create_plot",
  "title": "Service latency",
  "x_label": "Minute", "y_label": "Latency (ms)",
  "style": "neon",
  "series": [{
    "label": "p95", "type": "line",
    "x": [0, 1, 2, 3], "y": [22, 28, 24, 35],
    "color": "#36D7FF", "fill_alpha": 0.12
  }],
  "annotations": [{"x": 3, "y": 35, "label": "deploy"}]
}
  • ≤ 16 series
  • 4,096 points per series
  • 32 annotations
  • agents use the same schema

LaTeX, typeset natively#

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.

Branch any answer#

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.

AI chatYouExplain E = mc²Answera live explainer + sliderYouMake it about fissionAnswerfission converts ~0.1%Branch from hereBranch…same context up to this answerYouChallenge this passageAnswerchecks, assumptions, verdict⌥R — race a siblingtwo answers to the same turn, side by side
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.

Remember what matters#

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, then a challenge#

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.

Answers that run#

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.
Open the canvas gallery →
08 · MULTIMODAL

Files, images, screens and sound.

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.

Drop anything#

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.

Attach ↵AI chatCompactPreview3 filesover a chat, Attach takes the top petal
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.

What the model actually receives#

Portal prepares every file locally and says what a provider will get — before anything is sent.

FileWhat the model gets
PDFText from up to 40 pages, within 48 KiB. On macOS, also a first-page image.
DOCXThe main text with paragraph breaks — no macros, comments or embedded objects.
Text · Markdown · CSV · JSON · codeA bounded UTF-8 excerpt. The file can be gigabytes; only the prefix is read.
PNG · JPEG · WebP · HEIC · TIFFAn orientation-correct JPEG, at most 1600 px on the long side.
Archives, video, anything elseA linked reference with metadata — never a claim that its contents were read.
  • 8 files per message
  • 48 KiB shared text budget
  • ≤ 1 MiB per image
  • no automatic OCR

Pictures become vision tasks#

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.

screenshot.heicdropped into a chatvision model?per model registryJPEG, orientation-correct, ≤ 1600 pxsent with your messageAsks you to pick a vision modelthe draft stays intactyesno
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.

Agents that can see the screen#

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.

Sound, motion and live figures#

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.

Any model, the same harness#

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.

DeepSeekbuilt inZhipu GLMbuilt inXiaomi MiMobuilt inOpenAI-compatibleany endpoint, your keyOllama · LM Studiolocal, on loopbackClaude CodeClaude, through its CLICodexagent CLIOpenCodeagent CLIOne workspacesame harness · same canvaspick per chat · switch mid-threadput several on one question
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.

Several models, one question#

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.

Every knob, in one place#

⌘, 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.
  • Graphite · Claude · Codex · Neon
  • Worlds · Geometry · Classics
  • HDR glow
  • frosted tiles
  • depth: flat → dimensional
  • reduce motion

All of it is plain text#

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.

settings.yaml
window:
  width: 1440
  vsync: true
ui:
  accent: "#4C9EEB"
  reduce_motion: false
canvas:
  grid: true
  grid_size: 64          # comments survive every save
terminal:
  shell: "/bin/zsh"
  opacity: 0.98
ai:
  base_url: "https://api.xiaomimimo.com/v1"
  default_model: "mimo-v2.5"
Settings screen⌘, · every change autosaveswritesEnvironment variablesame key, upper casesettings.yamlplain text, comments keptBuilt-in defaultwhen nothing is setwins overwins overedit the file in any editor — the app reloads it; same-field conflicts ask: Use file · Keep edits
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.

411 settings, 26 sections#

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.

Per-chat and per-tile controls#

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.

The Inspector — Wireshark for AI#

⌘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.

Open any frame#

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.

  • first token
  • tok/s
  • tokens in · out · reasoning
  • cost
  • p50 · p95
  • follow session
  • copy as curl

Breakpoints on prompts#

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 actionDoes
holdparks the request at the breakpoint
dropnever sends it
replace_modelswaps the model — route a session to a local model while iterating
max_tokenscaps the reply
strip_layerlifts out a layer: pasted receipts, live context, memory, history, or a skill by name

A filter language#

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.

filters
kind:ai model:deepseek-chat tokens:>4000
session:"Compare my options" failed
kind:term exit:!0 slow:>2s
purpose:review cost:>0.01
#status:429 #edited

Every run, recorded#

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-&lt;run&gt;.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.

Keys worth learning first#

Commands, tools and skills⌘K
Next thing that needs you⌘J
Latest failure — Incident Lens⌥E
Lift a pipeline onto the board⌥L
Inspector — diagnostics⌘I
Ask the canvas⌘/
Settings⌘,
Split right · split down⌘D · ⇧⌘D
Hum to song⌥H (hold)
Filter the Harness list — and search this guide/

Honest limits#

  • Guards after the fact. Codex and other CLIs have no hook before a command runs: a guard is noticed afterwards and counted as “ran anyway”.
  • Risk is not a sandbox. Risk levels route approvals; isolation comes from where the command runs.
  • No automatic OCR. Image-only PDF pages beyond the first aren’t read, and encrypted PDFs must be unlocked first.
  • Machines can stop. A kernel PTY can’t outlive its machine — you get an honest archived transcript, not a resurrected process.
  • Reattached remotes keep their bundle. A remote session reopened from Recents keeps the harness it was born with; open it fresh to follow the tile.
  • Numbers are from the lab. Machine timings were measured on a single-node lab host, and the hundred-tile board is a recorded fixture.

Bring Portal to your team.

If your team runs more than one coding agent, works across machines, or wants rules its agents actually keep, tell us how you work.