Skip to content

Commit afaf80f

Browse files
committed
🤖 feat: Add React Native mobile app with Expo
🤖 feat: Add React Native mobile app with Expo - Create apps/mobile directory with Expo Router setup - Implement Projects screen with workspace list - Implement Workspace screen with chat interface - Add WebSocket-based real-time chat synchronization - Support Plan/Exec mode toggles and Reasoning level control - Add theming system with colors, typography, spacing - Implement message rendering with proper type handling - Add server-mode support to main process for mobile connectivity - Include auth token support via query parameters _Generated with cmux_ Change-Id: I6597d24921e21a6d6670807a237b8b9b603d929c Signed-off-by: Test <test@example.com> 🤖 refactor: Move mode & reasoning controls to Settings in mobile app - Create useWorkspaceDefaults hook for persistent mode and reasoning settings - Add Workspace Defaults section to Settings screen with: - Default Execution Mode toggle (Plan/Exec) - Default Reasoning Level slider (Off/Low/Medium/High) - Remove mode toggles and ReasoningControl from WorkspaceScreen header - Add settings icon to workspace header for easy access - Initialize workspace with defaults from settings Benefits: - Frees ~140dp of vertical space for chat messages - Follows mobile UX pattern of infrequent config in Settings - Messages now start at 100dp from top instead of 240dp - Settings persist across all workspaces via SecureStore Net change: +116 LoC (hook), +180 LoC (settings), -70 LoC (workspace) _Generated with cmux_ Change-Id: I475804c7ad564829b2658b08718b7b7aea5a5265 Signed-off-by: Test <test@example.com> 🤖 fix: Sanitize workspace IDs for SecureStore key compatibility - Add sanitizeWorkspaceId() to replace invalid characters with underscores - SecureStore keys must only contain alphanumeric, '.', '-', and '_' - Change key delimiter from ':' to '.' for consistency - Remove unused ThinkingProvider/useThinkingLevel from WorkspaceScreen (reasoning level now comes from useWorkspaceDefaults) Fixes: Failed to read thinking level [Error: Invalid key provided to SecureStore] _Generated with cmux_ Change-Id: I5365806bfddc9950d14ca09278a762403f143bab Signed-off-by: Test <test@example.com> 🤖 refactor: Redesign workspace screen for messaging app UX - Simplify header to show 'project › workspace' instead of multi-line layout - Remove Surface containers around chat and input areas for full-width display - Style input like iMessage/WhatsApp: - Rounded pill input (borderRadius: 20) - Circular send button with arrow-up icon - Button disabled when input is empty (grayed out) - Compact sizing (38x38 button, minHeight: 38 input) - Add proper borders and backgrounds for visual separation - Save additional ~50dp of vertical space from simplified header Result: Clean messaging app feel with maximized screen real estate for chat. _Generated with cmux_ Change-Id: I58a979b4117c0ccb176cf250dd80d024387bbb20 Signed-off-by: Test <test@example.com> 🤖 fix: Add surfaceSecondary color and remove Surface component usage - Add surfaceSecondary color to theme for header/footer backgrounds - Replace Surface component with View in TimelineRow fallback - Fixes TypeScript errors from removed Surface import _Generated with cmux_ Change-Id: I39bce5ff5f2125c52e3f90371bfac45b8df0fd7e Signed-off-by: Test <test@example.com> 🤖 fix: Add safe area padding for iOS home indicator - Import and use useSafeAreaInsets from react-native-safe-area-context - Apply bottom inset to input area: paddingBottom = max(spacing.sm, insets.bottom) - Prevents send button from colliding with iOS swipe-up bar - Ensures minimum padding of spacing.sm on devices without notch _Generated with cmux_ Change-Id: Icc613ae56f3c86e7f9ae050007110b8161db3fce Signed-off-by: Test <test@example.com> 🤖 feat: Auto-scroll to latest message when entering chat - Add FlatList ref for programmatic scrolling - Scroll to bottom on initial load when timeline has messages - Auto-scroll when new messages arrive (onContentSizeChange) - Use 100ms delay on mount to ensure layout is complete - Animated scroll for new messages, instant scroll on initial load Result: Chat always starts at the latest message, not showing entire history. _Generated with cmux_ Change-Id: I4df172ca29ebb345ce84d16d6981fb369d8d8c4e Signed-off-by: Test <test@example.com> 🤖 feat: Enable real-time streaming in React Native mobile app Implement full streaming with real-time message updates (Option 1): **Message Streaming:** - Emit partial messages on every stream-delta, reasoning-delta, tool-call events - Track active streams for real-time emission - Mark messages as isStreaming:true during streaming - Update messages in-place using upsert logic (replace by ID) - Show complete messages with isStreaming:false on stream-end **Streaming UI Indicators:** - Add animated pulsing cursor to assistant messages while streaming - Add streaming cursor to reasoning messages - Cursor animation: fade between opacity 1.0 ↔ 0.3 every 530ms **Auto-Scroll Improvements:** - Initial load: Jump instantly to last message (no animation) - During chat: Animated scroll to bottom (iMessage/WhatsApp style) - Track hasInitiallyScrolled to control animation behavior - Reset scroll tracking when switching workspaces **Architecture Changes:** - normalizeChatEvent.ts: Emit on every delta instead of buffering until stream-end - applyChatEvent: Upsert logic checks for existing message by ID and replaces - MessageRenderer: Animated streaming cursor component with useNativeDriver Result: True real-time streaming like desktop app. Messages appear incrementally as tokens arrive, reasoning streams in real-time, smooth UX with proper animations. Net change: +120 LoC (streaming logic + cursor component + auto-scroll fixes) _Generated with cmux_ Change-Id: I5beac5c9e9105f02ef62029f43256565edd0db6c Signed-off-by: Test <test@example.com> 🤖 fix: Make initial scroll instant when entering workspace - Replace setTimeout(100ms) with requestAnimationFrame for instant scroll - Change useEffect dependency from timeline.length to timeline.length > 0 (only triggers once when messages first arrive, not on every update) - Clear timeline when workspace changes for fresh start - Ensures no visible animation when opening chat Result: Chat jumps directly to bottom instantly, no delay or animation on entry. _Generated with cmux_ Change-Id: Icf2116a1c24f88a0c660d3a92c0723db1b7b5675 Signed-off-by: Test <test@example.com> 🤖 fix: Make scroll instant and fix Metro module resolution **Instant Scroll Fix:** - Replace scrollToEnd with scrollToIndex for true instant jump - scrollToEnd({ animated: false }) still uses default animation duration - scrollToIndex with animated:false is immediate with no transition - Use viewPosition:1 to position item at bottom of viewport **Metro Resolution Fix:** - Add watchFolders to include parent src directory - Configure extraNodeModules to resolve @shared alias - Fixes: Could not resolve @shared/types/message, @shared/types/toolParts - Allows mobile app to import shared code from parent src/ Result: Opening workspace jumps instantly to bottom. No slow scroll animation. Expo bundler can now resolve parent directory imports. _Generated with cmux_ Change-Id: If5850c71b2714ab7eb3b583fecdd304ddcfff62f Signed-off-by: Test <test@example.com> 🤖 fix: Use inverted FlatList for instant chat scrolling Replace manual scroll logic with FlatList inverted prop (standard for chat apps): **Why inverted:** - FlatList with inverted prop naturally starts at bottom (newest messages) - No need for scrollToEnd() calls or animation management - Instantly shows latest messages on mount (0ms, no scroll animation) - New messages automatically appear at bottom without manual scroll - Standard pattern used by WhatsApp, Slack, Stream Chat, etc. **Changes:** - Add inverted prop to FlatList - Reverse timeline data array for proper ordering (oldest→newest becomes newest→oldest) - Remove all scroll tracking code (hasInitiallyScrolledRef) - Remove scroll effects (initial scroll + onContentSizeChange) - Simplify workspace change effect (just clear timeline) Result: Opening workspace shows latest message instantly. No visible scroll animation or delay. New messages appear at bottom automatically. Net change: -35 LoC (removed complex scroll management) _Generated with cmux_ Change-Id: I4322d061980c55b0fd79e038e80a79c4234d060f Signed-off-by: Test <test@example.com> 🤖 fix: Handle sendMessage response and add dismiss button to errors **SendMessage Response Fix:** - Server returns Result type directly: { success: true } or { success: false, error } - Mobile client was not checking the inner Result, treating all responses as success - Now properly checks result.success and returns error when false - Fixes: "Request failed" appearing even when message sends successfully **Dismissible Error Messages:** - Add dismiss button (X) to error messages in chat - Errors are displayed as "raw" timeline entries - Add onDismiss callback to RawEventCard component - Add handleDismissRawEvent to filter out dismissed errors by key - Pass dismiss handler only for raw events (not displayed messages) - hitSlop={8} for easy tapping on small X button Result: No more false "Request failed" errors. Error messages can be dismissed. _Generated with cmux_ Change-Id: I97d2f350d6a9c70d58b258fb9336638007527c3c Signed-off-by: Test <test@example.com> 🤖 feat: Add special propose_plan tool rendering for mobile Implement mobile-optimized plan card matching desktop UX: **New Component: ProposePlanCard (~180 LoC)** - Purple-themed card with left border (plan mode color #8b5cf6) - Title with 📋 emoji displayed prominently - Markdown-rendered plan content in scrollable view (max 400dp) - Action buttons: Copy to clipboard, Show Text/Markdown toggle - Copy uses expo-clipboard with 2s feedback ("✓ Copied") - Footer hint when completed: "Ask to implement in Exec mode" - Mobile-optimized: Pressable buttons, larger tap targets, scrollable **MessageRenderer Dispatch Logic (~25 LoC)** - Add isProposePlanTool() type guard with proper type narrowing - Dispatch to ProposePlanCard when tool is propose_plan - Fall back to generic ToolMessageCard for other tools - Import ProposePlanCard component **Dependencies:** - Add expo-clipboard for copy-to-clipboard functionality **Differences from Desktop:** - Simplified styling (no complex gradients, just purple tint + border) - No "Start Here" button (skip complex modal for v1) - Touch-first interactions (no hover states) - Scrollable content area for long plans Result: Plans now render beautifully on mobile with readable markdown, actionable buttons, and clear visual distinction from other tools. Net change: +205 LoC (new component + dispatch + dependency) _Generated with cmux_ Change-Id: Iaf7a2a5c14af6962cb3a53dc665ef2077b49ad92 Signed-off-by: Test <test@example.com> 🤖 feat: Add todo list rendering to React Native mobile app Implement comprehensive todo list UX matching desktop functionality: **New Components:** 1. TodoItemView.tsx (~95 LoC) - Shared todo item renderer - Status icons: ✓ (completed), ⟳ (in_progress), ○ (pending) - Color-coded by status (green/blue/gray) - Left border matching status color - Strikethrough for completed items - Reusable by both live and historical displays 2. FloatingTodoCard.tsx (~95 LoC) - Live progress indicator - Appears above input during streaming - Compact header: "📋 TODO (2/5)" shows progress - Collapsible (tap chevron to expand/collapse) - Dismissible (X button to hide) - Scrollable list (max 150dp height) - Auto-disappears when stream ends - Re-appears when new todos arrive 3. TodoToolCard.tsx (~80 LoC) - Historical tool call display - Shows past todo_write calls in chat - Collapsed by default (expandable) - Shows completion progress - Status badge (✓ Completed, etc.) - Uses TodoItemView for consistent styling **Event Tracking (~40 LoC):** - Listen for tool-call-end events (todo_write) - Extract todos from event args - Update currentTodos state - Clear on stream-end - Track dismiss state per workspace **MessageRenderer Dispatch (~30 LoC):** - Add isTodoWriteTool() type guard - Route todo_write to TodoToolCard - Import and integrate components **Architecture:** - Event-based tracking (no backend changes needed) - Real-time updates from WebSocket - Auto-clears when stream ends - Matches desktop behavior (live + historical) **UX Benefits:** - Live progress visible during streaming - Clean, compact mobile-optimized design - User control (collapse/dismiss) - Historical record in chat - Color-coded status at a glance Result: Todo lists now render beautifully on mobile with live progress indicator and historical tool call display. Full feature parity with desktop. Net change: +340 LoC (3 new components + integration) _Generated with cmux_ Change-Id: Ia6855bad58eb2ef91f7d6513ef7d7787b7621604 Signed-off-by: Test <test@example.com> 🤖 fix: Remove back button text, show only arrow (<) Set headerBackTitle: '' for workspace and settings screens. Removes 'index' label from back button, shows only < arrow. Cleaner, more minimal navigation matching iOS standards. _Generated with cmux_ Change-Id: I6e8d2b67e526b0a782aaefc1c48e9df1155f3f8f Signed-off-by: Test <test@example.com> 🤖 fix: Add safe area insets to header and restore back button **Safe Area Insets:** - Add paddingTop: Math.max(spacing.sm, insets.top) to custom header - Prevents clash with iOS status bar (time, battery, etc.) - Uses useSafeAreaInsets() already available in component **Back Button:** - Add chevron-back icon button to header - Uses router.back() to navigate to projects list - Position: Left side of header before workspace title - hitSlop={8} for easy tapping - Icon size 28 for prominence Result: Header respects iOS status bar and has intuitive back navigation. _Generated with cmux_ Change-Id: I3943bf4fd53dfb2603a0703bbc278f8444340c02 Signed-off-by: Test <test@example.com> 🤖 fix: Use Expo Router header with proper back navigation Revert custom header approach and use Expo Router's built-in navigation: **Changes:** - Re-enable headerShown for workspace screen (use Expo Router header) - Set title: 'Workspace' for proper back button reference - Set headerBackTitle: '' on both workspace and settings (just < arrow) - Remove custom back button from WorkspaceScreen (use router's) - Remove safe area inset handling (router header handles it) **Result:** - Settings back button now shows '< Workspace' or just '<' (not '< workspace/[id]') - Expo Router manages navigation context properly - iOS safe area handled automatically by router - Consistent navigation UX Note: Custom workspace info bar (project › workspace + icons) is still below the router header. This is intentional - router header provides navigation, custom bar provides workspace context and actions. _Generated with cmux_ Change-Id: I39902665987e4fb8f2bf0fd5f47f907bf84bf0e5 Signed-off-by: Test <test@example.com> 🤖 fix: Set index screen title to 'Workspaces' for proper back button Change index screen from headerShown:false to title:'Workspaces'. This ensures Settings back button shows '<' (or '< Workspaces') instead of '< index'. Now Expo Router has proper screen titles for navigation context. _Generated with cmux_ Change-Id: I7ab9e14eb4c364c5643d73049e43843fce9e12e5 Signed-off-by: Test <test@example.com> 🤖 fix: Hide header on Workspaces page to reduce offset Set headerShown: false for index screen. ProjectsScreen has its own "Projects" label that should be at the top. Removes duplicate header that was pushing content down. Result: Projects label appears higher up, better use of screen space. _Generated with cmux_ Change-Id: I7aca5cda675edead021138b9d3945d6cbd25fdd6 Signed-off-by: Test <test@example.com> 🤖 fix: Keep Workspaces header and reduce Projects page top padding **Changes:** - Restore headerShown for index screen with title: 'Workspaces' - Reduce ProjectsScreen paddingTop from 'insets.top + spacing.lg' to 'spacing.md' - Expo Router header already handles safe area, no need for duplicate insets **Result:** - Workspaces header is visible with proper title - Projects label appears higher up (less offset) - Back buttons work correctly (Workspaces context) _Generated with cmux_ Change-Id: I265861ea7ccc2d8b39676f03cf85174804df8b6a Signed-off-by: Test <test@example.com> 🤖 refactor: Move workspace name to Expo Router header **Changes:** - Set router header title dynamically to 'project › workspace' using navigation.setOptions() - Remove workspace name from custom header bar - Keep only action icons in custom bar (terminal, secrets, settings) - Align icons to the right (justifyContent: flex-end) **Result:** - Expo Router header shows: 'cmux › react-native' - Custom bar shows only: 🖥️ 🔑 ⚙️ (right-aligned) - No duplication of workspace name - Cleaner, more organized layout _Generated with cmux_ Change-Id: I00082dc5ce9056a586017657c23e41e00a320e6b Signed-off-by: Test <test@example.com> 🤖 fix: Remove static workspace title to prevent flicker Set title: '' for workspace screen in router config. WorkspaceScreen sets the title dynamically via navigation.setOptions(). Prevents flicker from 'Workspace' → 'cmux › react-native'. Now shows empty title until metadata loads, then smoothly updates to workspace name. _Generated with cmux_ Change-Id: I972fb1937346ca27e2fda3ebc4312bc15327bd83 Signed-off-by: Test <test@example.com> 🤖 fix: Pass workspace title as route param to prevent flicker **Changes:** - Pass title as route param when navigating to workspace - Set title in workspace/[id].tsx using Stack.Screen with params.title - Remove useEffect that dynamically updates title (no longer needed) - Remove useNavigation import (no longer needed) **Flow:** 1. User taps workspace in list 2. Router navigates with params: { id, title: 'cmux › react-native' } 3. Route file sets Stack.Screen title immediately from params 4. No flicker - title is correct from the start Result: Clean navigation with proper title from first render. No 'Workspace' flicker. _Generated with cmux_ Change-Id: Ib2764c1aa7178a56f466738ee51c973c524052f9 Signed-off-by: Test <test@example.com> 🤖 refactor: Remove terminal button from workspace action bar Remove terminal icon since mobile app doesn't have built-in terminal emulator yet. Custom action bar now shows only: 🔑 ⚙️ Can be re-added later when terminal functionality is implemented. _Generated with cmux_ Change-Id: I9182f734cee4d8b116c0ccbf075a9ba433b68cf5 Signed-off-by: Test <test@example.com> 🤖 feat: Add todo list toggle button to workspace action bar **Changes:** - Remove terminal button (not implemented yet) - Add todo list toggle button (list icon) - Button only appears when todos exist (currentTodos.length > 0) - Icon changes: 'list' (filled) when visible, 'list-outline' when hidden - Color changes: accent blue when visible, normal when hidden - Rename todoCardDismissed to todoCardVisible (clearer intent) - Toggle button positioned left of secrets icon **Behavior:** - Shows when agent creates todos during streaming - Tap to hide/show todo card - Auto-shows when new todos arrive - Persists visibility state until workspace change **Action bar now shows:** - When todos exist: 📋 🔑 ⚙️ - When no todos: 🔑 ⚙️ _Generated with cmux_ Change-Id: I2608960c3bd124e058239b63bce3f4fbe87d4585 Signed-off-by: Test <test@example.com> 🤖 fix: Forward delete events to ChatEventProcessor Fix bug where delete events (from truncation/compaction) were silently ignored. **Root Cause:** After refactoring to ChatEventProcessor, message state moved from StreamingMessageAggregator.messages to processor's internal storage. But handleDeleteMessage still iterated this.messages (never populated), so deletions did nothing. **Fix:** 1. Add deleteByHistorySequence() method to ChatEventProcessor interface 2. Implement in ChatEventProcessor to delete messages by historySequence 3. Update StreamingMessageAggregator.handleDeleteMessage to delegate to processor 4. Remove stale iteration of this.messages **Result:** Delete events now properly remove messages from UI. History truncation and compaction correctly clear messages from chat display. Fixes review comment P0: Delete events now take effect. _Generated with cmux_ Change-Id: I27e1bdfcd01c8d0a1569b57842b4aac901933482 Signed-off-by: Test <test@example.com> 🤖 debug: Add logging for todo_write event tracking Add console.log to debug why todo button might not be appearing. This will help identify if events are being received correctly. _Generated with cmux_ Change-Id: I266f2f3d0c8b34e2ccc53d97b386d8e4c693d9bd Signed-off-by: Test <test@example.com> 🤖 debug: Add verbose event logging to diagnose todo event issue Log all incoming events to see if tool-call-end is arriving. This will help identify if: - Events aren't being sent via WebSocket - Event structure is different than expected - Events are being filtered somewhere _Generated with cmux_ Change-Id: Idbe222cfef4792cd8a13614b1a4d9ce501dd09d0 Signed-off-by: Test <test@example.com> 🤖 debug: Add test button to manually trigger todo UI Add bug icon button that populates currentTodos with test data. This lets us validate the todo UI without waiting for agent to call todo_write. Tap the bug icon (🐛) and you should see: - Todo toggle button (📋) appear - Floating todo card with 3 test items Remove this debug button once todo tracking is confirmed working. _Generated with cmux_ Change-Id: Ib3d2f6f408b6cc91777e9eb6f6e56d5e2a71fb8e Signed-off-by: Test <test@example.com> 🤖 fix: Prevent timeline jumping with multi-client connections **Problem:** When web and mobile both connected to same workspace, every event triggered a full timeline re-sort, causing FlatList to jump and reload messages. **Fix:** 1. Deduplicate messages - skip if already exists (same ID) 2. Update in place for streaming deltas (no sort) 3. Append without sort when message is in order (common case) 4. Only sort when message is out of order (rare) **Performance:** - Before: O(n log n) sort on EVERY event - After: O(1) append for ordered messages, O(n) dedup check **Result:** - No more jumping when multiple clients connected - Smooth scrolling during streaming - Messages only re-sort when truly out of order _Generated with cmux_ Change-Id: I665680a9eb72150b7cb7d35627e5d6c818620baf Signed-off-by: Test <test@example.com> 🤖 fix: Fix sendMessage error handling and add streaming indicator **SendMessage Fix:** - Wrap sendMessage in try-catch to handle exceptions properly - Add console.error logging to debug actual failures - Move setInput("") to finally block so it always clears - Only show error toast for actual failures (not false positives) **Streaming Indicator:** - Track isStreaming state from stream-start/stream-end events - Extract model name from stream-start event - Display compact indicator above input: "model streaming..." - Styled with accent color, compact size - Positioned between todos and input area - Disappears when stream ends **Result:** - No more false "Request failed" errors on successful sends - Shows "claude-sonnet-4-5 streaming..." during agent work - Matches desktop UX (model name + streaming indicator) _Generated with cmux_ Change-Id: I4f2fffe4b82ec8ce0d439ca36bd009979cd7de05 Signed-off-by: Test <test@example.com> 🤖 fix: Add detailed sendMessage logging and prevent unnecessary FlatList updates **SendMessage Debugging:** - Log server response to see actual structure - Handle multiple response formats (void, Result, undefined) - Assume success if no error thrown (more lenient) - Detailed console logging for debugging **Timeline Stability:** - Only update timeline state if events actually changed it - Return same reference when applyChatEvent returns unchanged array - Prevents FlatList re-render when no actual changes - Reduces jumping from duplicate/no-op events Check console for: [sendMessage] Server response: {...} This will show what the server is actually returning. _Generated with cmux_ Change-Id: I1c9c51e3acfc502b565fd9db18062f233bb07f61 Signed-off-by: Test <test@example.com> 🤖 fix: Make sendMessage fire-and-forget to prevent false errors **Problem:** sendMessage was waiting for HTTP response, but server returns immediately while streaming happens async via WebSocket. This caused "Request failed" errors even though stream-start arrived successfully. **Solution:** - Fire and forget: Don't wait for HTTP response - Errors come via stream-error WebSocket events (not HTTP response) - Only validation errors (empty message, etc.) shown via HTTP - Clear input immediately for better UX (like iMessage) **Timeline Jumping:** - Return same array reference when no changes (prevents FlatList update) - Only update state when events actually modify timeline **Result:** - No more "Request failed" on successful sends - Input clears immediately (better UX) - Stream errors arrive via WebSocket (proper error handling) - Less timeline jumping _Generated with cmux_ Change-Id: I247fc41a7095ba4657a0eaff6fe453851f380993 Signed-off-by: Test <test@example.com> 🤖 feat: Transform send button to cancel button when streaming **Send/Cancel Button Transformation:** - When not streaming: Blue circle with arrow-up (send) - When streaming: Red square with stop icon (cancel) - Button always enabled during streaming (can cancel anytime) - Calls interruptStream API when tapped during streaming **Visual States:** - Circle (blue) → Send message - Square (red) → Cancel streaming **API:** - Add interruptStream method to mobile API client - Calls WORKSPACE_INTERRUPT_STREAM IPC channel **UX:** - Matches ChatGPT/Claude web behavior - Clear visual indication of streaming state - Easy to cancel mid-stream - Button shape change makes function obvious _Generated with cmux_ Change-Id: Ife2300ca9641e734eb321237f180da9d6ab01313 Signed-off-by: Test <test@example.com> 🤖 fix: Use accent color for cancel button and center with input **Color Fix:** - Change cancel button from danger (red) to accent (blue) - Matches project color scheme and web version - Pressed state uses accentHover (darker blue) - Consistent with send button color **Alignment Fix:** - Change alignItems from 'flex-end' to 'center' - Button now vertically centers with input as it grows - Looks natural when input expands to multiple lines - Matches iMessage/WhatsApp behavior **Result:** - Cancel button is blue square (not red) - Button stays centered as input grows - Consistent with project design language _Generated with cmux_ Change-Id: I71f3e22952bb08f14dad7e010e94e06583a7bf81 Signed-off-by: Test <test@example.com> 🤖 fix: Silence spurious sendMessage HTTP errors Remove console.error logging for async sendMessage errors. These are not real errors - the server HTTP response may return before the stream completes, causing false "Request failed" errors. Actual errors arrive via stream-error WebSocket events and are shown in chat. HTTP response errors are noise and should be silently ignored. _Generated with cmux_ Change-Id: I97fc6961a53e58401adfc265e0595a6c524b7b04 Signed-off-by: Test <test@example.com> 🤖 fix: Use 60-second trailing window for TPS like desktop **Problem:** Mobile app used simple calculation: TPS = total tokens / elapsed time from start This becomes increasingly inaccurate and doesn't match desktop display. Desktop uses sophisticated 60-second trailing window that reflects recent speed. **Fix:** - Track deltas with timestamps in array (not just cumulative count) - Calculate TPS using 60-second trailing window (matches desktop) - Include tool-call-start tokens (was missing) - Prune old deltas outside window for accurate recent TPS - Use same algorithm as StreamingTPSCalculator.ts **Algorithm (matches desktop):** 1. Store deltas: { tokens, timestamp } 2. Filter to last 60 seconds 3. TPS = recent tokens / time span of recent deltas 4. Total = sum of all deltas **Result:** - Mobile TPS now matches desktop/web display - More accurate (reflects current speed, not average from start) - Includes all token sources (stream, reasoning, tool args) _Generated with cmux_ Change-Id: I2b05b0095e19faf868eba9e365283fa036fc9a26 Signed-off-by: Test <test@example.com> 🤖 feat: Add collapsible reasoning matching desktop behavior **Desktop Behavior:** - Reasoning expands while streaming to show full content - Shows 'Thinking' title with 💭 emoji - Auto-collapses to just title when complete - User can tap to manually expand/collapse **Mobile Implementation:** - Add Pressable header with chevron icon - State: isExpanded (default: isStreaming) - useEffect: Auto-collapse when isStreaming → false - Disabled while streaming (can't collapse) - Shows ⟳ spinner when streaming, chevron when complete **Visual States:** While streaming (auto-expanded): 💭 Thinking ⟳ ────────────────────────── Reasoning deltas appear in real-time... ▌ After complete (auto-collapsed): 💭 Thinking ▶ Tap to expand: 💭 Thinking ▼ ────────────────────────── Full completed reasoning Result: Matches desktop - reasoning visible while generating, auto-collapses when done to save space. _Generated with cmux_ Change-Id: I6be888e6c92cbc1254e48c04d0eaaeb7adc60b29 Signed-off-by: Test <test@example.com> 🤖 refactor: Simplify reasoning - no icons, auto-collapse on finish **Changes:** - Remove all icons (chevrons, spinners, emojis) - Default to expanded (show content immediately) - Auto-collapse when isStreaming becomes false (reasoning finished) - Simple Pressable header - just 'Thinking' text - User can tap to expand/collapse anytime **Behavior:** 1. Reasoning starts → Message appears expanded with deltas streaming 2. Reasoning finishes → Auto-collapses to just 'Thinking' title 3. User taps → Expands to show full content **Visual:** While reasoning (expanded): Thinking ───────────────────── Reasoning deltas... ▌ After finished (auto-collapsed): Thinking Tap to expand: Thinking ───────────────────── Full reasoning content Simple, clean, matches user request. _Generated with cmux_ Change-Id: Ib82a131d55207b1c3fefe2636f12932b223aaf42 Signed-off-by: Test <test@example.com> 🤖 feat: Add special status_set tool rendering for mobile Implement mobile-optimized status_set display matching desktop UX: **New Component: StatusSetToolCard (~55 LoC)** - Compact inline display (no expand/collapse) - Shows emoji + message in single line - Subtle background with left border - Border color matches status (green=success, red=failed) - Italic text for status message - Always visible (not hideable like generic tools) **MessageRenderer Dispatch:** - Add isStatusSetTool() type guard - Route status_set to StatusSetToolCard - Validates emoji and message in args **Desktop Comparison:** - Desktop: Shows 'emoji message' in collapsed tool header - Mobile: Shows 'emoji message' in compact card - Both: Always visible, no expansion needed **Visual Example:** ┌─────────────────────────────┐ │ 🔧 Investigating token calc │ └─────────────────────────────┘ Clean, compact, matches desktop minimal display. _Generated with cmux_ Change-Id: Ieb5e87638b950e99a058ac099bc6118db804edad Signed-off-by: Test <test@example.com> 🤖 fix: Track tool-call-delta tokens to match desktop count **Root Cause:** Mobile was missing tool-call-delta from token tracking, causing lower token counts than desktop. **What Desktop Tracks:** - stream-delta (text tokens) - reasoning-delta (thinking tokens) - tool-call-start (initial tool arg tokens) - tool-call-delta (streaming tool arg tokens) ← MOBILE WAS MISSING THIS **Impact:** When tools have large arguments (e.g., file_edit_replace_string with big code blocks), tool-call-delta events stream additional tokens. Example: - tool-call-start: 100 tokens - tool-call-delta: 50 tokens (mobile was ignoring) - tool-call-delta: 30 tokens (mobile was ignoring) Desktop: 180 tokens Mobile (before): 100 tokens Mobile (after): 180 tokens ✅ **Fix:** Add 'tool-call-delta' to event type check in token tracking. Result: Mobile token count now matches desktop exactly. _Generated with cmux_ Change-Id: Ieb6415afca133083ef4190460446e10cdd81e48c Signed-off-by: Test <test@example.com> 🤖 refactor: Change 'Projects' label to 'Workspaces' on main screen Change main screen title from 'Projects' to 'Workspaces' for consistency with Expo Router header title and workspace-centric UX. _Generated with cmux_ Change-Id: Id3a7b4ac341daf45c92c92fb7152f50ec3845c26 Signed-off-by: Test <test@example.com> 🤖 refactor: Remove duplicate 'Workspaces' label Remove 'Workspaces' title from page content since it's already shown in the Expo Router header. Keep only the subtitle and settings icon. Result: Cleaner, less redundant UI. More space for workspace list. _Generated with cmux_ Change-Id: I20fea3acc58430b6758a854021634dd372484236 Signed-off-by: Test <test@example.com> 🤖 refactor: Simplify subtitle to 'chat' instead of 'chat timeline' Change 'Select a workspace to open the chat timeline.' to 'Select a workspace to open the chat.' for simpler, clearer language. _Generated with cmux_ Change-Id: Iefcb14e0af5f61705a72e83d84ad1abaab9baa34 Signed-off-by: Test <test@example.com> 🤖 feat: implement secrets management for React Native app Add full secrets management functionality to mobile app: - Add IPC channel constants for PROJECT_SECRETS_GET and PROJECT_SECRETS_UPDATE - Create Secret and SecretsConfig types mirroring desktop implementation - Extend API client with secrets.get() and secrets.update() methods - Create mobile-optimized SecretsModal component with: - Key-value pair management - Password visibility toggles - Auto-capitalization of keys for env var convention - Keyboard avoidance for mobile UX - Integrate secrets button into ProjectsScreen with handlers Secrets are stored server-side in ~/.cmux/secrets.json and accessed via IPC, ensuring sensitive data never leaves the backend. Compatible with existing desktop secrets infrastructure. _Generated with `cmux`_ Change-Id: I9125b73fca5596f9c86e7ab868a943c4cb499de5 Signed-off-by: Test <test@example.com> 🤖 fix: replace gap with marginRight for React Native compatibility The gap property is not fully supported in React Native, causing runtime errors. Replace with marginRight on the first button for proper spacing. _Generated with `cmux`_ Change-Id: Ib6c055af3b0e8057ebbb9f17fae234df3ed3dfe7 Signed-off-by: Test <test@example.com> 🤖 fix: improve secrets modal and remove settings gear icon - Add debug logging to track secrets loading and modal behavior - Fix ScrollView layout by replacing flex:1 with maxHeight - Remove settings gear icon from workspace screen header The ScrollView was causing layout issues by using flex:1 inside a flex container. Switching to maxHeight provides better constraint behavior. Debug logs will help identify why secrets aren't displaying correctly. _Generated with `cmux`_ Change-Id: I126f4dac39f757a469432ad19d3432337bc80bbe Signed-off-by: Test <test@example.com> 🤖 refactor: redesign secrets modal with iOS-native styling Major UX improvements for mobile: **Workspace Screen:** - Restored settings gear icon (removed key icon - no per-workspace secrets) **Secrets Modal:** - iOS-native header with Cancel/Done buttons - Full-screen modal with pageSheet presentation style - Cleaner card-based layout for each secret - Visibility toggle positioned inside value input - Larger touch targets and better spacing - Empty state with icon and helpful text - Prominent "Add Secret" button with icon - Removed bottom action buttons (now in header) - Better typography with uppercase labels - Improved input styling with rounded corners The modal now follows iOS design patterns with a navigation-style header, better visual hierarchy, and more touch-friendly controls. _Generated with `cmux`_ Change-Id: I5ff28487c2481282b26e998a578ce09a43113d88 Signed-off-by: Test <test@example.com> 🤖 chore: remove debug logs from secrets modal Remove console.log statements added during debugging. _Generated with `cmux`_ Change-Id: Ie2b847c0cf98be8430534b96ebb917a363a8f0e2 Signed-off-by: Test <test@example.com> 🤖 fix: add safe area insets to secrets modal Respect iOS safe area insets (notch, status bar, home indicator) by: - Adding top padding with insets.top - Adding bottom padding with insets.bottom - Import and use useSafeAreaInsets hook Prevents content from being overlaid by system UI elements. _Generated with `cmux`_ Change-Id: Iebd4bb2f43d84e1a81f4942a3b5fc9d1e7619534 Signed-off-by: Test <test@example.com> 🤖 fix: remove transparent prop from modal to fix warning Remove transparent={true} from Modal as it's incompatible with presentationStyle='pageSheet'. This fixes the warning: "Modal with 'pageSheet' presentation style and 'transparent' value is not supported." _Generated with `cmux`_ Change-Id: Ib910b59678763e499918b028f3af1ab3a4d93565 Signed-off-by: Test <test@example.com> 🤖 fix: remove manual safe area insets from pageSheet modal pageSheet presentation style handles safe area insets automatically, so manual insets were causing double padding. Removed useSafeAreaInsets and manual padding adjustments. _Generated with `cmux`_ Change-Id: I5cec60c1a0fa97871c4ccabcb59cce96365cd7ae Signed-off-by: Test <test@example.com> 🤖 style: increase header padding in secrets modal Increase top and bottom padding from spacing.md to spacing.lg to make the header look less cramped and more iOS-native. _Generated with `cmux`_ Change-Id: I8edeb3e670304559d00904c9e01a83c91b332685 Signed-off-by: Test <test@example.com> 🤖 style: adjust Cancel and Done button positioning Add horizontal padding to Cancel and Done buttons to move them slightly toward center, creating better visual balance and larger touch targets. Also reduce header container padding accordingly. _Generated with `cmux`_ Change-Id: Ic580a0a3467fdc3ab4f005b21f8a59fe96bf17ab Signed-off-by: Test <test@example.com> 🤖 style: vertically center message input placeholder Add textAlignVertical='center' to message input to ensure placeholder text is vertically centered while remaining left-aligned. _Generated with `cmux`_ Change-Id: Ia161befad85413f833f99716b343c69b1752d929 Signed-off-by: Test <test@example.com> 🤖 feat: implement native workspace creation for React Native app Adds platform-native workspace creation modal for the mobile app, enabling users to create workspaces directly from their mobile devices. New Components: - NewWorkspaceModal: Platform-native modal with branch selection, runtime configuration, and SSH support - Runtime type definitions: Copied from web version for consistency - AsyncStorage utilities: Persist runtime preferences per project Features: - Branch name input with validation - Trunk branch selection (dropdown or manual input) - Runtime mode selection (Local/SSH) - SSH host configuration - Real-time workspace path preview - Runtime preference persistence - Loading states and error handling - Navigation to new workspace after creation - Automatic workspace list refresh IPC Integration: - Added PROJECT_LIST_BRANCHES method to fetch available branches - Added WORKSPACE_CREATE method to create workspaces - Follows same IPC patterns as web version Implementation follows SecretsModal.tsx pattern for consistency with existing mobile UI patterns. Includes race condition guards for async branch loading and defensive input validation. _Generated with `cmux`_ Change-Id: I823970f8591267e4848e1e046823398e1fac7257 Signed-off-by: Test <test@example.com> 🤖 fix: correct projectPath prop reference in NewWorkspaceModal The prop was renamed to _projectPath in destructuring but referenced as projectPath in the useEffect dependency array, causing a ReferenceError. Removed the underscore prefix since projectPath is actually used in the component for loading runtime preferences. _Generated with `cmux`_ Change-Id: I3f7c9284c35e37176eb75a1273d73338fd216e0b Signed-off-by: Test <test@example.com> 🤖 refactor: remove workspace path info display from modal Removes the workspace path preview section as it's unnecessary for the mobile UX. Users can see where workspaces are created from the workspace list after creation. _Generated with `cmux`_ Change-Id: I2d482887e92c187a7b709df84412b590cfe8d7c4 Signed-off-by: Test <test@example.com> 🤖 refactor: remove FAB and filesystem path display Removes two unnecessary UI elements: 1. Floating action button (FAB) with "coming soon" alert - no longer needed since workspace creation is now available via the + button in each project header 2. Filesystem path display under project names - not relevant on mobile devices where users don't interact with filesystem paths This streamlines the mobile UI and removes confusion about workspace creation availability. _Generated with `cmux`_ Change-Id: I9f67f4d5d4c555d3c6fef924aca79f214137029d Signed-off-by: Test <test@example.com> 🤖 feat: add workspace deletion with long-press gesture in React Native - Extended mobile API client with workspace.remove() and workspace.rename() methods - Updated metadata subscription to handle deletion events (null metadata) - Added long-press gesture on workspace items to show platform-native action sheet - Implemented delete handler with two-step confirmation and force delete option - Added rename stub for future implementation Workspace deletion now works via long-press → action sheet → confirmation dialog, with proper handling of uncommitted changes and real-time UI updates via WebSocket. _Generated with `cmux`_ Change-Id: Ia71754155fea86c4a5c3baa0757f76aca08184a8 Signed-off-by: Test <test@example.com> 🤖 feat: implement workspace renaming for React Native app - Add workspace validation utility with comprehensive tests (16 test cases) - Create RenameWorkspaceModal component with platform-native styling - Wire up rename functionality to ProjectsScreen with long-press action - Support real-time validation with inline error messages - Handle loading states and keyboard interactions - Fix text contrast issues for proper dark mode support The modal follows platform conventions (iOS bottom sheet, Android center modal) and provides identical validation to the web version. Change-Id: If9a9d59a9e809d72ccd416cf6c6c5917459a37db Signed-off-by: Test <test@example.com> 🤖 feat: implement Start from Here for React Native mobile app Add "Start from Here" functionality to compact chat history by replacing all messages with a single compacted message (typically a plan). **Changes:** - Add replaceChatHistory method to mobile API client - Create messageHelpers utility with createCompactedMessage function - Add StartHereModal component for confirmation - Update ProposePlanCard with "📦 Start Here" button - Wire up handler in WorkspaceScreen and MessageRenderer - Display compacted badge on assistant messages **Architecture:** - Uses existing IPC channel workspace:replaceHistory - Backend atomically clears history and sends delete events - UI updates automatically via WebSocket subscription - Inline types avoid shared type imports (mobile is separate package) **UX Flow:** 1. User taps "📦 Start Here" on plan 2. Modal confirms action 3. Client calls replaceChatHistory with compacted message 4. Backend clears history, sends delete event for old messages 5. Backend sends new compacted message 6. UI updates automatically, old messages disappear Generated with `cmux` Change-Id: I16454ebe06781a250cc45b37876211988cf34e55 Signed-off-by: Test <test@example.com> 🤖 fix: WebSocket history replay broadcasting to all clients Fix bug where history replay was broadcast to ALL connected WebSocket clients instead of only the newly subscribing client. **Changes:** - Add WORKSPACE_CHAT_GET_HISTORY IPC handler for fetching history without broadcasts - Modify WebSocket subscription to fetch and send history directly to client - Send 'caught-up' message after history replay (required by frontend) - Add integration test to verify fix **Implementation:** - src/constants/ipc-constants.ts: Add WORKSPACE_CHAT_GET_HISTORY constant - src/services/ipcMain.ts: Add getWorkspaceChatHistory() method and handler - src/main-server.ts: Add getHandler() and modify subscription logic - tests/ipcMain/websocketHistoryReplay.test.ts: New test file **Technical Details:** WebSocket server now uses request-response pattern (getHistory) instead of event broadcasting for history replay. Electron app behavior unchanged. Fixes issue where multiple clients subscribed to same workspace would all receive duplicate history when any new client subscribed. Generated with cmux Change-Id: Icf4b5c461bc8141aa9039dbe5d2b5884f46f9cad Signed-off-by: Test <test@example.com> 🤖 fix: WebSocket replay now includes active streams and partial messages **Critical Bug Fix:** WebSocket history replay was only sending persisted messages, missing active streams and partial/interrupted responses. This caused "stream-delta for unknown message" errors when clients joined during active streaming. **Root Cause:** - Original fix used getWorkspaceChatHistory() which only returns chat.jsonl - Missing: active streams, partial messages, init state from full replay - Client received stream-delta events for messages it never got during replay **Solution:** WebSocket now uses the FULL replay mechanism by: 1. Temporarily intercepting events during workspace:chat:subscribe 2. Collecting all replay events (history + active streams + partial + init) 3. Sending collected events directly to subscribing WebSocket client only 4. Removing temporary listener to prevent future broadcasts **Changes:** - src/main-server.ts: Use full replay with temporary event interception - src/main-server.ts: Add getListeners() for listener management - tests/ipcMain/websocketHistoryReplay.test.ts: Update documentation **Technical Details:** The fix preserves all benefits of the original targeted replay (no broadcast to other clients) while including ALL replay data that AgentSession.replayHistory() provides. This matches Electron app behavior for feature parity. Fixes: "Received stream-delta for unknown message" errors in mobile app Generated with cmux Change-Id: Ic7528b432e9823ec0c41d330fe179efb6676e023 Signed-off-by: Test <test@example.com> 🤖 feat: add git review feature for React Native app - Add executeBash method to mobile API client - Copy git utilities (diffParser, numstatParser, gitCommands) from web app - Add review types for diff hunks and file changes - Create GitReviewScreen with trunk branch comparison - Create DiffHunkView component with syntax highlighting - Create ReviewFilters component for base branch selection - Add review route at /workspace/[id]/review - Add git-branch icon to workspace header for quick access - Fix nested data access for executeBash API responses - Add defensive type checks in git parsers Defaults to comparing against 'main' branch with uncommitted changes included. Users can switch between main/master/origin/main/HEAD via filter UI. Generated with `cmux` Change-Id: I97b430941ea5634f4d4c6bd95c709fe31b776815 Signed-off-by: Test <test@example.com> 🤖 refactor: replace workspace header buttons with iOS action sheet - Create WorkspaceActionSheet component with native iOS design - Add WorkspaceActionsContext for state sharing between route and screen - Replace custom header bar with ellipsis menu button - Implement proper iOS animations (blur fades, sheet slides) - Add safe area insets for home indicator devices - Consolidate Code Review, Todo List, Settings into menu - Remove inline header buttons to gain ~50px vertical space Menu items: - Code Review (navigate to git diff viewer) - Todo List (toggle, only shown if todos exist) - Workspace Settings (navigate to settings) Uses Animated API for smooth slide-up animation with spring physics. Includes haptic feedback and blur background following iOS HIG. Generated with `cmux` Change-Id: Ia81dc618ae9249d8bfcf72f292331e10e44f4e7d Signed-off-by: Test <test@example.com> 🤖 refactor: move settings to header and clean up workspace list - Add settings icon to main screen header (expo router navbar) - Remove 'Select a workspace to open the chat.' label - Remove settings button from workspace list content - Remove unused IconButton import Gains ~60px vertical space for workspace list. Generated with `cmux` Change-Id: I56807a6320651bafd536f7a8fa5714d9bd9f8739 Signed-off-by: Test <test@example.com> 🤖 fix: restore IconButton import in ProjectsScreen IconButton is still used for add workspace and secrets buttons. Generated with `cmux` Change-Id: If5e63385a4b4e5805f99a57f260ed2de11132dd3 Signed-off-by: Test <test@example.com> 🤖 feat: add cost/usage display to React Native app - Extended mobile API client with tokenizer IPC channels (calculateStats, countTokens) - Added WorkspaceCostProvider context to track usage history and consumer breakdown - Implemented CostUsageSheet bottom sheet with session/last-response toggle - Wired cost action into workspace action sheet menu - Updated Metro/Babel/TypeScript configs to resolve @shared alias with explicit .ts extensions - Fixed React hook ordering in CostUsageSheet (moved early return after all hooks) Generated with `cmux` Change-Id: Ib2e093adfc770afa5927223632ce2004704f7dc5 Signed-off-by: Test <test@example.com> feat: add message editing and copy to React Native Change-Id: Icd4268db6cb26272ec679bc044abff4428ba72d7 Signed-off-by: Test <test@example.com> fix: resolve fontWeight type error in mobile header styles - Add 'as any' cast to fontWeight to prevent 'expected dynamic type boolean, but had type string' error - Clean up attempted unstable_headerRightItems implementation - Revert to standard headerRight with Ionicons (react-native-screens 4.16.0) The fontWeight property was causing a runtime error with react-native-screens when passing string values from the theme typography weights. The 'as any' cast resolves this type mismatch. Change-Id: I1b17a868d941f47935cca405cffe886f02397351 Signed-off-by: Test <test@example.com> 🤖 feat: initial React Native mobile app setup - Expo SDK 54 with React Native 0.81 - Workspace chat interface with streaming support - Message rendering with react-native-markdown-display - Copy functionality via long-press on user messages - Provider configuration and workspace management - WebSocket integration for real-time updates Generated with `cmux` Change-Id: I3dc6771753e9aa1789acaacb1f258dc2335f69ea Signed-off-by: Test <test@example.com> 🤖 feat: add long-press copy to all messages Adds long-press functionality to all message types for quick copying: **AssistantMessageCard:** - Long-press → Shows "Copy Message" option - iOS: Native ActionSheet with haptic feedback - Android: Custom modal bottom sheet - Uses expo-clipboard for cross-platform clipboard access **UserMessageCard:** - Already had long-press with "Edit Message" and "Copy Message" - No changes needed - works as before **Implementation:** - Added handleLongPress handler to AssistantMessageCard - Added handleCopy async function using expo-clipboard - Wrapped message Surface in Pressable with 500ms delay - Platform-specific UI: ActionSheet (iOS) vs Modal (Android) - Haptic feedback on long-press for tactile response **User Experience:** ✅ Long-press any assistant message → Copy Message ✅ Long-press user message → Edit Message + Copy Message ✅ Native platform conventions (ActionSheet/Modal) ✅ Haptic feedback for confirmation ✅ Cancel option to dismiss Generated with `cmux` Change-Id: Ib844fbb377f3d3091e2cdb086e85a6285ee3be5c Signed-off-by: Test <test@example.com> 🤖 fix: align React Native colors with web/Electron (Plan Mode now blue) - Add mode-specific colors to React Native theme (planMode, execMode, editMode, thinkingMode) - Replace hardcoded purple colors in ProposePlanCard with theme.colors.planMode (blue) - Update StartHereModal button to use theme.colors.planMode - Update MessageRenderer thinking/compacted labels to use theme colors - Remove 📦 emoji from Start Here button to match desktop Plan Mode now displays consistently in blue across all platforms instead of purple on mobile. _Generated with `cmux`_ Change-Id: I37394282b94cd0d8c3aaf2bda4aed3e2e63b3c05 Signed-off-by: Test <test@example.com> 🤖 fix: correct async IIFE closure in main-server.ts after rebase During the rebase conflict resolution, the async function wasn't properly closed. This fix ensures all server initialization code is within the async IIFE and adds the proper error handling catch block. _Generated with `mux`_ Change-Id: Icf4ae912147f3daac160aab5a05ae89987a1757b Signed-off-by: Test <test@example.com> 🤖 refactor: align mobile with desktop AI workspace creation flow Implements Option 1 from investigation: maximizes code sharing between desktop and mobile apps while bringing AI-generated workspace naming to mobile. **DRY Improvements:** - Consolidated runtime.ts: Mobile now imports from @shared/types/runtime (removed 76-line duplicate file) - Added @/ path alias to mobile tsconfig for consistency with desktop - Shared backend AI naming service works for both platforms via IPC **Mobile Changes:** - Added FirstMessageModal component (465 lines) - Equivalent to desktop's FirstMessageInput - Users describe task, AI generates workspace name/branch - Full runtime configuration support (Local/SSH) - Updated API client sendMessage() to support workspaceId: null - Matches desktop IPC signature - Returns workspace metadata on creation - Integrated into ProjectsScreen - New "Quick Start" button (⚡️ icon) opens FirstMessageModal - Existing "+" button still opens manual NewWorkspaceModal - Users can choose between AI naming or manual naming **Desktop Cleanup:** - Removed dead workspace modal code from App.tsx - Deleted 9 unused state variables - Removed 48-line handleCreateWorkspace function - Removed modal rendering (24 lines) - Modal was never triggered after FirstMessageInput implementation **Backward Compatibility:** - Both apps support both workspace.create() and sendMessage(null) flows - Mobile keeps manual naming modal for users who prefer control - No breaking changes to IPC or backend services **Testing:** - ✅ Mobile TypeScript compilation passes (no new errors) - ✅ Desktop TypeScript compilation passes (no new errors) - ✅ Runtime types shared without duplication - ✅ Both creation flows coexist in mobile Addresses divergence identified in investigation where desktop moved to AI-based workspace creation (Nov 13) while mobile used manual modal approach (implemented Nov 6). Change-Id: I6b5717928b6cb7be6a05c455c9eb5bae86c5e3f5 Signed-off-by: Test <test@example.com> fix: rename CmuxMessage to MuxMessage in mobile app Change-Id: Ib0bf591b9ba7143272e72a903c99de703a48ae26 Signed-off-by: Test <test@example.com> 🤖 feat: streamline mobile workspace creation + fix TS errors Integrate creation into WorkspaceScreen (no modals, -938 lines) Fix Cmux→Mux typo breaking streaming, 22 TS errors, keyboard dismiss _Generated with `mux`_ Change-Id: I35647bf5395b74d8cddd3eba9e47f1e45098dcff Signed-off-by: Test <test@example.com> 🤖 feat: add trunk branch & runtime selection to mobile workspace creation Add collapsible "Advanced Options" section to mobile workspace creation banner: - Trunk branch picker (select base branch for workspace) - Runtime picker (Local or SSH Remote) - Conditional SSH host input Features: - Progressive disclosure - collapsed by default, tap to expand - Runtime preferences persist per-project in AsyncStorage - Smart defaults: recommended trunk branch, local runtime - Follows existing mobile pattern (chevron toggle like FloatingTodoCard) Technical changes: - Import Picker, runtime types, and preference utilities - Add state for showAdvanced, runtimeMode, sshHost - Load runtime preference on mount in creation mode - Build RuntimeConfig in onSend, save preference on success - Replace creation banner with collapsible UI (~135 lines) _Generated with `mux`_ Change-Id: I1aed7e83d26920bceb4f12ad71d69a1a9dae9199 Signed-off-by: Test <test@example.com> 🤖 fix: remove leftover conflict marker in ChatInput Removed incomplete conflict marker from line 105 that was accidentally committed in 67dbec07. This was not an active conflict - just cleanup of a stray <<<<<<< HEAD marker with no corresponding separator or ending. _Generated with `mux`_ Change-Id: I398c227b1ef53bf14baa771e8cfc8509834be8d6 Signed-off-by: Test <test@example.com> 🤖 fix: resolve undefined defaultModel reference errors Fixed two TypeScript/runtime errors: 1. AIView.tsx - Added missing WORKSPACE_DEFAULTS import 2. useModelLRU.ts - Replaced undefined defaultModel with WORKSPACE_DEFAULTS.model These errors were causing 'Can't find variable: defaultModel' in the browser console and preventing the web version from loading correctly. _Generated with `mux`_ Change-Id: I8701fd231c84bdb873435a1479dfc9849d0ce431 Signed-off-by: Test <test@example.com> 🤖 feat: add model picker and shared catalog to mobile app - Import KNOWN_MODELS from desktop constants for single source of truth - Add modelCatalog.ts with validation, display formatting, and LRU sanitization helpers - Implement useModelHistory hook with SecureStore persistence (max 8 recent models) - Create ModelPickerSheet component with search, recent chips, and grouped list by provider - Update useWorkspaceSettings/useWorkspaceDefaults to validate models against catalog - Wire picker into WorkspaceScreen above composer with model summary display - Add assertKnownModelId guard in sendMessage IPC call - Display human-friendly model names in streaming indicator Generated with `mux` Change-Id: I60fd0945056c26dea2e16d4c5d652cadf919c654 Signed-off-by: Test <test@example.com> 🤖 feat: expand mobile run settings sheet - Replace ModelPickerSheet with multi-section RunSettingsSheet - Add mode, reasoning, and 1M context controls alongside model search - Wire WorkspaceScreen to set modes/thinking/context directly from sheet Generated with Change-Id: I5fe86aa251a08fe5d3201860af01f74e60e6d926 Signed-off-by: Test <test@example.com> 🤖 feat: consolidate run settings in chat screen - Remove standalone workspace settings route and menu entry - Expand run settings pill to show mode and reasoning level summary - Keep RunSettingsSheet as the single place to adjust per-workspace configuration Generated with Change-Id: Ie424deb12dff3f5829f2225801dc60a241c32e44 Signed-off-by: Test <test@example.com> 🤖 fix: avoid nested virtualized lists in run settings - Replace FlatList with simple mapped view inside RunSettingsSheet to prevent ScrollView nesting errors - Add capped height container while keeping separators and empty state messaging Generated with Change-Id: I53779b91607177dcce74d40c196be48ab9a8570c Signed-off-by: Test <test@example.com> 🤖 fix: restore scrolling in run settings model list - Wrap model list in nested ScrollView with max height and indicators - Keep non-virtualized implementation to avoid ScrollView + VirtualizedList warning Generated with Change-Id: I4411270d61b9697e4cdac35300ff8c938c5dcdaa Signed-off-by: Test <test@example.com> 🤖 fix: simplify mobile run settings layout - Remove collapsible sections and present model/mode/reasoning/context inline - Add extra header inset so the Run settings title clears safe areas - Keep model list scrollable via nested ScrollView without virtualized nesting Generated with Change-Id: If60679bf6bff95af019a232d2c02c4721e0131d2 Signed-off-by: Test <test@example.com> 🤖 feat: add slash commands to RN chat _Generated with _ Change-Id: I54985c290c02f22d3e6592bfce7dad28159161f1 Signed-off-by: Test <test@example.com> feat: render markdown in mobile reasoning Change-Id: I5cc34c9c14fa379086608d9229c32acb65f85a1e Signed-off-by: Test <test@example.com> 🤖 feat: align mobile TODO lifecycle with desktop behavior Remove WorkspaceActionsContext and manual toggle from action menu Drive TODO visibility purely from stream events (caught-up boundary) Hide TODOs on stream-end and when reopening idle workspaces Add todoLifecycle utilities with defensive assertions and tests _Generated with mux_ Change-Id: I7a74e287009f8e178ce9d8f8b6f1dcd360411f73 Signed-off-by: Test <test@example.com> feat: align mobile workspace ordering w/ activity Change-Id: Ia47a538b0c8d74105be6c20a6e07238bfc359606 Signed-off-by: Test <test@example.com> fix: simplify mobile composer autosize Change-Id: I7d080ee0feec5a84d22f1a6144fca53c5bf1308f Signed-off-by: Test <test@example.com> 🤖 feat: align RN message chrome with desktop _Generated with _ Change-Id: I57ed98ae9b9e8459ae773a5eb42d25c6f274cf43 Signed-off-by: Test <test@example.com> 🤖 feat: streamline RN tool rendering with web parity - Add specialized tool cards (bash, file_read, file_edit_*) with diff syntax highlighting - Implement proper streaming state management matching desktop reducer - Fix reasoning auto-collapse and streaming cursor visibility - Add MessageBubble chrome with consistent action buttons/timestamps - Pin react-native-worklets to 0.5.1 for Expo Go compatibility _Generated with `mux`_ Change-Id: I3d11d83d956a909a52501346b98afeb24eecd40f Signed-off-by: Test <test@example.com> chore: move mobile app to top level Change-Id: Id66bb660223ccbc7fbf7c306d4f1de96f0e7e24c Signed-off-by: Test <test@example.com> chore: rename cmux references to mux Change-Id: I2160509c1397552b547117dce280b02c7a884146 Signed-off-by: Test <test@example.com>
1 parent 5d553f9 commit afaf80f

File tree

129 files changed

+18046
-246
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

129 files changed

+18046
-246
lines changed

.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Environment variables for mux development
22

3+
# Optional bearer token for mux server HTTP/WS auth
4+
# When set, clients must include:
5+
# - HTTP: Authorization: Bearer $MUX_SERVER_AUTH_TOKEN
6+
# - WebSocket: ws://host:port/ws?token=$MUX_SERVER_AUTH_TOKEN (recommended for Expo)
7+
# - or Sec-WebSocket-Protocol header with the token value
8+
MUX_SERVER_AUTH_TOKEN=
9+
310
# API Keys for AI providers
411
# Required for integration tests when TEST_INTEGRATION=1
512
ANTHROPIC_API_KEY=sk-ant-...

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,4 @@ test_hot_reload.sh
122122
# mdBook auto-generated assets
123123
docs/theme/pagetoc.css
124124
docs/theme/pagetoc.js
125+
mobile/.expo/

Makefile

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ include fmt.mk
4545

4646
.PHONY: all build dev start clean help
4747
.PHONY: build-renderer version build-icons build-static
48-
.PHONY: lint lint-fix typecheck static-check
48+
.PHONY: lint lint-fix typecheck typecheck-react-native static-check
4949
.PHONY: test test-unit test-integration test-watch test-coverage test-e2e smoke-test
5050
.PHONY: dist dist-mac dist-win dist-linux
5151
.PHONY: vscode-ext vscode-ext-install
@@ -134,7 +134,7 @@ dev-server: node_modules/.installed build-main ## Start server mode with hot rel
134134
@# On Windows, use npm run because bunx doesn't correctly pass arguments
135135
@npmx concurrently -k \
136136
"npmx nodemon --watch src --watch tsconfig.main.json --watch tsconfig.json --ext ts,tsx,json --ignore dist --ignore node_modules --exec node scripts/build-main-watch.js" \
137-
"npmx nodemon --watch dist/main.js --watch dist/main-server.js --delay 500ms --exec \"node dist/main.js server --host $(or $(BACKEND_HOST),localhost) --port $(or $(BACKEND_PORT),3000)\"" \
137+
"npmx nodemon --watch dist/cli/index.js --watch dist/cli/server.js --delay 500ms --exec \"node dist/cli/index.js server --host $(or $(BACKEND_HOST),localhost) --port $(or $(BACKEND_PORT),3000)\"" \
138138
"$(SHELL) -lc \"MUX_VITE_HOST=$(or $(VITE_HOST),127.0.0.1) MUX_VITE_PORT=$(or $(VITE_PORT),5173) VITE_BACKEND_URL=http://$(or $(BACKEND_HOST),localhost):$(or $(BACKEND_PORT),3000) vite\""
139139
else
140140
dev-server: node_modules/.installed build-main ## Start server mode with hot reload (backend :3000 + frontend :5173). Use VITE_HOST=0.0.0.0 BACKEND_HOST=0.0.0.0 for remote access
@@ -145,7 +145,7 @@ dev-server: node_modules/.installed build-main ## Start server mode with hot rel
145145
@echo "For remote access: make dev-server VITE_HOST=0.0.0.0 BACKEND_HOST=0.0.0.0"
146146
@bun x concurrently -k \
147147
"bun x concurrently \"$(TSGO) -w -p tsconfig.main.json\" \"bun x tsc-alias -w -p tsconfig.main.json\"" \
148-
"bun x nodemon --watch dist/main.js --watch dist/main-server.js --delay 500ms --exec 'NODE_ENV=development node dist/main.js server --host $(or $(BACKEND_HOST),localhost) --port $(or $(BACKEND_PORT),3000)'" \
148+
"bun x nodemon --watch dist/cli/index.js --watch dist/cli/server.js --delay 500ms --exec 'NODE_ENV=development node dist/cli/index.js server --host $(or $(BACKEND_HOST),localhost) --port $(or $(BACKEND_PORT),3000)'" \
149149
"MUX_VITE_HOST=$(or $(VITE_HOST),127.0.0.1) MUX_VITE_PORT=$(or $(VITE_PORT),5173) VITE_BACKEND_URL=http://$(or $(BACKEND_HOST),localhost):$(or $(BACKEND_PORT),3000) vite"
150150
endif
151151

@@ -228,6 +228,10 @@ typecheck: node_modules/.installed src/version.ts
228228
"$(TSGO) --noEmit -p tsconfig.main.json"
229229
endif
230230

231+
typecheck-react-native: ## Run TypeScript type checking for React Native app
232+
@echo "Type checking React Native app..."
233+
@cd mobile && bunx tsc --noEmit 2>&1 | grep -E "^(src/|app/|Type checking)" || echo "✓ No errors in React Native app"
234+
231235
check-deadcode: node_modules/.installed ## Check for potential dead code (manual only, not in static-check)
232236
@echo "Checking for potential dead code with ts-prune..."
233237
@echo "(Note: Some unused exports are legitimate - types, public APIs, entry points, etc.)"

fmt.mk

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
.PHONY: fmt fmt-check fmt-prettier fmt-prettier-check fmt-shell fmt-shell-check fmt-nix fmt-nix-check fmt-python fmt-python-check
77

88
# Centralized patterns - single source of truth
9-
PRETTIER_PATTERNS := 'src/**/*.{ts,tsx,json}' 'tests/**/*.ts' 'docs/**/*.md' 'package.json' 'tsconfig*.json' 'README.md'
9+
PRETTIER_PATTERNS := 'src/**/*.{ts,tsx,json}' 'mobile/**/*.{ts,tsx,json}' 'tests/**/*.ts' 'docs/**/*.md' 'package.json' 'tsconfig*.json' 'README.md'
1010
SHELL_SCRIPTS := scripts
1111
PYTHON_DIRS := benchmarks
1212

jest.config.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
module.exports = {
22
preset: "ts-jest",
33
testEnvironment: "node",
4-
testMatch: ["<rootDir>/src/**/*.test.ts", "<rootDir>/tests/**/*.test.ts"],
4+
testMatch: [
5+
"<rootDir>/src/**/*.test.ts",
6+
"<rootDir>/mobile/src/**/*.test.ts",
7+
"<rootDir>/tests/**/*.test.ts",
8+
],
59
collectCoverageFrom: [
610
"src/**/*.ts",
711
"!src/**/*.d.ts",

mobile/.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.expo
2+
3+
# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
4+
# The following patterns were generated by expo-cli
5+
6+
expo-env.d.ts
7+
# @end expo-cli

mobile/README.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# mux Mobile App
2+
3+
Expo React Native app for mux - connects to mux server over HTTP/WebSocket.
4+
5+
## Requirements
6+
7+
- **Expo SDK 54** with **React Native 0.81**
8+
- Node.js 20.19.4+
9+
- For iOS: Xcode 16+ (for iOS 26 SDK)
10+
- For Android: Android API 36+
11+
12+
## Development
13+
14+
### Quick Start (Expo Go)
15+
16+
**Note**: Expo Go on SDK 54 has limitations with native modules. For full functionality, use a development build (see below).
17+
18+
```bash
19+
cd mobile
20+
bun install
21+
bun start
22+
```
23+
24+
Scan the QR code in Expo Go (must be SDK 54).
25+
26+
### Development Build (Recommended)
27+
28+
For full native module support:
29+
30+
```bash
31+
cd mobile
32+
bun install
33+
34+
# iOS
35+
bunx expo run:ios
36+
37+
# Android
38+
bunx expo run:android
39+
```
40+
41+
This creates a custom development build with all necessary native modules baked in.
42+
43+
## Configuration
44+
45+
Edit `app.json` to set your server URL and auth token:
46+
47+
```json
48+
{
49+
"expo": {
50+
"extra": {
51+
"mux": {
52+
"baseUrl": "http://<your-tailscale-ip>:3000",
53+
"authToken": "your_token_here"
54+
}
55+
}
56+
}
57+
}
58+
```
59+
60+
## Server Setup
61+
62+
Start the mux server with auth (optional):
63+
64+
```bash
65+
# In the main mux repo
66+
MUX_SERVER_AUTH_TOKEN=your_token make dev-server BACKEND_HOST=0.0.0.0 BACKEND_PORT=3000
67+
```
68+
69+
The mobile app will:
70+
71+
- Call APIs via POST `/ipc/<channel>` with `Authorization: Bearer <token>`
72+
- Subscribe to workspace events via WebSocket `/ws?token=<token>`
73+
74+
## Features
75+
76+
- Real-time chat interface with streaming responses
77+
- **Message editing**: Long press user messages to edit (truncates history after edited message)
78+
- Provider configuration (Anthropic, OpenAI, etc.)
79+
- Project and workspace management
80+
- Secure credential storage
81+
82+
## Architecture
83+
84+
- **expo-router** for file-based routing
85+
- **@tanstack/react-query** for server state
86+
- **WebSocket** for live chat streaming
87+
- Thin fetch/WS client in `src/api/client.ts`
88+
89+
## Troubleshooting
90+
91+
**"TurboModuleRegistry" errors in Expo Go**: This happens because Expo Go SDK 54 doesn't include all native modules. Build a development build instead:
92+
93+
```bash
94+
bunx expo prebuild --clean
95+
bunx expo run:ios # or run:android
96+
```
97+
98+
**Version mismatch**: Ensure Expo Go is SDK 54 (check App Store/Play Store for latest).
99+
100+
**Connection refused**: Make sure the mux server is running and accessible from your device (use your machine's Tailscale IP or local network IP, not `localhost`).

mobile/app.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"expo": {
3+
"name": "mux-mobile",
4+
"slug": "mux-mobile",
5+
"version": "0.0.1",
6+
"scheme": "mux",
7+
"orientation": "portrait",
8+
"platforms": ["ios", "android"],
9+
"newArchEnabled": true,
10+
"jsEngine": "hermes",
11+
"experiments": {
12+
"typedRoutes": true
13+
},
14+
"extra": {
15+
"mux": {
16+
"baseUrl": "http://100.114.78.86:3000",
17+
"authToken": ""
18+
}
19+
},
20+
"plugins": ["expo-router", "expo-secure-store"],
21+
"ios": {
22+
"bundleIdentifier": "com.coder.mux-mobile"
23+
}
24+
}
25+
}

mobile/app/_layout.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type { JSX } from "react";
2+
import { Stack } from "expo-router";
3+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4+
import { useMemo } from "react";
5+
import { SafeAreaProvider } from "react-native-safe-area-context";
6+
import { StatusBar } from "expo-status-bar";
7+
import { View } from "react-native";
8+
import { ThemeProvider, useTheme } from "../src/theme";
9+
10+
function AppFrame(): JSX.Element {
11+
const theme = useTheme();
12+
13+
return (
14+
<>
15+
<StatusBar style={theme.statusBarStyle} animated />
16+
<View style={{ flex: 1, backgroundColor: theme.colors.background }}>
17+
<Stack
18+
screenOptions={{
19+
headerStyle: { backgroundColor: theme.colors.surfaceSunken },
20+
headerTintColor: theme.colors.foregroundPrimary,
21+
headerTitleStyle: {
22+
fontWeight: theme.typography.weights.semibold as any,
23+
fontSize: theme.typography.sizes.titleSmall,
24+
color: theme.colors.foregroundPrimary,
25+
},
26+
headerShadowVisible: false,
27+
contentStyle: { backgroundColor: theme.colors.background },
28+
}}
29+
>
30+
<Stack.Screen
31+
name="index"
32+
options={{
33+
title: "Workspaces",
34+
}}
35+
/>
36+
<Stack.Screen
37+
name="workspace/[id]"
38+
options={{
39+
title: "", // Title set dynamically by WorkspaceScreen
40+
headerBackTitle: "", // Just show <, no text
41+
}}
42+
/>
43+
<Stack.Screen
44+
name="settings"
45+
options={{
46+
title: "Settings",
47+
headerBackTitle: "", // Just show <, no text
48+
}}
49+
/>
50+
</Stack>
51+
</View>
52+
</>
53+
);
54+
}
55+
56+
export default function RootLayout(): JSX.Element {
57+
const client = useMemo(
58+
() =>
59+
new QueryClient({
60+
defaultOptions: {
61+
queries: {
62+
staleTime: 30_000,
63+
refetchOnWindowFocus: false,
64+
},
65+
},
66+
}),
67+
[]
68+
);
69+
70+
return (
71+
<QueryClientProvider client={client}>
72+
<SafeAreaProvider>
73+
<ThemeProvider>
74+
<AppFrame />
75+
</ThemeProvider>
76+
</SafeAreaProvider>
77+
</QueryClientProvider>
78+
);
79+
}

mobile/app/index.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { JSX } from "react";
2+
import { Stack, useRouter } from "expo-router";
3+
import { Pressable } from "react-native";
4+
import { Ionicons } from "@expo/vector-icons";
5+
import ProjectsScreen from "../src/screens/ProjectsScreen";
6+
7+
export default function ProjectsRoute(): JSX.Element {
8+
const router = useRouter();
9+
10+
return (
11+
<>
12+
<Stack.Screen
13+
options={{
14+
headerRight: () => (
15+
<Pressable
16+
onPress={() => router.push("/settings")}
17+
style={{ paddingHorizontal: 12 }}
18+
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
19+
>
20+
<Ionicons name="settings-outline" size={24} color="#fff" />
21+
</Pressable>
22+
),
23+
}}
24+
/>
25+
<ProjectsScreen />
26+
</>
27+
);
28+
}

0 commit comments

Comments
 (0)