The canvas becomes three columns, and a connection stops being invisible - #38
Merged
keirsalterego merged 3 commits intoAug 2, 2026
Conversation
…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.
There was a problem hiding this comment.
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) toEdge(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", | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
AgentNodecarried 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:AgentNodespawn logicspawn.tsAgentNodestatus machinenodeState.tsAgentNode/BrowserNode/FileNode/NoteNodePaneFocusView+REFIT_EVENTtidy.ts,preview.ts, the board thumbnail@xyflow/reactpackage.json1,933 insertions, 2,296 deletions.
Three things less obvious than the layout
Edgeis untouched, deliberately.canvas.rs:35says it plainly: two nodes share context only when an edge joins them, enforced in three places inidentra-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,yand 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,efor 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.tsis 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_nodesis 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
Edgewas kept as data when everything else about the canvas went.Grantorisunknown | you | agent, andUnknownis the default — not a placeholder forYou. 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.get_peer_contextre-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-mcpneeded no change. Same slice, same refusals. The field is additive with a serde default, exactly aslockedandseatwere.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 testand the frontend build are clean.