Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/jaypopat/cf_ai_duet/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Duet’s shared terminal provides a fully synchronized shell environment where all participants see exactly the same output in real time. Every keystroke, command, and output is instantly visible to everyone in the session.

How it works

The terminal is built on three key technologies:

PTY

Pseudoterminal provides a real shell process with full terminal capabilities

VT10x

Terminal emulator handles ANSI escape sequences, colors, and cursor positioning

Pub/Sub

Subscriber pattern broadcasts updates to all connected clients

Terminal initialization

When a room is created, Duet starts a shell in an isolated workspace:
func (t *Terminal) Start() error {
    shell := os.Getenv("SHELL")
    if shell == "" {
        shell = "/bin/sh"
    }

    t.cmd = exec.Command(shell)
    t.cmd.Dir = t.workDir  // Isolated per-room directory
    t.cmd.Env = append(os.Environ(),
        "TERM=xterm-256color",
    )

    t.ptmx, err = pty.StartWithSize(t.cmd, &pty.Winsize{
        Rows: uint16(t.height),
        Cols: uint16(t.width),
    })
}
Each room gets its own shell process running in /app/workspaces/{workspace-name}. This provides complete isolation between sessions.

Real-time synchronization

Duet uses a subscriber pattern to keep all participants in sync:
1

Input from any client

When you type in the terminal, your keystrokes are sent directly to the PTY:
// Special keys are mapped to ANSI sequences
switch key {
case "enter":
    data = []byte("\r")
case "up":
    data = []byte("\x1b[A")
case "backspace":
    data = []byte{127}
}

m.terminal.Write(data)
2

PTY processes the input

The shell receives the input and generates output (stdout/stderr).
3

VT10x parses the output

A background read loop feeds PTY output into the VT10x emulator:
func (t *Terminal) readLoop() {
    buf := make([]byte, 4096)
    for {
        n, err := t.ptmx.Read(buf)
        t.vt.Write(buf[:n])  // Parse ANSI sequences
        t.dirty = true
        t.broadcast()  // Notify all subscribers
    }
}
4

All clients re-render

Each connected client receives a notification and renders the updated terminal state.

Rendering optimization

Duet minimizes CPU usage with smart caching:
func (t *Terminal) Render() string {
    // Return cached render if not dirty
    if !t.dirty && t.lastRender != "" {
        return t.lastRender
    }

    // Render cells with run-length encoding for colors
    for y := 0; y < rows; y++ {
        for x := range cols {
            cell := t.vt.Cell(x, y)
            // Only emit ANSI codes when colors change
            if cell.FG != prevFG {
                sb.WriteString(fgColor(cell.FG))
            }
            sb.WriteRune(cell.Char)
        }
    }

    t.lastRender = sb.String()
    t.dirty = false
    return t.lastRender
}
  • Caching: Only re-renders when PTY output changes (dirty flag)
  • Run-length encoding: ANSI color codes only emitted when colors change
  • Efficient broadcast: Non-blocking channel sends to all subscribers

Window resizing

When your terminal window changes size, Duet automatically adjusts:
func (t *Terminal) Resize(width, height int) {
    t.width = width
    t.height = height
    t.dirty = true
    t.lastRender = ""

    if t.vt != nil {
        t.vt.Resize(width, height)
    }

    if t.ptmx != nil {
        pty.Setsize(t.ptmx, &pty.Winsize{
            Rows: uint16(height),
            Cols: uint16(width),
        })
    }
}
All participants share the same terminal size. If one client resizes their window, it affects everyone. The terminal dimensions are set when the room is created based on the host’s window size.

Typing indicators

Duet shows when other participants are actively typing:
if m.currentRoom != nil && time.Since(m.typingTime) > 500*time.Millisecond {
    m.currentRoom.BroadcastEvent(room.RoomEvent{
        Type:     "typing",
        Username: m.username,
    }, m.clientID)
    m.typingTime = time.Now()
}
Typing events are debounced to 500ms to avoid flooding the network. Indicators disappear after 2 seconds of inactivity.

Cursor rendering

The cursor position is synchronized across all clients using reverse video:
isCursor := cursorVisible && x == cursor.X && y == cursor.Y

if isCursor {
    // Swap foreground/background for cursor
    fg, bg = bg, fg
}
Everyone sees the cursor blinking at the same position, making it clear who’s actively working.

Terminal lifecycle

Supported features

  • Execute any shell command
  • Run interactive programs (vim, htop, etc.)
  • Tab completion
  • Command history (arrow keys)
  • Job control (Ctrl+C, Ctrl+Z)
  • Cursor movement (\x1b[A, \x1b[B, etc.)
  • Text styling (bold, italic, underline)
  • 256-color palette
  • Clear screen / line
  • Save/restore cursor position
All keyboard input is properly mapped:
  • Arrow keys → \x1b[A through \x1b[D
  • Home/End → \x1b[H / \x1b[F
  • Backspace → ASCII 127
  • Delete → \x1b[3~
  • Tab → \t
  • Enter → \r

Workspace isolation

Each room operates in its own directory:
baseDir := "/app/workspaces"
workspaceDir := filepath.Join(baseDir, workspaceName)

// Copy template with basic tools
cmd := exec.Command("cp", "-r", "/app/workspace-template/.", workspaceDir)
Workspaces are completely isolated. You can create files, install packages, and run processes without affecting other rooms. Everything is cleaned up when the session ends.

Limitations

The following are not currently supported:
  • Individual cursor positions per user
  • Different terminal sizes per participant
  • Screen splitting or pane management
  • Terminal scrollback history
All participants share a single unified view of the terminal.

Next steps

AI assistant

Get AI-powered help while coding in the shared terminal

Sandbox execution

Run isolated commands using Cloudflare Sandboxes

Build docs developers (and LLMs) love