- Rename binary from claudia-web to opcode-web in Cargo.toml - Update all references in justfile (web commands) - Update console output messages in web_server.rs and web_main.rs - Update documentation in web_server.design.md This completes the project rename from Claudia to Opcode. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
15 KiB
Opcode Web Server Design
This document describes the implementation of Opcode's web server mode, which allows access to Claude Code from mobile devices and browsers while maintaining full functionality.
Overview
The web server provides a REST API and WebSocket interface that mirrors the Tauri desktop app's functionality, enabling phone/browser access to Claude Code sessions.
Architecture
┌─────────────────┐ WebSocket ┌─────────────────┐ Process ┌─────────────────┐
│ Browser UI │ ←──────────────→ │ Rust Backend │ ────────────→ │ Claude Binary │
│ │ REST API │ (Axum Server) │ │ │
│ • React/TS │ ←──────────────→ │ │ │ • claude-code │
│ • WebSocket │ │ • Session Mgmt │ │ • Subprocess │
│ • DOM Events │ │ • Process Spawn │ │ • Stream Output │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Key Components
1. Rust Web Server (src-tauri/src/web_server.rs)
Main Functions:
create_web_server()- Sets up Axum server with routesclaude_websocket_handler()- Manages WebSocket connectionsexecute_claude_command()/continue_claude_command()/resume_claude_command()- Execute Claude processesfind_claude_binary_web()- Locates Claude binary (bundled or system)
Key Features:
- WebSocket Streaming: Real-time output from Claude processes
- Session Management: Tracks active WebSocket sessions
- Process Spawning: Launches Claude subprocesses with proper arguments
- Comprehensive Logging: Detailed trace output for debugging
2. Frontend Event Handling (src/components/ClaudeCodeSession.tsx)
Dual Mode Support:
const listen = tauriListen || ((eventName: string, callback: (event: any) => void) => {
// Web mode: Use DOM events
const domEventHandler = (event: any) => {
callback({ payload: event.detail });
};
window.addEventListener(eventName, domEventHandler);
return Promise.resolve(() => window.removeEventListener(eventName, domEventHandler));
});
Message Processing:
- Handles both string payloads (Tauri) and object payloads (Web)
- Maintains compatibility with existing UI components
- Comprehensive error handling and logging
3. WebSocket Communication (src/lib/apiAdapter.ts)
Request Format:
{
"command_type": "execute|continue|resume",
"project_path": "/path/to/project",
"prompt": "user prompt",
"model": "sonnet|opus",
"session_id": "uuid-for-resume"
}
Response Format:
{
"type": "start|output|completion|error",
"content": "parsed Claude message",
"message": "status message",
"status": "success|error"
}
Message Flow
1. Prompt Submission
Browser → WebSocket Request → Rust Backend → Claude Process
2. Streaming Response
Claude Process → Rust Backend → WebSocket → Browser DOM Events → UI Update
3. Event Chain
- User Input: Prompt submitted via FloatingPromptInput
- WebSocket Send: JSON request sent to
/ws/claude - Process Spawn: Rust spawns
claudesubprocess - Stream Parse: Stdout lines parsed and wrapped in JSON
- Event Dispatch: DOM events fired for
claude-output - UI Update: React components receive and display messages
File Structure
opcode/
├── src-tauri/src/
│ └── web_server.rs # Main web server implementation
├── src/
│ ├── lib/
│ │ └── apiAdapter.ts # WebSocket client & environment detection
│ └── components/
│ ├── ClaudeCodeSession.tsx # Main session component
│ └── claude-code-session/
│ └── useClaudeMessages.ts # Alternative hook implementation
└── justfile # Build configuration (just web)
Build & Deployment
Development
nix-shell --run 'just web'
# Builds frontend and starts Rust server on port 8080
Production Considerations
- Binary Location: Checks bundled binary first, falls back to system PATH
- CORS: Configured for phone browser access
- Error Handling: Comprehensive logging and graceful failures
- Session Cleanup: Proper WebSocket session management
Debugging Features
Comprehensive Tracing
- Backend: All WebSocket events, process spawning, and message forwarding
- Frontend: Event setup, message parsing, and UI updates
- Process: Claude binary execution and output streaming
Debug Output Examples
[TRACE] WebSocket handler started - session_id: uuid
[TRACE] Successfully parsed request: {...}
[TRACE] Claude process spawned successfully
[TRACE] Forwarding message to WebSocket: {...}
[TRACE] DOM event received: claude-output {...}
[TRACE] handleStreamMessage - message type: assistant
Key Fixes Implemented
1. Event Handling Compatibility
Problem: Original code only worked with Tauri events
Solution: Enhanced listen function to support DOM events in web mode
2. Message Format Mismatch
Problem: Backend sent JSON strings, frontend expected parsed objects
Solution: Parse content field in WebSocket handler before dispatching events
3. Process Integration
Problem: Web mode lacked Claude binary execution Solution: Full subprocess spawning with proper argument passing and output streaming
4. Session Management
Problem: No state tracking for multiple concurrent sessions Solution: HashMap-based session tracking with proper cleanup
5. Missing REST Endpoints
Problem: Frontend expected cancel and output endpoints that didn't exist
Solution: Added /api/sessions/{sessionId}/cancel and /api/sessions/{sessionId}/output endpoints
6. Error Event Handling
Problem: WebSocket errors and unexpected closures didn't dispatch UI events
Solution: Added claude-error and claude-complete event dispatching for all error scenarios
Critical Issues Still Remaining
1. Session-Scoped Event Dispatching (CRITICAL)
Problem: The UI expects session-specific events like claude-output:${sessionId} but the backend only dispatches generic events like claude-output.
Current Backend Behavior:
// Only dispatches generic events
window.dispatchEvent(new CustomEvent('claude-output', { detail: claudeMessage }));
window.dispatchEvent(new CustomEvent('claude-complete', { detail: success }));
window.dispatchEvent(new CustomEvent('claude-error', { detail: error }));
Frontend Expectations:
// Expects session-scoped events
await listen(`claude-output:${sessionId}`, handleOutput);
await listen(`claude-error:${sessionId}`, handleError);
await listen(`claude-complete:${sessionId}`, handleComplete);
Impact: Session isolation doesn't work - all sessions receive all events.
2. Process Management and Cancellation (CRITICAL)
Problem: The cancel endpoint is just a stub that doesn't actually terminate running Claude processes.
Current Implementation:
async fn cancel_claude_execution(Path(sessionId): Path<String>) -> Json<ApiResponse<()>> {
// Just logs - doesn't actually cancel anything
println!("[TRACE] Cancel request for session: {}", sessionId);
Json(ApiResponse::success(()))
}
Missing:
- Process tracking and storage in session state
- Actual process termination via
kill()or process handles - Proper cleanup of WebSocket sessions on cancellation
- Session-specific process management
3. Missing stderr Handling (MEDIUM)
Problem: Claude processes can write errors to stderr, but the web server only captures stdout.
Current: Only child.stdout is captured and streamed
Missing: child.stderr capture and claude-error event emission
4. Missing claude-cancelled Events (MEDIUM)
Problem: The Tauri implementation emits claude-cancelled events but the web server doesn't.
Tauri Implementation:
let _ = app.emit(&format!("claude-cancelled:{}", sid), true);
let _ = app.emit("claude-cancelled", true);
Web Server: No claude-cancelled events are dispatched.
5. WebSocket Session ID Mapping (MEDIUM)
Problem: The web server generates its own session IDs but doesn't map them to the frontend's session IDs.
Current: WebSocket handler creates uuid::Uuid::new_v4().to_string() but frontend passes sessionId in request.
Missing: Proper session ID mapping and tracking.
Required Fixes for Full Functionality
Priority 1 (Critical - Breaks Core Functionality)
-
Session-Scoped Event Dispatching
- Modify
apiAdapter.tsto dispatch both generic and session-specific events - Update WebSocket handler to use the frontend's sessionId instead of generating new ones
- Ensure events like
claude-output:${sessionId}are dispatched correctly
- Modify
-
Process Management and Cancellation
- Add process handle storage to AppState
- Implement actual process termination in
cancel_claude_execution - Add proper cleanup on WebSocket disconnection
Priority 2 (High - Improves Reliability)
-
stderr Handling
- Capture both stdout and stderr in Claude process execution
- Emit
claude-errorevents for stderr content - Properly handle process error states
-
claude-cancelled Events
- Add
claude-cancelledevent dispatching for consistency with Tauri - Implement proper cancellation flow matching desktop behavior
- Add
Priority 3 (Medium - Nice to Have)
- Session ID Mapping
- Use frontend-provided sessionId consistently
- Remove UUID generation in WebSocket handler
- Ensure session tracking works correctly
Implementation Notes
Session-Scoped Events Fix
The web server should dispatch both generic and session-specific events to match Tauri:
// Both events should be dispatched
window.dispatchEvent(new CustomEvent('claude-output', { detail: claudeMessage }));
window.dispatchEvent(new CustomEvent(`claude-output:${sessionId}`, { detail: claudeMessage }));
Process Management Fix
The AppState should track process handles:
pub struct AppState {
pub active_sessions: Arc<Mutex<HashMap<String, tokio::sync::mpsc::Sender<String>>>>,
pub active_processes: Arc<Mutex<HashMap<String, tokio::process::Child>>>,
}
Performance Considerations
- Streaming: Real-time output without buffering delays
- Memory: Proper cleanup of completed sessions
- Concurrency: Multiple WebSocket connections supported
- Error Recovery: Graceful handling of process failures
Security Notes
- Binary Execution: Uses
--dangerously-skip-permissionsflag for web mode - CORS: Allows all origins for development (should be restricted in production)
- Process Isolation: Each session runs in separate subprocess
- Input Validation: JSON parsing with error handling
Future Enhancements
- Authentication: Add user authentication for production deployment
- Rate Limiting: Prevent abuse of Claude API calls
- Session Persistence: Save/restore session state across reconnections
- Mobile Optimization: Enhanced UI for mobile browsers
- Error Recovery: Automatic reconnection on WebSocket failures
- Process Monitoring: Add process health checks and automatic restart
- Concurrent Session Limits: Limit number of concurrent Claude processes
- File Management: Add file upload/download capabilities for web mode
- Advanced Logging: Structured logging with log levels and rotation
Testing
Manual Testing
- Start web server:
nix-shell --run 'just web' - Open browser to
http://localhost:8080 - Select project directory
- Send prompt and verify streaming response
- Check browser console for trace output
Debug Tools
- Browser DevTools: WebSocket messages and console logs
- Server Logs: Rust trace output for backend debugging
- Network Tab: REST API calls and WebSocket traffic
Troubleshooting
Common Issues
- No Claude Binary: Check PATH or install Claude Code
- WebSocket Errors: Verify server is running and accessible
- Event Not Received: Check DOM event listeners in browser console
- Process Spawn Failure: Verify project path and permissions
- Session Events Not Working: Check if session-scoped events are being dispatched (critical issue)
- Cancel Button Doesn't Work: Process cancellation not implemented yet (critical issue)
- Multiple Sessions Interfere: Generic events cause cross-session interference
- Errors Not Displayed: stderr not captured, only stdout is shown
Debug Commands
# Check Claude binary
which claude
# Test WebSocket endpoint
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Key: test" -H "Sec-WebSocket-Version: 13" \
http://localhost:8080/ws/claude
# Monitor server logs
tail -f server.log # if logging to file
Current Status
The web server implementation provides basic functionality but has critical issues that prevent full feature parity with the Tauri desktop app:
✅ Working Features
- WebSocket-based Claude execution with streaming output
- Basic session management and tracking
- REST API endpoints for most functionality
- Comprehensive debugging and tracing
- Error handling for WebSocket failures
- Basic process spawning and output capture
❌ Critical Issues (Breaks Core Functionality)
- Session-scoped event dispatching: Sessions interfere with each other
- Process cancellation: Cancel button doesn't actually terminate processes
- stderr handling: Error messages from Claude not displayed
- claude-cancelled events: Missing cancellation event support
⚠️ Current State
The web server is functional for single-session use but not suitable for production due to the session isolation issues. Multiple concurrent sessions will interfere with each other, and users cannot cancel running processes.
🔧 Next Steps
- Fix session-scoped event dispatching (highest priority)
- Implement proper process management and cancellation
- Add stderr capture and error event emission
- Test with multiple concurrent sessions
This implementation successfully bridges the gap between Tauri desktop and web deployment, but requires the above fixes to achieve full feature parity while adapting to browser constraints.