> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dedaluslabs.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Terminals

> PTY sessions on a Dedalus Machine, streamed over WebSocket

export const Pop = ({content, children}) => {
  const [open, setOpen] = useState(false);
  const [hoverOpen, setHoverOpen] = useState(false);
  const [isDark, setIsDark] = useState(false);
  const triggerRef = useRef(null);
  const portalRef = useRef(null);
  const closeTimer = useRef(null);
  const isOpen = open || hoverOpen;
  useEffect(() => {
    const probe = () => setIsDark(document.documentElement.classList.contains("dark"));
    probe();
    const obs = new MutationObserver(probe);
    obs.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"]
    });
    return () => obs.disconnect();
  }, []);
  const onEnter = () => {
    clearTimeout(closeTimer.current);
    setHoverOpen(true);
  };
  const onLeave = () => {
    closeTimer.current = setTimeout(() => setHoverOpen(false), 150);
  };
  useEffect(() => {
    if (!isOpen) return;
    const node = document.createElement("div");
    node.setAttribute("role", "dialog");
    node.textContent = content;
    document.body.appendChild(node);
    portalRef.current = node;
    const dark = document.documentElement.classList.contains("dark");
    Object.assign(node.style, {
      position: "fixed",
      zIndex: "9999",
      maxWidth: "20rem",
      width: "max-content",
      padding: "0.5rem 0.75rem",
      fontSize: "0.875rem",
      lineHeight: "1.55",
      color: dark ? "rgb(244, 244, 245)" : "rgb(39, 39, 42)",
      backgroundColor: dark ? "rgba(24, 24, 27, 0.4)" : "rgba(255, 255, 255, 0.4)",
      backdropFilter: "blur(4px) saturate(160%)",
      WebkitBackdropFilter: "blur(4px) saturate(160%)",
      border: dark ? "1px solid rgba(255, 255, 255, 0.18)" : "1px solid rgba(0, 0, 0, 0.16)",
      boxShadow: "0 8px 32px rgba(0, 0, 0, 0.12)",
      opacity: "0",
      transform: "translateY(-4px)",
      transition: "opacity 180ms cubic-bezier(0.16, 1, 0.3, 1), transform 180ms cubic-bezier(0.16, 1, 0.3, 1)"
    });
    const place = () => {
      const r = triggerRef.current?.getBoundingClientRect();
      if (!r || !portalRef.current) return;
      portalRef.current.style.left = `${r.left}px`;
      portalRef.current.style.top = `${r.bottom + 8}px`;
    };
    place();
    requestAnimationFrame(() => {
      node.style.opacity = "1";
      node.style.transform = "translateY(0)";
    });
    node.addEventListener("mouseenter", onEnter);
    node.addEventListener("mouseleave", onLeave);
    window.addEventListener("scroll", place, true);
    window.addEventListener("resize", place);
    return () => {
      window.removeEventListener("scroll", place, true);
      window.removeEventListener("resize", place);
      node.remove();
      portalRef.current = null;
    };
  }, [isOpen, content]);
  useEffect(() => {
    if (!open) return;
    const dismiss = e => {
      const inside = triggerRef.current?.contains(e.target) || portalRef.current?.contains(e.target);
      if (!inside) {
        setOpen(false);
        setHoverOpen(false);
      }
    };
    const onKey = e => e.key === "Escape" && (setOpen(false), setHoverOpen(false));
    document.addEventListener("mousedown", dismiss);
    document.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("mousedown", dismiss);
      document.removeEventListener("keydown", onKey);
    };
  }, [open]);
  return <button ref={triggerRef} type="button" onClick={() => setOpen(v => !v)} onMouseEnter={onEnter} onMouseLeave={onLeave} aria-expanded={isOpen} className="cursor-pointer bg-transparent border-0 p-0 text-inherit underline decoration-dotted underline-offset-[3px] transition-colors" style={{
    textDecorationColor: isDark ? "rgb(113, 113, 122)" : "rgb(161, 161, 170)"
  }}>
			{children}
		</button>;
};

A terminal is a <Pop content="A pseudo-terminal: a program-facing terminal device that behaves like a real interactive shell.">PTY</Pop> opened inside the machine, with stdin/stdout streamed to your client over a WebSocket. The machine wakes if it was sleeping.

<CodeGroup>
  ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}}
  dedalus machines terminals create \
    --machine-id <machine_id> \
    --width 80 \
    --height 24 \
    --shell /bin/bash \
    --cwd /root \
    --env FOO=bar
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  term = client.machines.terminals.create(
      machine_id="<machine_id>",
      width=80,
      height=24,
      shell="/bin/bash",            # optional
      cwd="/root",                  # optional
      env={"FOO": "bar"},           # optional
  )
  print(term.stream_url)            # wss://...
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const term = await client.machines.terminals.create({
    machine_id: "<machine_id>",
    width: 80,
    height: 24,
  });
  console.log(term.stream_url);
  ```

  ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}}
  term, _ := client.Machines.Terminals.New(ctx, dedalus.MachineTerminalNewParams{
      MachineID: machineID,
      TerminalCreateParams: dedalus.TerminalCreateParams{
          Width:  80,
          Height: 24,
          Shell:  dedalus.String("/bin/bash"),
          Cwd:    dedalus.String("/root"),
          Env:    map[string]string{"FOO": "bar"},
      },
  })
  fmt.Println(term.StreamURL)
  ```
</CodeGroup>

<Accordion title="Parameters">
  <ParamField path="machine_id" type="string" required>
    The machine where the terminal opens.
  </ParamField>

  <ParamField body="width" type="integer" required>
    Initial terminal width in columns.
  </ParamField>

  <ParamField body="height" type="integer" required>
    Initial terminal height in rows.
  </ParamField>

  <ParamField body="shell" type="string" default="/bin/bash">
    Shell to start inside the PTY.
  </ParamField>

  <ParamField body="cwd" type="string" default="/root">
    Working directory for the shell.
  </ParamField>

  <ParamField body="env" type="object">
    Extra environment variables for the shell.
  </ParamField>
</Accordion>

## Wire protocol

| Direction       | Frame       | Payload                                                          |
| --------------- | ----------- | ---------------------------------------------------------------- |
| Client → server | Binary      | Raw bytes written to the shell's stdin (keystrokes, paste, etc.) |
| Client → server | Text (JSON) | `{"cols": 100, "rows": 30}` to resize the PTY                    |
| Server → client | Binary      | Raw bytes from the shell's stdout/stderr (ANSI escapes intact)   |
| Server → client | Close       | Shell exited or session expired                                  |

## Manage terminals

<CodeGroup>
  ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}}
  dedalus machines terminals list --machine-id <machine_id>
  dedalus machines terminals retrieve --machine-id <machine_id> --terminal-id <terminal_id>
  dedalus machines terminals delete --machine-id <machine_id> --terminal-id <terminal_id>
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  client.machines.terminals.list(machine_id="<machine_id>")
  client.machines.terminals.retrieve(machine_id="<machine_id>", terminal_id="<terminal_id>")
  client.machines.terminals.delete(machine_id="<machine_id>", terminal_id="<terminal_id>")
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  await client.machines.terminals.list({ machine_id: "<machine_id>" });
  await client.machines.terminals.retrieve({
    machine_id: "<machine_id>",
    terminal_id: "<terminal_id>",
  });
  await client.machines.terminals.delete({
    machine_id: "<machine_id>",
    terminal_id: "<terminal_id>",
  });
  ```

  ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}}
  client.Machines.Terminals.List(ctx, dedalus.MachineTerminalListParams{
      MachineID: machineID,
  })
  client.Machines.Terminals.Get(ctx, dedalus.MachineTerminalGetParams{
      MachineID:   machineID,
      TerminalID: "<terminal_id>",
  })
  client.Machines.Terminals.Delete(ctx, dedalus.MachineTerminalDeleteParams{
      MachineID:   machineID,
      TerminalID: "<terminal_id>",
  })
  ```
</CodeGroup>

<Accordion title="Parameters">
  <ParamField path="machine_id" type="string" required>
    The machine that owns the terminal.
  </ParamField>

  <ParamField path="terminal_id" type="string" required>
    The terminal to retrieve or close.
  </ParamField>
</Accordion>
