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:
- Remove client from room’s connection list
- 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
- Client sends AI message via
Ctrl+G
- Worker returns updated message history
- Client calls
currentRoom.SetAIMessages(msgs)
- Client broadcasts
ai_sync event
- 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.