Roadmap for three connected workstreams. Two live in this repo (hyperpanes); the MCP server is a separate project that sits on top of the control surface we add here.
Locked design decisions (2026-06-05):
- MCP scope — both, phased. Phase 1 is stateless launch-config generation (compose
a workspace + shell out to
hyperpanes). Phase 2 adds live control of a running instance (read/inspect panes, send input, mutate layout). - Instance model — single canonical process. Add
app.requestSingleInstanceLock(); a secondhyperpanes …invocation (and every MCP call) routes into the running process and adds windows/tabs instead of starting a rival process. - CLI scope — per-pane flags +
--tab/--windowseparators. Full multi-tab / multi-window parity with the JSON format; JSON stays the canonical format for large setups, the CLI grammar compiles down to the same structure.
The single-instance lock is the keystone. Launch-time multi-window and MCP both need a
canonical running process to target ("open this in my running session", "the agent drives
the app"). The expanded CLI and the JSON windows layer must produce the same
in-memory structure so there's one seeding code path. MCP Phase 2 is a thin adapter over a
local control API we expose from main.
Sequence: schema + single-instance → CLI → control API → MCP (P1 then P2).
M0, M1, M2 BUILT & static-verified (typecheck + 108 unit tests + electron-vite build
all green); manual GUI pass still pending. M3/M4 (the separate MCP project) not started —
handoff doc in OS temp (hyperpanes-mcp-handoff.md).
- M0 —
WindowSpeclayer +windowsOfnormalizer (src/main/workspace.ts,src/renderer/types.ts);requestSingleInstanceLock+second-instancerouting (src/main/index.ts); per-window push-seed viagetSeed.windowSpec(ipc.ts/window.ts/App.tsx). Bounds + cascade placement honored. - M1 —
parseClirewritten as a window→tab→pane state machine with--window/--tabseparators + per-pane--cwd/--shell/--font; back-compat legacy shape preserved (workspace.test.ts, 16 cases). - M2 —
src/main/control-server.ts: loopback HTTP, token +userData/control.jsondiscovery, off by default,allowInputsecond gate. Renderers publish structure (control.ts+App.tsx); Preferences → General has the opt-in toggles. - M2b — WebSocket event stream BUILT (2026-06-05).
/eventsWebSocket on the same port + token (via?token=), usingws. Pushes{type:'output',sessionUid,paneId,data},{type:'exit',…,code}, and a coalesced{type:'state'}re-fetch ping; greets with{type:'hello',pid,version}.control.jsonnow includes the fulleventsws URL. Output/ exit are teed from ipc's session handlers (no-op when no client connected); asessionUid→ paneIdreverse index (rebuilt only on structure change) keeps the per-batch path O(1). Poll routeGET /panes/:id/output?tail=Nremains for the initial backlog.
Total launch granularity — DONE (2026-06-05). GroupSpec gained sizes (per-pane
split fractions), mainFraction (Main+Stack split), and focused/zoomed (pane indices),
so a launched/restored tab reproduces its exact split + which pane is focused/maximized.
groupFromSpec consumes them (defensively validated → defaults on bad input; auto layout
stays equal); specFromGroup emits them only when non-default. Mirrored in the MCP schema
(C:\hyperpanes-mcp\src\schema.ts) + flagged CLI-lossy in compile-cli.ts. JSON/file launch
only — no CLI flags. Still not settable: nothing left on the wish-list; per-pane zoom + split
ratios + intra-tab focus are now all expressible.
Multi-window restore — DONE (2026-06-05). Every window now publishes its tabs to main
(workspace:windowSession); main aggregates by window id, captures per-window bounds, and
writes a windows[] last-workspace.json, so a relaunch restores all windows (with
positions). Empty-guard prevents wiping; a quitting flag stops cascading closes from
shrinking the session; closing one window (app up) drops just that window. Replaced the old
primary-only serializeSession/saveLast path. (109 unit tests.)
parseCli(src/main/workspace.ts) is single-tab only: per-pane-c/-l/--color;--cwd/--shellare launch-wide. It cannot emit thegroupsarray the file format already supports, and there is no window concept anywhere.WorkspaceFile={ name?, layout?, panes[], groups?[], active? };GroupSpec={ title?, layout?, panes[] };PaneSpec={ label?, subtitle?, color?, command?, cwd?, shell?, fontSize? }.- Runtime multi-window already works:
spawnWindow()(src/main/ipc.ts) makes per-window session-owned windows for tear-off, all in one process. - No
requestSingleInstanceLock(src/main/index.ts) → a second launch starts a separate OS process. - Launch seed is pull-based and process-global: the renderer calls
workspace:getInitial→getInitialWorkspace(). There is no per-window seeding. - No external control channel; IPC is internal main↔renderer only.
Add a window layer above tabs (groups). Back-compatible: absent windows ⇒ the
top-level panes/groups describe a single window.
Work:
- Extend types in
src/main/workspace.ts(WorkspaceFile, addWindowSpec) and mirror insrc/renderer/types.ts.resolveCwdsmust also walkwindows[].groups[].panes. src/main/index.ts:requestSingleInstanceLock(); quit if not primary; onsecond-instance(_e, argv, cwd)parse that argv and spawn the declared windows into the running process. Watch argv-slicing differences (dev vs packaged) and macOSopen-file.- Per-window seeding (push, not pull): extend
spawnWindowto accept a window-level seed (multiple tabs + active index), not just the single-GroupPayloadtear-off seed. Main computes the window list and callsspawnWindowonce perWindowSpec;workspace:getInitialstays as the fallback for the very first window only. - Default routing: DONE (2026-06-07). A second
hyperpanes …now attaches into the focused window by default (content lands as new tabs);--new-window(or any--windowseparator) forces a new window, and--attach[=focused|last|<id>]/--into-current+--as tab|panesmake the target/unit explicit.parseCliemits aLaunchRouting;index.ts'ssecond-instancecallsipc.ts'srouteLaunch, which spawns new windows or dispatches a unifiedattachcontrol command into the target window's renderer (a fresh store actionappendGroupsadds the tabs). The sameattachcommand backs the MCPopen_tabtool, so CLI and agents share one path. (First launch is always new-window.) - Persistence:
serializeSession(src/renderer/workspace/serialize.ts) currently snapshots one window. Decide: extend autosave/restore to multi-window, or keep restore single-window for M0 and only honorwindowson explicit file/CLI launch. - Tests: window round-trip + per-window seeding (extend
workspace.test.ts).
Rewrite parseCli into a small window → tab → pane state machine. Separator tokens
segment the pane stream:
hyperpanes \
--window --name app --layout main-stack \
-c "npm run dev" -l server --color "#e5484d" --cwd ./app --shell pwsh \
-c "tail -f log" -l logs --font 12 \
--tab --name tests --layout columns \
-c "vitest" -l unit \
--window --name db \
-c "psql mydb" -l psql --cwd ./dbRules:
--windowstarts a new window (own--name/title,--layout, optional--active).--tabstarts a new tab (group) in the current window.-c/--commandadds a pane to the current tab;-l/--label,--color,--cwd,--shell,--fontattach to the most recent-c.- Back-compat: with no separators it's one tab / one window (today's behavior).
--cwd/--shellset before the first-cremain launch-wide defaults, overridable per-pane after a-c. - Output:
parseClinow returnswindows[]; both CLI and JSON funnel into the same M0 seeding path. - Tests: extend
workspace.test.tsfor separators, attachment, and back-compat.
A loopback control server in main, off by default behind a setting.
- Transport:
127.0.0.1HTTP + WebSocket (reachable from a Node MCP server in any language). Per-instance bearer token. - Discovery: write
{ port, token, pid }touserData/control.jsonon start (single instance ⇒ exactly one file). The CLI and MCP server read it to find the running app. - Read surface:
listWindows/Tabs/Panes,readPane(tail N | scrollback),paneStatus(running/exitCode),getSelection. - Control surface:
spawn{Window,Tab,Pane}(spec),close{Pane,Tab,Window},focusPane,setLayout,rename/recolor,restartPane,movePane. - Input surface (sharp edge):
sendInput(paneId, text)/sendKey— see Safety. - Events (WS): pane output stream, exit, focus change.
- Implementation: node-pty lives in
main(session.ts/session-manager.ts), so read-output and send-input can be served main-side (tee the existing batched pty output; addwrite(uid, data)). Pane/tab/window structure is owned by the renderer store (useWorkspace); keep a lightweight read-model mirrored inmainfor queries, route structural mutations renderer-side via IPC.
Stateless; depends only on the M1 CLI. No running-app dependency — ships first.
- Tools:
build_workspace(spec) → writes/returns JSON,launch_workspace(path|spec)→ spawnshyperpanes,list_layouts,validate_workspace. - Essentially typed codegen + shell-out, with the
windows/groups/panesschema as the contract.
Thin adapter over the M2 control API.
- Tools:
list_panes,read_pane(tail),open_pane,set_layout,close_pane,restart_pane,focus_pane,send_input(guarded), plus pane output exposed as MCP resources with subscriptions for streaming. - Safety model for
send_input(an agent typing into live shells):- Control server bound to loopback + token; disabled by default, enabled via an explicit in-app toggle ("Allow agent control").
send_inputgated behind an allowlist / per-call confirmation; never on by default.- Document the risk prominently in the MCP project README.
- Single-instance argv differs dev vs packaged; handle macOS
open-fileand Windows shortcut args. - Per-window seeding interacts with autosave/restore — decide multi-window persistence scope (M0 note).
- Control-API security: loopback bind, token, explicit consent for input.
- Keep JSON canonical; the CLI grammar is sugar that compiles to the same
windows[]structure — one seeding/validation path, not two.
WorkspaceFile { name?, // legacy single-window fields kept for back-compat: layout?, panes[], groups?[], active?, // new: windows?: WindowSpec[] } WindowSpec { title?, active?: number, // active tab index bounds?: { x?, y?, width?, height?, maximized?, fullscreen? }, groups: GroupSpec[] // tabs in this window }