Add host-owned Project Actions with remote execution and worktree setup - #158
Add host-owned Project Actions with remote execution and worktree setup#158jsgrrchg wants to merge 23 commits into
Conversation
# Conflicts: # crates/engine/src/lib.rs # crates/engine/src/rpc.rs # crates/engine/tests/device_routing.rs # crates/proto/src/entities.rs # crates/ui/src/lib.rs # crates/ui/src/shell/tabs.rs
wingleeio
left a comment
There was a problem hiding this comment.
Requesting changes for two reproduced functional regressions at d48351a9a5c0c5bb801900daeea130d8a8a8354d (inline findings below).
Validation on the PR head:
cargo test --locked -p zeron-proto: 26 passed.cargo test --locked -p zeron-engine --lib project_actions: 8 passed.- Engine integration suites
device_routing,m5_repos_diffs_terminals, andworktree_on_run: 7 + 23 + 1 passed. cargo test --locked -p zeron-ui --lib project_actions: 5 passed.git diff --check origin/main...HEAD: passed.- Additional regression test: a 1,000-character
printfpayload executes, but a 6,000-character payload produces no output file. Reproduced separately withSHELL=/bin/sh,/bin/bash, and/bin/zshthroughlaunch_project_action; the launch reports success in each failing case. - Native Linux UI, built from this head with an isolated engine/project and an added capture example: imported a suggested Action, explicitly saved it, and launched it through the actual title-bar control. The terminal displayed the host-resolved project and checkout paths. Populated 50 saved Actions through the real RPC and confirmed that scrolling cannot reach the lower menu entries.
The 70 targeted tests pass, but do not cover these two failures. The PR's Linux browser CI also currently fails with browser input did not return after menu dismissal; this review does not establish whether that separate failure is caused by this change. I did not run macOS or iOS locally.
Screenshots below are native application captures from the isolated fixture, uploaded as GitHub user attachments. No screenshot assets or audit harness changes were committed to the PR.
Import and manual execution
Imported command awaiting explicit save
Saved Action launched from the title bar; output replayed in the terminal




Summary
This PR adds first-class Project Actions to Zeron: named shell commands that can be created, imported, edited, deleted, and launched from the selected chat's title bar.
Actions are private, account-scoped configuration owned by the engine that owns the project. Local and remote viewports use the same device-routed RPCs, while the owning engine remains responsible for resolving the project, checkout, environment, shell, and terminal.
One explicitly saved Action may also run automatically when Zeron creates a new worktree. Desktop carries worktree creation in the durable queued
Run; the host starts setup before agent dispatch and exposes the already-open terminal through a command-scoped handoff without blocking send on worktree materialization or command completion.Motivation
Projects often have a small set of commands that are repeatedly needed alongside an agent session: development servers, tests, linters, builds, or one-time worktree setup.
Running those commands from a remote viewport is not a UI-only problem. The project path, checkout, shell, persisted command, and PTY all belong to the device that owns the project. This implementation therefore makes Actions a host-owned engine feature and treats the desktop control as a remote-capable viewport onto that authority.
The trust boundary is also explicit. A repository may suggest commands through
zeron.json, but merely opening or cloning it never authorizes execution. Every suggested Action must be imported and saved before it can run manually or become worktree setup.User-facing behavior
Add action; importable Actions are listed in the menu whenzeron.jsonis present.Actions unavailableretry surface. Engines that do not support the new RPCs hide the control without repeated errors.Architecture
Host-local storage
ProjectActionsStorepersists Actions in the active engine profile'sproject-actions.jsonrather than in the synced Loro registry, session documents, or edge state.The store is versioned, account-scoped, protected by an internal mutex, and written atomically through a temporary file plus rename. Each project entry records both
space_idand the observed project root so an id cannot silently reuse commands after its root changes.The owning engine normalizes all drafts and enforces the following limits:
runOnWorktreeCreateenabled; saving a new setup Action atomically clears the previous one.An absent, corrupt, or unsupported-version store does not prevent the engine from opening. The engine starts with an empty in-memory store and repairs persistence on the next successful write.
Explicit imports from
zeron.jsonThe owning engine reads an optional
zeron.jsonfrom the exact project root and returns valid candidates asimportableActions:{ "actions": [ { "name": "Dev server", "command": "pnpm dev", "icon": "play", "runOnWorktreeCreate": false } ] }The parser rejects malformed JSON, unknown fields, unknown icons, invalid drafts, files above 256 KiB, or lists above 50 entries as a whole. Candidates are hidden when a saved Action already has the same exact command or case-insensitive name.
Imports are suggestions only. They cannot be addressed by
RunProjectActionuntil the user explicitly imports and saves them into the host-local store.Device-routed RPCs
The PR adds four unary, relay-forwardable methods:
Each desktop request includes
targetDeviceIdwhen the selected chat is owned by another device. The destination engine strips the forwarding metadata through the existing relay path and executes the same local handler used by a local viewport.The client sends stable ids and drafts, never a run-time command, cwd, project root, or reserved environment variables. Before any run, the owning engine verifies that the Space is local, the Chat belongs to the same device and Space, and the requested cwd resolves to a valid checkout of that project.
There is no local fallback when a remote owner is offline or unreachable.
Managed terminal execution
Terminalscan now open a login shell with host-resolved environment overrides, while the existing terminal path continues to use an empty override map.Every Action invocation opens a fresh PTY, writes the saved command followed by carriage return, and returns a
ProjectActionRuncontaining the terminal session. Zeron does not wrap the command insh -cand does not attempt to infer whether an existing interactive terminal is idle.Manual runs receive:
If initial input fails after opening the PTY, the engine closes the session instead of leaking a terminal slot.
Desktop reserves a named placeholder tab before the RPC completes, attaches the returned host-side session to it, and reuses the existing subscribe/reconnect/replay pipeline. Output emitted before subscription is therefore recovered from terminal replay, and subsequent subscribe, resize, write, and close requests preserve the terminal's owning
targetDeviceId.Worktree setup
Desktop includes an optional
spaceIdinRunRequest.worktree. The owning engine materializes that worktree while draining the durable command, validates the Space root, starts the saved setup Action, and records its terminal in a short-lived handoff keyed by the queued command id.Desktop polls
TakeProjectActionSetupafterQueueCommandacknowledges. This poll is detached from composer send state, relay-forwardable throughtargetDeviceId, one-shot, and ignored when an older host does not implement it. Setup always receives bothZERON_PROJECT_ROOTandZERON_WORKTREE_PATH.The setup launch waits only until the PTY accepts the command. It does not wait for command exit or readiness. A setup failure is logged and handed to desktop as
setupError; the created worktree remains valid and the first agent turn continues.Desktop attaches the handoff terminal to the exact minted chat, even if selection changes while setup is in flight. Direct
CreateWorktreecallers, including iOS, retain the optionalspaceIdand flattenedCreateWorktreeOutcomepath.Only
NewWorktreetriggers setup. The main checkout and reused worktrees retain their existing behavior.UI state and resilience
The desktop controller caches snapshots by
(device_id, space_id)and models idle, loading, ready, saving, unavailable, and unsupported states.Load generations prevent a late response from a previously selected project from replacing the active snapshot. Mutation generations additionally prevent superseded save/delete responses from closing or updating an editor for a different project.
Transport failures preserve the last successful snapshot when one exists. An initial failure synthesizes an empty visible snapshot so the title-bar control remains recoverable and can expose the retry menu instead of disappearing permanently.
UnknownMethodis treated as version skew rather than a transport failure, allowing a new desktop to continue using chats hosted by an older engine without rendering a broken control.End-to-end flows
Manual Action
New worktree with setup
Compatibility
targetDeviceId; paths and PTYs remain on the owner.UnknownMethodhides Actions; the chat remains usable and no local fallback occurs.spaceIdretain legacy worktree behavior and do not trigger setup.Worktreereplies decode asCreateWorktreeOutcomewith absent setup fields.spaceIdenables host-side setup; iOS continues decoding the returned worktree path only.The flattened outcome preserves the existing top-level Worktree fields, so serializing it still decodes as the legacy
Worktreetype.Trust, safety, and failure semantics
zeron.jsonnever authorizes execution by itself.., or the viewer's interpretation of a remote path.Validation
The current branch was validated with:
cargo test -p zeron-proto— 19 passed.cargo test -p zeron-engine project_actions --lib— 7 passed.cargo test -p zeron-engine --test device_routing— 3 passed, including remote CRUD/run/setup ownership, replay, and offline forwarding behavior.cargo test -p zeron-engine --test m5_repos_diffs_terminals— 21 passed, including host-resolved manual runs, setup, fresh PTYs, environment injection, cleanup, and legacy worktree behavior.cargo test -p zeron-ui project_actions --lib— 5 passed, including preferred selection, responsive collapse, version skew, retry visibility, and stale load/mutation protection.CreateWorktreecaller.git diff --check main...HEAD.Repo-wide formatting, clippy, and workspace-test commands still encounter pre-existing baseline issues outside this change, including existing formatting drift, warnings promoted to errors, and an unrelated shell-environment test. The targeted suites covering this PR are green.
Manual QA checklist
pwd,ZERON_PROJECT_ROOT, andZERON_WORKTREE_PATHfrom both the main checkout and a worktree.zeron.json, confirm imports remain inert, import one, and verify that it becomes executable only after saving.zeron.jsonand confirm that the issue appears without hiding saved Actions.Reviewer guide
Suggested review order:
crates/proto/src/entities.rs,crates/rpc/src/lib.rs, andcrates/engine/src/project_actions.rsfor the domain model, trust boundary, persistence, imports, and limits.crates/engine/src/rpc.rsandcrates/engine/src/terminals.rsfor device ownership, checkout validation, forwarding, PTY creation, and setup integration.crates/ui/src/terminal/panel.rsfor placeholder tabs and attachment of already-open local or remote terminal sessions.crates/ui/src/project_actions.rsandcrates/ui/src/shell/actions_ui.rsfor UI state, stale-response protection, unavailable recovery, editor behavior, and launch handoff.crates/ui/src/shell/tabs.rs,crates/ui/src/settings.rs, and the Action icon assets for title-bar integration and viewport-local preference.crates/ui/src/composer.rs,crates/ui/src/shell.rs, and the iOS caller for worktree setup propagation.crates/engine/tests/device_routing.rs,crates/engine/tests/m5_repos_diffs_terminals.rs, anddocs/reference/project-actions.mdfor end-to-end guarantees and user-facing documentation.Out of scope
Settings -> Shortcuts.zeron.json.