Skip to content

The canvas becomes three columns, and a connection stops being invisible - #38

Merged
keirsalterego merged 3 commits into
identra-dev:mainfrom
keirsalterego:ux/three-column-shell-replaces-the-canvas
Aug 2, 2026
Merged

The canvas becomes three columns, and a connection stops being invisible#38
keirsalterego merged 3 commits into
identra-dev:mainfrom
keirsalterego:ux/three-column-shell-replaces-the-canvas

Conversation

@keirsalterego

Copy link
Copy Markdown
Contributor

The first real user feedback this project has had said the same thing twice: the workspace picker and the greeting are good, and the canvas inside a workspace is not. That reading holds up on the merits and not only on taste. A coding task maps to a branch, and branches are a list. Spatial arrangement earns its keep when the relationships between things matter more than their contents, and for four agents on four tasks it does not.

So the plane goes and three regions take its place: what you can open on the left, what is open in the centre, what the project knows on the right. Nodes become tabs in creation order; a tab opens into the focused pane of a split tree.


What this deletes is the point

AgentNode carried a terminal, a spawn, a status machine, a lock, an expand, two remembered sizes and a pair of connection ports — because it was a box on a plane that also happened to run an agent. A pane is a view over a process it does not own, so:

Was Is
AgentNode spawn logic spawn.ts
AgentNode status machine nodeState.ts
AgentNode / BrowserNode / FileNode / NoteNode one Pane
FocusView + REFIT_EVENT nothing — a single-leaf pane is already full size
tidy.ts, preview.ts, the board thumbnail nothing — all functions of positions
@xyflow/react removed from package.json

1,933 insertions, 2,296 deletions.

Three things less obvious than the layout

Edge is untouched, deliberately. canvas.rs:35 says it plainly: two nodes share context only when an edge joins them, enforced in three places in identra-mcp. Drawing a wire was the user's consent gesture for agent-to-agent access. Deleting the canvas without noticing that would have revoked every permission anyone had granted, as a side effect of a UI decision.

Positions are ignored, not erased. x, y and the viewport round-trip exactly as they came, so a workspace last saved by v0.1.2 is not rewritten by the first save from here. A workspace that was arranged says so once, on first open: the agents are still here as tabs, the arrangement is not, the connections carried over. Ten months of layout vanishing into something the user did not choose, with nothing saying it was deliberate, reads as a corrupted file rather than a new version.

The split tree is session-only and the keyboard came first. Ratios are not persisted and not draggable. The keys are the whole v0.2.0 set — number to switch tab, [/] between panes, \ to split, shift+\ to close a pane, e for the right column — captured on the window, because every pane here is a terminal that takes keys first. Retrofitting focus into a pane tree means touching every pane, so the tree was built around it.

layout.ts is pure with its own tests, including the two that would be silent bugs: closing the last pane leaves it alone, and closing a node blanks every pane showing it rather than leaving a dead id mounting a terminal onto a pty that is gone.


The second commit closes the gap the first one named

An edge is the bus authorization, and connect_nodes is a bus command any agent on the bus can issue, gated only by the lock on either end. So an agent can grant itself read access to another agent's context. The canvas made that survivable by accident: a line appeared in front of you, unasked. Take the canvas away and the grant still happens with nothing drawing it.

The Links column is the replacement, and it is why Edge was kept as data when everything else about the canvas went.

  • It lists every connection, whatever made it. A panel showing only yours would look like a complete list and would not be one — worse than the canvas, not better. The count sits on the button whether or not the column is open.
  • It never guesses who. Grantor is unknown | you | agent, and Unknown is the default — not a placeholder for You. An edge saved before this field existed has no record of who made it, and answering "you" would make the control confidently wrong about the one thing it exists to be right about. There is a Rust test on exactly that, because the failure is silent: every pre-existing grant on every user's disk would be presented as their own.
  • Revoking bites on the next read, since get_peer_context re-reads the slice per call — while granting only takes effect at next launch, because a CLI reads its MCP servers once. That asymmetry is the right way round and worth keeping: a permission should be slower to give than to take away.

identra-mcp needed no change. Same slice, same refusals. The field is additive with a serde default, exactly as locked and seat were.

The third commit

The README headline still read "A desktop canvas for running coding agents" — the most visible thing in v0.1.2 and the least defensible thing here, and as of the first two commits it described software nobody can download.


Not in this PR, deliberately: persisted split ratios, drag-to-reorder tabs, command palette, per-pane zoom. Each is an addition to a shell that exists, and none is cheaper to decide now.

cargo fmt, clippy --all-targets, the Rust tests, tsc, bun test and the frontend build are clean.

…tops being work

The first real user feedback this project has had said the same thing twice: the workspace
picker and the greeting are good, and the canvas inside a workspace is not. That reading holds
up on the merits and not only on taste. A coding task maps to a branch, and branches are a
list. Spatial arrangement earns its keep when the relationships between things matter more
than their contents, and for four agents on four tasks it does not.

So the plane goes and three regions take its place: what you can open on the left, what is
open in the centre, what the project knows on the right. Nodes become tabs in the order they
were made, and a tab opens into the focused pane of a split tree.

What that deletes is the point. `AgentNode` carried a terminal, a spawn, a status machine, a
lock, an expand, two remembered sizes and a pair of connection ports, because it was a box on
a plane that also happened to run an agent. A pane is a view over a process it does not own,
so the spawn moved to `spawn.ts`, the status machine to `nodeState.ts`, and the four node
components collapsed into one `Pane`. `FocusView` went with them: a pane holding the only leaf
is already full size, so the overlay that existed to fake that has nothing left to do — and
with it went the refit event, which existed only because an overlay could change a pty's
wrapping without changing any box. `tidy` and the board thumbnail went because both were
functions of positions. React Flow is no longer a dependency.

Three things in here are less obvious than the layout:

**`Edge` is untouched, deliberately.** `canvas.rs:35` says it plainly: two nodes share context
only when an edge joins them, and it is enforced in three places in `identra-mcp`. Drawing a
wire was the user's consent gesture for agent-to-agent access, so deleting the canvas without
noticing that would have revoked every permission anyone had granted, as a side effect of a UI
decision. Edges load, save and carry across untouched. What is missing is the drawing of them,
which is the next commit and is called out in the comment above `edgesRef`: `connect_nodes` is
a bus command any agent can issue, the canvas at least made that grant appear in front of you,
and nothing in this shell does yet.

**Positions are ignored, not erased.** `x`, `y` and the viewport are read and written back
exactly as they came, so a workspace last saved by v0.1.2 round-trips rather than being
rewritten by the first save from here. And a workspace that *was* arranged says so once, on
first open: the agents are still here as tabs, the arrangement is not, the connections carried
over. Ten months of layout vanishing into something the user did not choose, with nothing
saying it was deliberate, reads as a corrupted file rather than a new version.

**The split tree is session-only and the keyboard came first.** Ratios are not persisted and
not draggable; an even split is what a split means until someone asks for it to mean something
else. The keys are the whole v0.2.0 set — tab by number, `[`/`]` between panes, `\` to split,
`shift+\` to close a pane, `e` for the right column — captured on the window, because every
pane here is a terminal that takes keys first. Retrofitting focus into a pane tree means
touching every pane, so the tree was built around it.

`layout.ts` is pure and has its own tests, including the two that would be silent bugs: closing
the last pane leaves it alone, and closing a node blanks every pane showing it rather than
leaving a dead id mounting a terminal onto a pty that is gone.

tsc, bun test and cargo fmt are clean, and the frontend builds.
… and revocable

The shell landed with a gap it named in its own commit message. An edge is the authorization for
the context bus — `canvas.rs:35` says so, and `identra-mcp` enforces it in three places — and
`connect_nodes` is a bus command any agent on the bus can issue, gated only by the lock on either
end. So an agent can grant itself read access to another agent's context.

That was survivable on the canvas by accident rather than by design: a line appeared in front of
you, unasked, and you could see it. Take the canvas away and the grant still happens with nothing
drawing it. This is the control that replaces the drawing, and it is the reason `Edge` was kept as
data when everything else about the canvas went.

**It lists every connection, whatever made it.** A panel showing only the ones you made would look
like a complete list and would not be one, which is worse than the canvas was rather than better.
The count sits on the button whether or not the column is open, the same as the memory badge and
for the same reason: a number you only see once you go looking tells you nothing happened.

**It never guesses who.** `Grantor` has three values and `Unknown` is the default, which is not a
placeholder for `You`. An edge in a canvas written before this field existed genuinely has no
record of who made it, and answering "you" for it would make the control confidently wrong about
the one thing it exists to be right about. There is a test on exactly that, because the failure
mode is silent: every pre-existing grant on every user's disk would be presented as their own.

Revoking drops the edge, and the wording says what that does and does not do. `get_peer_context`
re-reads the edge slice per call, so a revoke bites on the next read — while granting one only
takes effect when the CLI next starts, because a CLI reads its MCP servers once. That asymmetry is
the right way round and worth keeping: a permission should be slower to give than to take away.

`identra-mcp` needed no change. Same slice, same refusals. The field is additive with a serde
default, so an older build reads a newer file and ignores what it does not know, exactly as
`locked` and `seat` did.

cargo fmt, clippy --all-targets, the Rust tests, tsc, bun test and the frontend build are clean.
The headline read "A desktop canvas for running coding agents." It described the most visible
thing in v0.1.2 and the least defensible thing here, and as of the last two commits it describes
software nobody can download.

What replaces it is the thing that is actually hard to copy: agents open already holding the
project's memory, because it arrives in the MCP handshake rather than through a tool they have to
choose to call. That claim was already in the positioning; the README was the last place still
contradicting it.

Everything downstream follows the same rename — nodes are tabs, wires are connections, the dock is
the sidebar, the board is the workspace — plus the one genuinely new sentence: connections are
listed with who made them and can be revoked, which is a thing a canvas of lines could not do.
Copilot AI review requested due to automatic review settings August 2, 2026 03:34
@keirsalterego
keirsalterego merged commit 76923dd into identra-dev:main Aug 2, 2026
3 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the React Flow-based infinite canvas UI with a three-column “shell” layout (sidebar for opening things, centre pane tree for what’s open, right column for project/work), while preserving the existing Edge-based authorization model by making connections auditable via a new Links/Connections view and an additive Grantor field on edges.

Changes:

  • Replace canvas node UI with tab + pane-tree shell (including keyboard navigation/splitting) and new process-spawn/state logic extracted into standalone modules.
  • Add Grantor (unknown|you|agent) to Edge (serde defaulted) and introduce a Connections/Links panel that lists and revokes all connections.
  • Remove canvas-only features and dependencies (React Flow, minimap/preview/tidy/focus overlay) and update README wording to match the new UI model.

Reviewed changes

Copilot reviewed 27 out of 28 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
README.md Updates product description from “canvas” to “shell” and documents Links/connection auditing.
crates/identra-mcp/tests/bus_http.rs Updates tests to include Edge.by with Grantor.
crates/identra-mcp/src/server.rs Updates tests to include Edge.by with Grantor.
crates/identra-mcp/src/lib.rs Updates tests to include Grantor; continues enforcing edge authorization (undirected).
crates/identra-core/src/canvas.rs Adds Grantor enum and Edge.by with serde default + regression test for Unknown.
apps/identra-desktop/frontend/src/WorkspacePicker.tsx Adjusts workspace tile preview usage to match new preview component API.
apps/identra-desktop/frontend/src/tidy.ts Removes canvas-only “tidy positions” helper.
apps/identra-desktop/frontend/src/tidy.test.ts Removes tests for deleted tidy.ts.
apps/identra-desktop/frontend/src/styles.css Removes React Flow/canvas styling and adds shell/panes/tabs/right-column/connection list styling.
apps/identra-desktop/frontend/src/spawn.ts New extracted process-start logic for panes (dev + agent start errors surface in terminal).
apps/identra-desktop/frontend/src/preview.ts Removes canvas thumbnail layout helper.
apps/identra-desktop/frontend/src/preview.test.ts Removes tests for deleted preview.ts.
apps/identra-desktop/frontend/src/Pane.tsx New pane component replacing node rendering (terminal/browser/file/note bodies).
apps/identra-desktop/frontend/src/nodeState.ts New extracted node status hook for state dots (ready/running/needs-input/exited).
apps/identra-desktop/frontend/src/layout.ts New pure pane-tree model (split/close/set/step/clear) for centre column.
apps/identra-desktop/frontend/src/layout.test.ts Tests for pane-tree operations and edge cases.
apps/identra-desktop/frontend/src/FocusView.tsx Removes full-window focus overlay (no longer needed in pane model).
apps/identra-desktop/frontend/src/FileNode.tsx Removes React Flow file node (replaced by file pane body).
apps/identra-desktop/frontend/src/ConnectionsPanel.tsx New Connections/Links panel listing all edges with grant attribution + revoke.
apps/identra-desktop/frontend/src/BrowserNode.tsx Removes React Flow browser node (replaced by browser pane body).
apps/identra-desktop/frontend/src/BoardPreview.tsx Replaces miniature canvas thumbnail with “running agents” icon row.
apps/identra-desktop/frontend/src/attachTerminal.ts Updates attachment logic to support “pane starts process” and byte-scanning hooks.
apps/identra-desktop/frontend/src/App.tsx Major rewrite: shell layout, tabs, pane tree, right column modes, connection tracking + grantor.
apps/identra-desktop/frontend/src/api.ts Extends Edge with by: Grantor and adds Grantor type for frontend.
apps/identra-desktop/frontend/src/AgentNode.tsx Removes React Flow agent node component (replaced by Pane + extracted logic).
apps/identra-desktop/frontend/package.json Removes @xyflow/react dependency.
apps/identra-desktop/frontend/bun.lock Removes @xyflow/* and related transitive dependencies.

Comment on lines +459 to +461
if (edgesRef.current.some((e) => e.source === from && e.target === to)) {
return;
}
Comment on lines 850 to 853
const lockedReason = (id: string) => {
const name =
nodesRef.current.find((n) => n.id === id)?.data.title ?? id;
return `${name} is locked, so it cannot be wired to by an agent. The person at the keyboard can unlock it or wire it themselves.`;
const name = nodesRef.current.find((n) => n.id === id)?.title ?? id;
return `${name} is locked, so an agent cannot connect to it. The person at the keyboard can unlock it or connect it themselves.`;
};
Comment on lines +78 to +80
{node.kind === "note" && (
<div className="identra-note__body">{node.title}</div>
)}
Comment on lines +30 to +34
const SAID: Record<Edge["by"], string> = {
you: "You connected these",
agent: "An agent connected these itself",
unknown: "Connected before Identra recorded who by",
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants