- Project Overview
- Project Structure
- Key Components
- Custom Hooks
- Library Modules
- API Routes
- Data Flow
- Data Models
- Testing
- Development Guide
Remote Claude Code is a mobile-optimized web interface for interacting with the Claude Agent SDK. It enables users to run Claude Code on their laptop and access it from their phone via a local network.
- Framework: Next.js (App Router)
- UI: React 19 + TypeScript + Tailwind CSS
- State Management: Zustand
- Database: SQLite with Drizzle ORM
- Streaming: Server-Sent Events (SSE)
- PWA: Service worker for offline capability
- Real-time streaming responses with live tool execution
- Nested sub-agent tool calls with hierarchical display
- Session persistence and continuation from Claude Code CLI
- Message queueing for rapid input during streaming
- iOS-optimized keyboard handling
- Task progress tracking with TodoWrite integration
- Stream reconnection after page reload or network interruption
- Dark/light theme support
remote-cc/
|-- src/
| |-- app/ # Next.js App Router
| | |-- api/
| | | |-- filesystem/
| | | | +-- route.ts # GET /api/filesystem - Directory listing
| | | |-- sessions/
| | | | |-- route.ts # GET /api/sessions - List sessions
| | | | +-- [id]/
| | | | |-- route.ts # GET /api/sessions/:id - Session details
| | | | |-- message/
| | | | | +-- route.ts # POST /api/sessions/:id/message - Send message
| | | | +-- stream/
| | | | |-- status/
| | | | | +-- route.ts # GET - Check stream status
| | | | +-- reconnect/
| | | | +-- route.ts # GET - Reconnect to active stream
| | | +-- debug/
| | | +-- streams/
| | | +-- route.ts # Debug endpoint for stream inspection
| | |-- sessions/
| | | |-- [id]/
| | | | +-- page.tsx # Session chat view
| | | +-- new/
| | | +-- page.tsx # New session page
| | |-- settings/
| | | +-- page.tsx # Settings page
| | |-- globals.css # Global styles with Tailwind
| | |-- layout.tsx # Root layout with providers
| | +-- page.tsx # Home page (redirects to new chat)
| |
| |-- components/ # React components
| | |-- AppShell.tsx # Layout wrapper with sidebar
| | |-- ChatInterface.tsx # Main chat orchestrator
| | |-- ChatMessage.tsx # Message rendering with markdown
| | |-- CodeBlock.tsx # Syntax-highlighted code blocks
| | |-- DirectoryPicker.tsx # File system navigation
| | |-- MobileNav.tsx # Mobile navigation component
| | |-- ServiceWorkerRegistration.tsx # PWA service worker setup
| | |-- Sidebar.tsx # Session navigation
| | |-- ThemeMetaTag.tsx # Dynamic theme-color meta tag
| | |-- ThinkingIndicator.tsx # Streaming status with stats
| | |-- TodoList.tsx # Task progress display
| | +-- ToolCallDisplay.tsx # Tool call visualization
| |
| |-- hooks/ # Custom React hooks
| | |-- useChat.ts # SSE streaming and state management
| | +-- useNotifications.ts # Browser notifications and haptics
| |
| |-- lib/ # Core business logic
| | |-- __mocks__/
| | | +-- claude.ts # Mock for testing
| | |-- claude.ts # Claude SDK wrapper
| | |-- db.ts # Database connection
| | |-- sessions.ts # Session file parsing
| | |-- store.ts # Zustand state management
| | |-- streamRegistry.ts # SSE stream management
| | |-- theme.tsx # Theme context provider
| | +-- utils.ts # Utility functions
| |
| +-- __tests__/ # Test files
| |-- api/ # API route tests
| |-- components/ # Component tests
| |-- hooks/ # Hook tests
| +-- lib/ # Library tests
|
|-- db/
| |-- schema.ts # Drizzle ORM schema
| |-- migrations/ # Database migrations
| +-- sessions.db # SQLite database file
|
|-- public/
| |-- icons/ # PWA icons (various sizes)
| |-- manifest.json # PWA manifest
| |-- sw.js # Service worker
| +-- theme-init.js # Theme initialization script
|
|-- scripts/
| +-- generate-icons.js # Icon generation utility
|
+-- drizzle.config.ts # Drizzle ORM configuration
Location: /src/components/ChatInterface.tsx
The main chat orchestrator and UI container. Manages the entire chat experience including message display, input handling, and streaming state.
Key Features:
- Message display with intelligent auto-scroll (respects user scroll position)
- Message queueing during streaming to allow rapid input
- iOS keyboard height detection using
visualViewportAPI - Safe area insets for notched devices
- Reconnection banner for interrupted streams
Important State:
const [messageQueue, setMessageQueue] = useState<QueuedMessage[]>([]);
const [workingDir, setWorkingDir] = useState(initialWorkingDir || "");
const [keyboardHeight, setKeyboardHeight] = useState(0);Key Behaviors:
- Queues messages when
isStreamingis true; processes queue when streaming completes - Detects user scroll-up to prevent forced scrolling during streaming
- Provides vibration and audio feedback on stream completion
- Filters out TodoWrite tool calls from display (shown separately in TodoList)
Location: /src/components/Sidebar.tsx
Session navigation with starring and project grouping capabilities.
Key Features:
- Sessions grouped by project name
- Star/unstar sessions (persisted to localStorage)
- Collapsible project groups (state persisted to localStorage)
- Starred sessions displayed at top in dedicated section
- New chat button
- Theme toggle
- Settings link
Custom Hooks Used:
useStarredSessions()- Manages starred session IDs in localStorageuseCollapsedGroups()- Manages collapsed project group state
Key Components:
SessionItem- Individual session entry with star toggleProjectGroup- Collapsible group of sessions by project
Location: /src/components/ChatMessage.tsx
Message rendering with full markdown support.
Key Features:
- User messages: Right-aligned blue bubbles
- Assistant messages: Full-width with markdown parsing
- Special handling for "Insight" blocks (styled callouts)
- Session continuation messages (context compaction summaries)
- Memoized components to prevent unnecessary re-renders
Exported Components:
ChatMessage- Historical message displayStreamingMessage- Live streaming message with cursorSessionContinuationMessage- Expandable compacted context displayMarkdownContent- Markdown parser and rendererisSessionContinuationMessage()- Detection helper function
Markdown Support:
- Headers (h1, h2, h3)
- Bold, italic, inline code
- Links (open in new tab)
- Bulleted and numbered lists
- Horizontal rules
- Code blocks with syntax highlighting
Location: /src/components/ToolCallDisplay.tsx
Visualizes tool execution with categorized display patterns.
Tool Categories:
-
Inline Tools (Read, Glob, Grep, WebSearch)
- Non-expandable, single-line display
- Shows:
ToolName: parameter
-
Inline with Output (Bash)
- Shows command inline
- Expandable to show output (auto-scrolls)
-
Expandable Tools (Edit, Write, etc.)
- Shows input/output when expanded
- Edit tool displays visual diff
-
Task Tools (Sub-Agents)
- Purple color scheme
- Shows nested child tool calls
- Expandable "Agent Details" section
- Displays sub-agent prompt and final response
Key Components:
ToolCallDisplay- Routes to appropriate display based on tool nameTaskToolDisplay- Specialized display for Task (sub-agent) toolsToolCallList- Renders list of tool callsDiffView- Visual diff for Edit tool
Status Colors:
- Running: Yellow/amber
- Complete: Green
- Error: Red
- Task running: Purple
Location: /src/components/TodoList.tsx
Displays task progress from TodoWrite tool calls.
Features:
- Collapsible task list with progress counter
- Shows current in-progress task when collapsed
- Status indicators (spinner for in-progress, checkmark for completed)
- Auto-hides when all tasks completed
Todo Interface:
interface Todo {
content: string; // Task description (imperative)
status: "pending" | "in_progress" | "completed";
activeForm: string; // Present tense form shown during execution
}Location: /src/components/ThinkingIndicator.tsx
Shows streaming status with statistics.
Displayed Information:
- Animated "Thinking..." indicator during streaming
- "Done" checkmark when complete
- Elapsed time (minutes:seconds)
- Token count (input + output breakdown)
State Management:
- Captures final stats when streaming completes
- Resets on new stream start
- Updates elapsed time every second during streaming
Location: /src/components/DirectoryPicker.tsx
File system navigation for selecting working directories.
Features:
- Breadcrumb navigation
- Quick nav buttons (Home, Up, Type Path)
- Manual path input with keyboard support
- Directory listing with click-to-navigate
- Select button to confirm choice
Location: /src/components/AppShell.tsx
Layout wrapper that provides the sidebar and main content area.
Features:
- Responsive sidebar (persistent on large screens, sliding on mobile)
- Escape key closes sidebar
- Handles safe area insets
Location: /src/hooks/useChat.ts
SSE stream parser and state manager. The core hook for chat functionality.
Responsibilities:
- Parse Server-Sent Events from the API
- Aggregate text blocks during streaming
- Track tool call status transitions
- Manage nested tool calls (Task/sub-agents via
parent_tool_use_id) - Extract and update TodoWrite data
- Support stream reconnection after page reload
Key State:
const [streamBlocks, setStreamBlocks] = useState<StreamBlock[]>([]);
const [todos, setTodos] = useState<Todo[]>([]);
const [streamingStats, setStreamingStats] = useState<StreamingStats>({
startTime: null,
inputTokens: 0,
outputTokens: 0,
});
const [isReconnecting, setIsReconnecting] = useState(false);Returned Values:
{
sendMessage, // (prompt, workingDirectory?) => void
stopGeneration, // () => void - Abort current stream
streamBlocks, // Current streaming content
todos, // Current task list
currentSessionId, // Session ID (may update mid-stream for new sessions)
streamingStats, // Time and token counts
isReconnecting, // True during reconnection
reconnectProgress, // { total, replayed } during replay
checkAndReconnect, // Manual reconnect trigger
}Stream Message Handling:
systemwithsubtype: "init": Captures session IDassistant: Processes text and tool_use blocksuserwithtool_result: Marks tools complete (especially sub-agents)tool_result: SDK format for tool completiondone: Final cleanup and session refresh
Nested Tool Call Logic:
- Uses
parent_tool_use_idfrom messages to identify sub-agent tools - Finds parent Task by toolUseId and appends to
childToolCalls - Falls back to top-level if parent not found
Location: /src/hooks/useNotifications.ts
Browser notifications and haptic feedback.
Features:
- Browser notification support (via service worker)
- Vibration API support (Android)
- Web Audio API beep (iOS fallback)
- Combined
notifyComplete()for stream completion
Returned Values:
{
permission, // NotificationPermission state
isSupported, // Browser support check
requestPermission, // Request notification permission
showNotification, // Send notification via service worker
vibrate, // Trigger vibration pattern
playSound, // Play audio beep
notifyComplete, // Combined notification on completion
}Location: /src/lib/store.ts
Zustand state management for global application state.
State Shape:
interface SessionsState {
sessions: Session[];
currentSession: Session | null;
messages: Message[];
isLoading: boolean;
isStreaming: boolean;
error: string | null;
}Actions:
setSessions,setCurrentSession,setMessages,addMessagesetLoading,setStreaming,setErrorclearForNewChat()- Reset for new chatfetchSessions()- Load session list from APIfetchSession(id)- Load single session with messages
Design Pattern:
- Optimistic UI: Add temp messages immediately
- Replaced by real data after
fetchSessioncompletes
Location: /src/lib/sessions.ts
Session file parsing from Claude Code CLI data.
Key Functions:
listSessions()- List all sessions from~/.claude/projects/getSessionMessages(sessionId, filePath?)- Parse JSONL session filegetSessionProjectPath(sessionId)- Get working directory for sessionfindSessionFilePath(sessionId)- Direct file lookup without listing allinvalidateSessionCache()- Clear cached session list
Caching:
- Session list cached with 5-second TTL
- Metadata cache (cwd, firstMessage) persisted by file mtime
- Uses
globalThisfor cache persistence across Next.js module re-evaluation
Session File Format:
- JSONL files in
~/.claude/projects/{encoded-path}/{session-id}.jsonl - Each line is a JSON message with type, content, timestamp, etc.
Location: /src/lib/streamRegistry.ts
Server-side module for tracking active streams and enabling reconnection.
Key Functions:
createStream(sessionId)- Initialize new stream bufferaddMessage(sessionId, message)- Buffer message and notify subscriberscompleteStream(sessionId, error?)- Mark stream as donegetStream(sessionId)- Get stream stategetMessagesAfter(sessionId, sequence)- Get missed messages for replaysubscribe(sessionId, callback)- Subscribe to live updatesmigrateStream(tempId, realId)- Update stream key when session ID assigneddeleteStream(sessionId)- Remove stream after client catches up
Configuration:
MAX_BUFFER_SIZE: 1000 messagesTTL_RUNNING: 1 hourTTL_COMPLETED: 30 minutesCLEANUP_INTERVAL: 5 minutes
Stream State:
interface ActiveStream {
sessionId: string;
status: "running" | "completed" | "error";
startedAt: number;
completedAt?: number;
buffer: BufferedMessage[];
lastSequence: number;
subscribers: Set<callback>;
error?: string;
}Location: /src/lib/theme.tsx
Dark/light theme provider using React Context.
Features:
- Persists theme choice to localStorage
- Manages
darkclass on document element - Provides
toggleTheme()function - Safe defaults for SSR
Location: /src/lib/claude.ts
Claude Agent SDK wrapper.
Key Function:
async function* runClaudeQuery(prompt, options): AsyncGenerator<SDKMessage>Options:
interface ClaudeQueryOptions {
sessionId?: string; // Resume existing session
workingDir?: string; // Working directory (default: cwd)
model?: string; // Model (default: claude-sonnet-4-5)
maxBudgetUsd?: number; // Budget limit (default: 10)
}Allowed Tools:
- Read, Edit, Write, Bash
- Glob, Grep
- WebSearch, WebFetch
- Task (sub-agents)
- TodoWrite (task tracking)
Permission Mode: acceptEdits
Location: /src/lib/db.ts
Database connection using better-sqlite3 and Drizzle ORM.
Features:
- Lazy initialization via Proxy
- WAL mode for better concurrent access
- Path:
db/sessions.db
Schema (in /db/schema.ts):
sessions- Session metadata (id, title, workingDirectory, timestamps, cost, status)messages- Message history (sessionId, role, content, tool data, timestamp)sessionMetadata- Extended session info (model, token counts, errors)
Location: /src/app/api/sessions/route.ts
Lists all available sessions from ~/.claude/projects/.
Query Parameters:
limit(default: 50) - Number of sessions to returnoffset(default: 0) - Pagination offset
Response:
{
"sessions": [
{
"id": "session_uuid",
"title": "First user message...",
"projectPath": "/path/to/project",
"projectName": "project-name",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z"
}
],
"total": 10
}Location: /src/app/api/sessions/[id]/route.ts
Get session details and message history.
Response:
{
"session": {
"id": "session_uuid",
"title": "Session title",
"projectPath": "/path/to/project",
"projectName": "project-name",
"workingDirectory": "/path/to/project",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z"
},
"messages": [
{
"id": "msg_uuid",
"sessionId": "session_uuid",
"role": "user",
"content": "Hello",
"timestamp": "2024-01-01T00:00:00Z"
},
{
"id": "msg_uuid",
"sessionId": "session_uuid",
"role": "assistant",
"content": "Hi there!",
"toolCalls": [...],
"timestamp": "2024-01-01T00:00:01Z"
}
]
}Location: /src/app/api/sessions/[id]/message/route.ts
Send a message and stream the response.
Request Body:
{
"prompt": "User message text",
"workingDirectory": "/path/to/project"
}Response: Server-Sent Events stream
SSE Message Types:
data: {"type":"system","subtype":"init","session_id":"session_123","sequence":1}
data: {"type":"assistant","message":{"content":[{"type":"text","text":"Hello"}]},"sequence":2}
data: {"type":"assistant","message":{"content":[{"type":"tool_use","name":"Read","input":{...},"id":"tool_123"}]},"sequence":3}
data: {"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tool_123","content":"..."}]},"sequence":4}
data: {"type":"done","sessionId":"session_123","sequence":5}
Location: /src/app/api/sessions/[id]/stream/status/route.ts
Check if a session has an active stream.
Response:
{
"active": true,
"status": "running",
"lastSequence": 42,
"startedAt": 1704067200000,
"bufferedMessages": 42
}Location: /src/app/api/sessions/[id]/stream/reconnect/route.ts
Reconnect to an active or recently completed stream.
Query Parameters:
lastSequence- Last received sequence number
Response: Server-Sent Events stream with:
replay_start- Indicates start of missed messages- Missed messages (with sequence numbers)
replay_end- Indicates end of replay- Live messages (if stream still running)
reconnect_done- Final status
Location: /src/app/api/filesystem/route.ts
List directories for the directory picker.
Query Parameters:
path- Directory path (default: home directory, supports~)
Response:
{
"currentPath": "/Users/user/projects",
"parentPath": "/Users/user",
"directories": [
{ "name": "project-a", "path": "/Users/user/projects/project-a" }
],
"homePath": "/Users/user"
}1. User types message in ChatInterface
|
2. handleSubmit() called
|
+-- If streaming: Queue message for later
|
+-- If not streaming:
|
v
3. sendMessage() in useChat hook
|
+-- Add optimistic user message to store
|
+-- POST to /api/sessions/[id]/message
|
v
4. API route:
|
+-- Create stream in streamRegistry
|
+-- Call runClaudeQuery() from claude.ts
|
+-- For each SDK message:
|
+-- Add to buffer with sequence number
|
+-- Stream to client via SSE
|
v
5. useChat hook parses SSE:
|
+-- "system" init: Capture session ID
|
+-- "assistant" text: Append to currentTextBlock
|
+-- "assistant" tool_use: Create tool call entry
|
+-- "user" tool_result: Mark tool complete
|
+-- "done": Fetch fresh session data
|
v
6. ChatInterface renders:
|
+-- Historical messages from store
|
+-- Streaming content from streamBlocks
|
+-- Tool calls from ToolCallDisplay
|
+-- Todos from TodoList
1. Page reload or network interruption
|
v
2. useChat mounts, calls checkAndReconnect()
|
+-- GET /api/sessions/[id]/stream/status
|
+-- If active or has missed messages:
|
v
3. reconnectToStream()
|
+-- GET /api/sessions/[id]/stream/reconnect?lastSequence=N
|
v
4. Server streams:
|
+-- replay_start
+-- Missed messages from buffer
+-- replay_end
+-- Live messages (if still running)
+-- reconnect_done
|
v
5. useChat processes messages normally
|
+-- Updates reconnectProgress during replay
+-- Clears isReconnecting after replay_end
interface Session {
id: string;
title?: string;
firstMessage?: string;
projectPath?: string;
projectName?: string;
workingDirectory?: string;
createdAt: string;
updatedAt: string;
}interface Message {
id: string;
sessionId: string;
role: "user" | "assistant" | "system";
content: string;
toolCalls?: ToolCall[];
timestamp: string;
}interface ToolCall {
name: string;
input: unknown;
output?: unknown;
status: "running" | "complete" | "error";
toolUseId?: string;
childToolCalls?: ToolCall[]; // For Task (sub-agent) tools
}interface StreamBlock {
type: "text" | "tool";
content?: string; // For text blocks
toolCall?: ToolCallData; // For tool blocks
}interface Todo {
content: string; // Imperative form: "Run tests"
status: "pending" | "in_progress" | "completed";
activeForm: string; // Present tense: "Running tests"
}sessions table:
id(text, PK) - Claude SDK session_idtitle(text)working_directory(text)created_at(integer, timestamp)updated_at(integer, timestamp)total_cost_usd(real)status(text) - "active" | "archived"
messages table:
id(integer, auto-increment, PK)session_id(text, FK to sessions)role(text) - user, assistant, system, toolcontent(text) - JSON for complex contenttool_name(text)tool_input(text) - JSONtool_output(text) - JSONtimestamp(integer, timestamp)
session_metadata table:
session_id(text, PK, FK to sessions)model(text)total_input_tokens(integer)total_output_tokens(integer)last_error(text)
Tests are organized in /src/__tests__/ mirroring the source structure:
src/__tests__/
|-- api/
| |-- filesystem.test.ts
| |-- message.test.ts
| |-- sessions.test.ts
| |-- sessions-id.test.ts
| |-- stream-reconnect.test.ts
| +-- stream-status.test.ts
|
|-- components/
| |-- AppShell.test.tsx
| |-- ChatInterface.test.tsx
| |-- ChatMessage.test.tsx
| |-- CodeBlock.test.tsx
| |-- DirectoryPicker.test.tsx
| |-- MobileNav.test.tsx
| |-- Sidebar.test.tsx
| |-- ThinkingIndicator.test.tsx
| |-- TodoList.test.tsx
| +-- ToolCallDisplay.test.tsx
|
|-- hooks/
| |-- useChat.test.ts
| +-- useNotifications.test.ts
|
+-- lib/
|-- sessions.test.ts
|-- store.test.ts
|-- streamRegistry.test.ts
|-- theme.test.tsx
+-- utils.test.ts
# Run all tests
npm test
# Run tests in watch mode
npm test -- --watch
# Run specific test file
npm test -- src/__tests__/hooks/useChat.test.ts
# Run with coverage
npm test -- --coverage/src/lib/__mocks__/claude.ts- Mock for Claude SDK in tests
npm install
npm run devnpm run build
npm start# Find your local IP
ifconfig | grep "inet "
# Start on all interfaces
npm run dev -- -H 0.0.0.0
# Access from phone: http://YOUR_IP:3000# View database
sqlite3 db/sessions.db
# Run migrations
npm run db:push
# Generate migrations
npm run db:generateAdd new tool display type:
- Update
INLINE_TOOLSorINLINE_WITH_OUTPUT_TOOLSinToolCallDisplay.tsx - Add case to
getInlineDisplay()function - Optionally create specialized display component
Change streaming behavior:
- Modify
handleStreamMessageRefinuseChat.ts - Update message type handling in switch statement
Add new API endpoint:
- Create route file in
/src/app/api/ - Export GET/POST handler functions
- Update types in
/src/lib/store.tsif needed
Modify UI layout:
- Edit
ChatInterface.tsxfor chat layout - Update
AppShell.tsxfor shell structure - Modify
globals.cssfor theme colors
- No authentication: Designed for trusted local networks only
- File system access: Backend can read/write files in working directory
- Tool permissions: "acceptEdits" mode allows automatic file modifications
- Budget limits: Configured $10 max spend per query
- Open app in mobile browser
- Add to Home Screen
- App installs as standalone PWA
- Service worker enables offline capability
Streaming stops working:
- Check SSE connection in Network tab
- Verify server logs for errors
- Check if stream timed out (1 hour TTL)
Messages not persisting:
- Verify session files exist in
~/.claude/projects/ - Check database connection
- Ensure write permissions on session directory
Tools not displaying:
- Check tool name matches allowed tools list
- Verify tool call format in stream messages
- Check console for parsing errors
iOS keyboard issues:
- Ensure viewport meta tag is set
- Check
visualViewportAPI support - Verify safe area insets in CSS
Reconnection not working:
- Check if stream is still in registry (30-min TTL after completion)
- Verify lastSequence is being stored in sessionStorage
- Check server logs for reconnect endpoint errors
Last Updated: January 2025