Skip to content

Export one embedded terminal API for attached and spawned PTYs #3

Description

@schickling-assistant

Outcome

Export one Rust terminal handle with two clear constructors:

  • TerminalHandle::attach attaches to an existing persistent PTY session. The handle does not own the child. Closing it detaches.
  • TerminalHandle::spawn starts a local ephemeral child in an embedded PTY. The handle owns the child. Closing it terminates and reaps the child.

Do not add create_tty. Interactive programs such as lazygit require a pseudo-terminal, terminal geometry, and control-sequence handling. A TTY name describes the wrong operating-system object.

Both constructors return the same deep terminal interface. Fractal must not implement a second terminal emulator or a second focus and input adapter for ephemeral tools.

Attach identity

SessionRef declares a PTY root and session ID.

  • The pair means the current session at that address.
  • If a session exits and a replacement uses the same root and ID, a new attach targets the replacement.
  • Generation can be observed for diagnostics when available.
  • Generation is not required for discovery, preflight, or attach eligibility.
  • Each attach and reconnect attempt has a private local AttemptId or epoch.
  • The handle drops late events from an older attempt before they can update current terminal state.

The current Node PTY can omit generation from list and stats. That omission must not block attach.

Use cases

  • Attach to a long-lived coding-agent session.
  • Spawn lazygit in the selected agent workspace.
  • Spawn tail -f or another short-lived log viewer.
  • Spawn a shell or interactive maintenance command that should end with its pane or owner.

Proposed public shape

pub struct SessionRef {
    pub root: PtyRoot,
    pub id: SessionId,
}

pub enum TerminalOrigin {
    Attached { session: SessionRef },
    Spawned { child: ChildId },
}

impl TerminalHandle {
    pub async fn attach(session: SessionRef, options: AttachOptions) -> Result<Self>;
    pub async fn spawn(command: Command, options: SpawnOptions) -> Result<Self>;

    pub fn origin(&self) -> &TerminalOrigin;
    pub fn write(&self, bytes: &[u8]) -> Result<()>;
    pub fn resize(&self, size: TerminalSize) -> Result<()>;
    pub fn snapshot(&self, viewport: Viewport) -> TerminalSnapshot;
    pub fn events(&self) -> impl Stream<Item = TerminalEvent>;
    pub async fn close(self) -> Result<TerminalOutcome>;
}

This is an interface sketch. Exact Rust names can change. The required property is one handle with explicit ownership and close behavior.

Lifecycle rules

Origin Process owner Close behavior Reconnect Durable registry
Attached external PTY daemon detach only supported by client policy yes
Spawned TerminalHandle actor terminate and reap not required no

A spawned handle can use an explicit grace policy. It must not become a hidden persistent daemon or an st2-reconciled service.

Input and byte contract

  • Terminal output remains raw bytes into libghostty.
  • Interactive text is UTF-8 plus terminal control sequences.
  • Incomplete UTF-8 is buffered across reads.
  • Malformed input is reported. It is not silently replaced.
  • The current PTY wire frame is already byte-safe.
  • Binary input is a future negotiated capability only if a real byte-round-trip fixture requires it.

Acceptance

  • Both constructors use the same libghostty actor, typed events, snapshots, cell grid, wrapped-line facts, cursor state, mode state, and scrollback API.
  • Attach accepts declared root plus session ID and resolves the current session.
  • Attach works when list and stats do not provide generation.
  • A new attach reaches a replacement under the same root and session ID.
  • Generation, when observed, is diagnostic only.
  • Late events from an older local AttemptId or epoch cannot update the current handle.
  • Attach uses the current PTY protocol and never takes child ownership.
  • Spawn uses portable-pty, owns the child, and reaps it on normal exit and close.
  • Dropping or closing an attached handle cannot kill the persistent session.
  • Dropping or closing a spawned handle cannot leak its process or process group.
  • Resize, alternate screen, mouse, Kitty keyboard, paste, focus, and selection behavior are source-independent above the handle.
  • Tests cover lazygit, log tailing, shell exit, forced close, resize, owner crash, and no leaked child.
  • Node and Rust conformance fixtures cover shared attach and spawn behavior.
  • Observable language or libghostty deviations are recorded as decisions.
  • No required generation-fenced attach path exists.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions