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.

Rooms are the core abstraction for collaborative sessions in Duet. Each room represents an isolated workspace with a shared terminal and multiple connected clients.

Room Manager

The Manager type in /internal/room/manager.go coordinates all rooms:
type Manager struct {
    rooms     map[string]*Room
    mu        sync.RWMutex
    workerURL string
    aiClient  *ai.Client
    logger    *log.Logger
}

Thread-Safe Operations

The manager uses sync.RWMutex for concurrent access:
  • RLock/RUnlock: Used for read operations (GetRoom, RoomCount, GetAIClient)
  • Lock/Unlock: Used for write operations (CreateRoom, LeaveRoom)
This allows multiple clients to read room state simultaneously while preventing race conditions during modifications.

Room Structure

Each room contains:
type Room struct {
    ID           string
    Description  string
    Host         string
    Connections  []*Client
    mu           sync.RWMutex
    Terminal     *terminal.Terminal
    AIMessages   []AIMessage
    WorkspaceDir string
}

Key Fields

  • ID: UUID generated at creation time
  • Description: Optional user-provided description (used for workspace naming)
  • Host: Username of the room creator
  • Connections: List of connected clients
  • Terminal: Shared terminal instance (nil until first client starts it)
  • AIMessages: Conversation history for AI chat feature
  • WorkspaceDir: Isolated filesystem directory for this room

Client Structure

type Client struct {
    ID       string
    Username string
    IsHost   bool
    Events   chan RoomEvent
}
Each client has:
  • Unique ID: UUID to distinguish multiple connections from same user
  • Display name: From SSH session username
  • Host flag: Only the creator has this set to true
  • Event channel: Receives room events (join/leave/typing/ai_sync)

Room Lifecycle

1. Room Creation

func (m *Manager) CreateRoom(host, description string) (*Room, error) {
    m.mu.Lock()
    defer m.mu.Unlock()

    roomID := uuid.New().String()

    // Generate workspace name from description or random name
    var workspaceName string
    if description != "" {
        workspaceName = slugify(description)
    }
    if workspaceName == "" {
        workspaceName = fmt.Sprintf("%s-%s", 
            adjectives[rand.Intn(len(adjectives))], 
            nouns[rand.Intn(len(nouns))])
    }

    // Create isolated workspace directory
    baseDir := "/app/workspaces"
    if _, err := os.Stat(baseDir); os.IsNotExist(err) {
        baseDir = filepath.Join(os.TempDir(), "duet-workspaces")
    }
    workspaceDir := filepath.Join(baseDir, workspaceName)

    // Copy template if available
    cmd := exec.Command("cp", "-r", "/app/workspace-template/.", workspaceDir)
    if err := cmd.Run(); err != nil {
        // Fallback: create empty directory
        if err := os.MkdirAll(workspaceDir, 0755); err != nil {
            return nil, fmt.Errorf("failed to create workspace directory: %w", err)
        }
    }

    room := &Room{
        ID:           roomID,
        Description:  description,
        Host:         host,
        Connections:  make([]*Client, 0),
        WorkspaceDir: workspaceDir,
    }
    m.rooms[roomID] = room
    return room, nil
}
Workspace Naming:
  • If description provided: slugify("My Project")my-project
  • Otherwise: random name like cosmic-phoenix or brave-dragon
Workspace Template: The server attempts to copy /app/workspace-template/ contents into each workspace, providing pre-configured files or tools. Falls back to an empty directory if template is unavailable.

2. Joining a Room

func (m *Manager) GetRoom(roomID string) (*Room, error) {
    m.mu.RLock()
    defer m.mu.RUnlock()

    room, exists := m.rooms[roomID]
    if !exists {
        return nil, ErrRoomNotFound
    }

    return room, nil
}
Clients join by providing the room ID. The manager returns the room reference if it exists.

3. Client Registration

In the UI model (/internal/ui/model.go):
func (m *Model) registerAsClient(r *room.Room, isHost bool) {
    m.eventChan = make(chan room.RoomEvent, 10)

    client := &room.Client{
        ID:       m.clientID,
        Username: m.username,
        IsHost:   isHost,
        Events:   m.eventChan,
    }
    r.AddClient(client)
}
The room broadcasts a join event to all existing clients:
func (r *Room) AddClient(client *Client) {
    r.mu.Lock()
    defer r.mu.Unlock()

    // Remove duplicate if reconnecting
    for i, c := range r.Connections {
        if c.ID == client.ID {
            if c.Events != nil {
                close(c.Events)
            }
            r.Connections = remove(r.Connections, i)
            break
        }
    }

    r.Connections = append(r.Connections, client)

    // Notify other clients
    for _, c := range r.Connections {
        if c.ID != client.ID && c.Events != nil {
            select {
            case c.Events <- RoomEvent{Type: "join", Username: client.Username}:
            default:
            }
        }
    }
}
Non-Blocking Sends: The select with default case ensures event sends never block. If a client’s event channel is full, the event is dropped for that client.

4. Leaving a Room

func (m *Manager) LeaveRoom(roomID, clientID string) bool {
    m.mu.Lock()
    defer m.mu.Unlock()

    room, exists := m.rooms[roomID]
    if !exists {
        return false
    }

    room.RemoveClient(clientID)

    // Clean up if room is now empty
    if room.ClientCount() == 0 {
        if room.Terminal != nil {
            room.Terminal.Close()
            room.Terminal = nil
        }
        // Delete workspace directory
        if room.WorkspaceDir != "" {
            os.RemoveAll(room.WorkspaceDir)
        }
        // Cleanup external resources
        if m.workerURL != "" {
            go m.cleanupRoomResources(roomID)
        }
        delete(m.rooms, roomID)
        return true // Room destroyed
    }
    return false // Room still active
}
Cleanup Actions:
  1. Remove client from room’s connection list
  2. If last client:
    • Close terminal and kill shell process
    • Delete workspace directory from filesystem
    • Send DELETE request to worker (if configured) to clean up sandbox and AI state
    • Remove room from manager’s map

5. Resource Cleanup

External resources are cleaned up asynchronously:
func (m *Manager) cleanupRoomResources(roomID string) {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    
    url := fmt.Sprintf("%s/api/rooms/%s", m.workerURL, roomID)
    req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
    if err != nil {
        m.logger.Warn("failed to create cleanup request", "roomID", roomID, "error", err)
        return
    }
    
    httpClient := &http.Client{Timeout: 10 * time.Second}
    resp, err := httpClient.Do(req)
    if err != nil {
        m.logger.Warn("failed to cleanup room resources", "roomID", roomID, "error", err)
        return
    }
    defer resp.Body.Close()
    
    m.logger.Info("cleaned up room resources", "roomID", roomID)
}
This runs in a goroutine so it doesn’t block the client’s disconnect.

Event Broadcasting

Room Events

type RoomEvent struct {
    Type     string
    Username string
    Data     string
}
Event Types:
  • join: User joined the room
  • leave: User left the room
  • typing: User is typing in terminal (debounced to 500ms)
  • ai_sync: AI chat messages updated (triggers viewport refresh)

Broadcast Implementation

func (r *Room) BroadcastEvent(event RoomEvent, excludeClientID string) {
    r.mu.RLock()
    defer r.mu.RUnlock()

    for _, c := range r.Connections {
        if c.ID != excludeClientID && c.Events != nil {
            select {
            case c.Events <- event:
            default:
            }
        }
    }
}
The excludeClientID parameter prevents echoing events back to the sender.

Workspace Isolation

Each room’s workspace provides:

Directory Structure

/app/workspaces/
├── cosmic-phoenix/     (room 1)
│   ├── [template files]
│   └── [user files]
├── my-project/         (room 2)
│   └── ...
└── brave-dragon/       (room 3)
    └── ...

Template Files

If /app/workspace-template/ exists, it’s copied to each workspace:
# Production setup might include:
workspace-template/
├── .bashrc
├── .vimrc
├── README.md
└── tools/
    └── helper-scripts

Terminal Working Directory

When starting the terminal:
workDir := "/app"
if m.currentRoom != nil && m.currentRoom.WorkspaceDir != "" {
    workDir = m.currentRoom.WorkspaceDir
}

m.terminal = terminal.New(terminalW, termH, workDir)
The shell process executes with cmd.Dir = workDir, providing filesystem isolation.

AI Message Synchronization

Rooms store AI conversation history:
type AIMessage struct {
    Role   string `json:"role"`
    UserID string `json:"user_id"`
    Text   string `json:"text"`
    Ts     int64  `json:"ts"`
}

Thread-Safe Access

func (r *Room) SetAIMessages(msgs []AIMessage) {
    r.mu.Lock()
    defer r.mu.Unlock()
    r.AIMessages = msgs
}

func (r *Room) GetAIMessages() []AIMessage {
    r.mu.RLock()
    defer r.mu.RUnlock()
    result := make([]AIMessage, len(r.AIMessages))
    copy(result, r.AIMessages)
    return result
}

Sync Flow

  1. Client sends AI message via Ctrl+G
  2. Worker returns updated message history
  3. Client calls currentRoom.SetAIMessages(msgs)
  4. Client broadcasts ai_sync event
  5. Other clients receive event and call GetAIMessages() to refresh their viewport
This ensures all clients see the same AI conversation in real-time.

Concurrency Patterns

Read-Heavy Optimization

sync.RWMutex is used because:
  • Reads (GetRoom, RoomCount) are frequent (every client action)
  • Writes (CreateRoom, LeaveRoom) are infrequent (only on join/leave)
Multiple goroutines can hold read locks simultaneously, improving performance.

Channel-Based Events

Each client has a buffered channel (chan RoomEvent, 10) that:
  • Decouples event producers (broadcast) from consumers (client UI loop)
  • Prevents blocking if client is slow to process
  • Automatically closes when client disconnects

Graceful Reconnection

If a client reconnects with the same ID:
for i, c := range r.Connections {
    if c.ID == client.ID {
        if c.Events != nil {
            close(c.Events)
        }
        r.Connections = remove(r.Connections, i)
        break
    }
}
The old channel is closed and the client is re-added with a fresh channel.

Build docs developers (and LLMs) love