Skip to content

Conversation

@ahundt
Copy link
Contributor

@ahundt ahundt commented Dec 7, 2025

feat: New Session Page with Profile System and Alternative AI Backend Support

companion to slopus/happy-cli#107 (replaces slopus/happy-cli#86)

Motivation

Users want to use Happy with alternative AI backends (OpenAI, DeepSeek, Anthropic direct API) and configure environment variables from their mobile device. Previously, all AI configuration required manual CLI setup. This PR enables:

  • Switch Claude Code backends e.g. anthropic, z.ai's GLM4.6, and deepseek on claude code, OpenAI Codex, with microsoft and openai backends from your phone.
  • Secure API key management - Configure API keys once in a profile, sync to CLI automatically
  • Create sessions on your phone, continue on your desktop terminal - tmux integration keeps sessions alive when switching devices
  • No more CLI fumbling - Savable / selectable session profiles, Permission modes, working directories, all with pre-set reasonable defaults you don't have to change unless you want a specially configured setup.

New Session Wizard

One tap profiles to select different cli tools and ai backends, tap once then type your message and hit send!

happy-new-session-0-select-profile-2025-12-07 happy-new-session-1-dir-2025-12-07

Editable Profiles

happy-new-session-3-edit-profile-2025-12-07 happy-new-session-4-edit-profile-2025-12-07

Key Features (Priority Order)

1. Profile System with Alternative AI Backend Support

  • Built-in profiles for Claude, OpenAI, DeepSeek, Gemini, Azure, and more
  • Custom profile creation with environment variable configuration
  • Cross-device profile sync with end-to-end encryption
  • Profile compatibility warnings when CLI lacks required tools

2. Environment Variable Configuration

  • New EnvironmentVariableCard and EnvironmentVariablesList components
  • Security-aware display: shows ${VAR} syntax without exposing secrets
  • Bash parameter expansion support: ${VAR:-default} syntax
  • Retrieve actual values from remote machine for verification

3. tmux Session Integration (when tmux is available)

  • Enable/disable tmux per session with checkbox
  • Defaults to first existing session when name is empty
  • Session persistence across device switches

4. Agent-Specific Permission Modes

  • Permission mode selector in wizard (plan, default, auto, yolo)
  • Auto-reset permission mode when switching between Claude/Codex
  • Cross-agent permission mapping for compatibility

5. Working Directory Selection

  • Inline path selector with search, favorites, and recent history
  • Collapsible sections for All/Recent/Favorites
  • Machine online status indicators

6. CLI Detection & Availability Warnings

  • Automatic non-blocking CLI detection per machine
  • Dismissible warnings (per-machine or global)
  • Profile compatibility indicators based on available CLIs

7. Internationalization

  • 900+ new translation strings across 7 languages (en, ru, pl, es, pt, ca, zh-Hans)
  • Complete coverage for new wizard and profile management UI

8. UI/UX Polish

  • Session view header no longer disappears on desktop resize
  • SearchableListSelector border radius consistency
  • Theme spacing/sizing improvements
  • Sticky AgentInput at bottom of wizard

Screenshots

Stats

  • 158 commits | 54 files changed | +9,636 / -879 lines

Related

  • Current Companion CLI PR: happy-cli#107 (provides backend support for profiles and tmux)
  • Outdated Companion CLI PR: happy-cli#86 (provides backend support for profiles and tmux)

denysvitali and others added 30 commits September 23, 2025 12:57
- Fix YOLO mode not persisting across app restarts and agent type changes
- Add comprehensive profile management system for environment variables
- Support ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_MODEL, and TMUX_SESSION_NAME
- Add profile selection UI in settings and session creation flows
- Extend session spawning to accept environment variables from profiles
- Add translations for profiles feature in English, Spanish, Russian, and Polish
- Include proper mode mapping between Claude and Codex permission systems

Resolves:
- GitHub issue slopus#206: YOLO mode persistence bug
- GitHub issue slopus#204: Profile management and environment variable support
…riables

Based on tmux best practices research, add comprehensive tmux environment variable support:

- Add TMUX_TMPDIR support for custom socket directories
- Add tmuxUpdateEnvironment toggle for automatic environment updates
- Enhance profile schema with additional tmux-specific options
- Update session spawning to properly pass TMUX_TMPDIR environment variable
- Add comprehensive UI for configuring tmux options in profile management
- Include translations for new tmux options in all supported languages
- Improve profile display to show tmux directory configuration

Technical improvements:
- Follow tmux native environment variable patterns
- Support both custom session names and socket directories
- Enable automatic environment variable updates per session
- Maintain backward compatibility with existing profiles

This provides users with full control over tmux session configuration
as requested in GitHub issue slopus#204.
…agement

- Fix TypeScript configuration with proper expo/tsconfig.base.json reference
- Resolve Modal.alert TypeScript conflicts by using Alert directly
- Update settings parsing to preserve unknown fields while maintaining validation
- Fix all settings tests to accommodate new profile management fields
- Add comprehensive type interfaces for Modal class to prevent conflicts
- Ensure complete TypeScript compliance with zero errors

Previous behavior: TypeScript errors prevented compilation due to Modal type conflicts
What changed: Fixed type definitions, updated configuration, resolved test expectations
Why: To ensure production-ready code with full type safety and no regressions

Files affected:
- sources/app/(app)/settings/profiles.tsx: Fixed Modal.alert usage
- sources/modal/*: Added explicit type interfaces
- sources/sync/settings.ts: Enhanced parsing logic
- sources/sync/settings.spec.ts: Updated test expectations
- sources/text/translations/*: Added missing profile keys
- tsconfig.json: Fixed base configuration

Testable: All TypeScript checks pass, settings tests pass, profile management complete
- Remove TODO comments from profiles.tsx (replaced with proper validation)
- Add comprehensive comments for tmux environment variables in ops.ts
- Add detailed architecture documentation for translation file structure
- Implement profile validation (empty names and duplicate name prevention)
- Improve inline documentation for maintainability

Previous behavior: TODO comments and incomplete validation
What changed: Added proper validation, comprehensive documentation, cleaner code
Why: To meet maintainer standards and improve long-term maintainability

Files affected:
- sources/app/(app)/settings/profiles.tsx: Added validation and removed TODOs
- sources/sync/ops.ts: Added detailed environment variable documentation
- sources/text/translations/en.ts: Added architecture documentation

Testable: Profile validation works, no TypeScript errors, functionality preserved
- Add useProfileMap hook for O(1) profile lookup using Map instead of array.find()
- Create transformProfileToEnvironmentVars helper to eliminate repetitive code
- Replace linear profile search with optimized Map-based lookup in session creation
- Improve performance for users with many profiles while maintaining functionality

Previous behavior: O(n) profile lookup with repetitive environment variable mapping
What changed: O(1) profile lookup with clean transformation helper
Why: Performance optimization for scalability and code maintainability

Files affected:
- sources/app/(app)/new/index.tsx: Added optimized lookup and transformation utilities

Testable: All settings tests pass, TypeScript compliance verified, no regressions
- Add built-in profiles (Anthropic, DeepSeek, Z.AI) with pre-configured settings
- Implement profile selector in AgentInput settings overlay
- Add comprehensive profile management in settings screen
- Support custom environment variables beyond predefined fields
- Enable built-in profile editing with configurable names
- Add O(1) profile lookup optimization using Map
- Integrate profile environment variables into session creation
- Support arbitrary custom environment variables with add/remove UI

Previous behavior: No profile system existed, users had to manually configure environment variables each session
What changed: Complete profile management system with built-in configurations and custom profile support
Why: Streamlines user workflow by saving common configurations and reducing repetitive setup

Files affected:
- sources/app/(app)/new/index.tsx: Add profile interface, built-in profiles, and session creation integration
- sources/components/AgentInput.tsx: Integrate profile selector into settings overlay with optimized lookup
- sources/app/(app)/settings/profiles.tsx: Complete profile management UI with custom environment variables

Note: WIP implementation - needs CLI synchronization and persistence validation
…riendly tmux integration

Summary: Add comprehensive AI backend profile system with agent compatibility validation, environment variable management, and seamless tmux session integration across happy app and happy-cli.

Previous behavior:
- Flat profile configuration with basic Anthropic settings only
- Manual environment variable filtering in daemon
- Limited TypeScript support in tmux utilities
- No profile versioning or compatibility validation

What changed:
- sources/sync/settings.ts: Replaced simple schema with comprehensive Zod-validated AIBackendProfileSchema supporting multiple AI providers (Anthropic, OpenAI, Azure, Together AI) with nested configurations, compatibility flags, and versioning
- sources/app/(app)/new/index.tsx: Updated built-in profiles to use new schema structure, added agent compatibility validation with user-friendly warnings, integrated profile-based environment variable filtering
- sources/app/(app)/settings/profiles.tsx: Migrated profile management UI to use new schema, updated form handling for nested configuration objects
- sources/components/AgentInput.tsx: Removed legacy profile reference in favor of unified schema

Why:
- Enable support for multiple AI providers beyond just Anthropic
- Provide type-safe profile management across both happy app and happy-cli
- Add automatic environment variable filtering based on agent compatibility
- Implement proper versioning for future profile schema migrations
- Improve tmux integration with strong typing and validation

Files affected:
- sources/sync/settings.ts: Core profile schema and helper functions
- sources/app/(app)/new/index.tsx: New session creation with profile selection
- sources/app/(app)/settings/profiles.tsx: Profile management interface
- sources/components/AgentInput.tsx: Removed legacy profile reference

Testable:
- Create new sessions with different AI provider profiles (anthropic, deepseek, zai, openai, azure-openai, together)
- Verify profile compatibility warnings appear when using incompatible profiles
- Confirm environment variables are properly filtered by agent type
- Test profile management in settings with new nested configuration structure
…nt fallbacks

Summary: Replace reactive error handling with proactive profile filtering that prevents incompatible selections by showing only compatible profiles for the current agent type and automatically selecting compatible defaults when switching agents.

Previous behavior:
- Users could select incompatible profiles for their chosen agent type
- Agent switching showed warning dialogs after making bad selections
- Reactive error handling with automatic profile switching as fallbacks
- Profile picker showed all profiles regardless of agent compatibility

What changed:
- sources/app/(app)/new/index.tsx: Added compatibleProfiles useMemo that filters profiles by agent type using validateProfileForAgent, added isCurrentProfileCompatible validation for UI feedback, enhanced handleAgentClick to proactively select compatible built-in profiles when current profile is incompatible, added compatibleProfiles and isCurrentProfileCompatible props to AgentInput component
- sources/components/AgentInput.tsx: Extended AgentInputProps interface with compatibleProfiles and isCurrentProfileCompatible fields for UI filtering support

Why: Prevents user errors by design rather than handling them after the fact. The previous approach relied on fallbacks and warnings, indicating poor UX design. The new approach prevents incompatible selections at the UI level, eliminating the need for complex error recovery logic.

Files affected:
- sources/app/(app)/new/index.tsx: Profile filtering logic and agent switching improvements
- sources/components/AgentInput.tsx: Props extension for compatibility filtering support

Testable:
- Switch between Claude and Codex agents - incompatible profiles should be filtered from picker
- Select "OpenAI (GPT-5)" profile with Claude agent - should automatically switch to "Anthropic (Default)"
- All profiles in picker should be compatible with current agent type
- No warning dialogs should appear when switching agents (prevention vs. recovery)
… system

Replaces the AgentInput-based new session interface with a multi-step wizard that guides users through session creation with proper profile management and AI backend selection.

Previous behavior: Single input interface with minimal configuration options and reactive error handling with fallbacks.

What changed:
- Implemented multi-step wizard (welcome → ai-backend → tmux-config → session-details → creating)
- Added built-in AI profiles for Anthropic, DeepSeek, Z.AI, OpenAI, Azure OpenAI, and Together AI
- Added profile creation workflow with custom profile support
- Integrated AI backend compatibility validation (Claude vs Codex)
- Added profile filtering to prevent incompatible selections
- Preserved yolo and permission mode persistence from original implementation
- Enhanced environment variable handling with agent-specific filtering

Why: Provides a guided, user-friendly session creation experience while maintaining the robust profile system and yolo persistence fixes. The wizard prevents configuration errors through proactive filtering rather than reactive fallbacks.

Files affected:
- sources/app/(app)/new/index.tsx: Complete wizard implementation replacing AgentInput interface
- sources/sync/settings.ts: Added profile schema, validation, and environment variable helpers

Testable: Navigate to /new and verify wizard guides through profile selection, AI backend choice, and session creation with proper error handling and settings persistence.
Adds API schemas for profile synchronization across clients and fixes RevenueCat type exports to properly separate runtime values from types.

Changes:
- ApiUpdateProfileSchema: Profile update events for cross-client sync
- ApiDeleteProfileSchema: Profile deletion events
- ApiActiveProfileSchema: Active profile change events
- Fixed RevenueCat exports to separate types from runtime enums

Files affected:
- sources/sync/apiTypes.ts: Added profile synchronization schemas
- sources/sync/revenueCat/index.ts: Fixed type vs value exports
Previous behavior: Wizard only handled basic session configuration without profile support.
Users could not configure AI backend providers or API keys through the interface.

What changed:
- sources/components/NewSessionWizard.tsx: Added complete profile-centric wizard flow with 6 built-in profiles (Anthropic, DeepSeek, OpenAI, Azure OpenAI, Z.ai, Microsoft Azure)
- sources/sync/settings.ts: Added comprehensive profile schema with validation, environment variable mapping, and compatibility checking
- sources/app/(app)/new/index.tsx: Updated session creation to accept profileId and environmentVariables from wizard
- sources/sync/ops.ts: Added environmentVariables to SpawnSessionOptions interface and RPC calls
- sources/text/_default.ts, sources/text/translations/en.ts: Added complete translation support for profile management

Why: Enable users to configure AI backend providers through a user-friendly wizard interface
while maintaining seamless CLI integration and proper environment variable management.

Files affected:
- sources/components/NewSessionWizard.tsx: Complete profile wizard with conditional configuration steps
- sources/sync/settings.ts: Profile schema, validation, and environment variable mapping functions
- sources/app/(app)/new/index.tsx: Session creation integration with profile data
- sources/sync/ops.ts: RPC interface updates for environment variable support
- sources/text/_default.ts: Profile management translations
- sources/text/translations/en.ts: Dedicated English translation file

Testable: Wizard profile selection → API key configuration → session creation with proper environment variable flow
…ization

WIP - Added complete profile CRUD operations with bidirectional sync between GUI and CLI

Major changes:
- Enhanced NewSessionWizard with dual-action profile cards (Use As-Is, Edit)
- Implemented profile management operations (Create, Duplicate, Delete)
- Added ProfileSyncService for bidirectional GUI-CLI synchronization
- Profiles sync to ~/.happy/settings.json with proper conflict resolution
- Added manual configuration option with CLI environment variable support
- Enhanced profile compatibility checking for claude/codex agents
- Added progressive disclosure for management actions (custom profiles only)

Profile Management Features:
- Profile creation with auto-generated UUIDs
- Profile duplication with customizable naming
- Profile deletion with confirmation dialogs
- Built-in profile protection (delete/edit restrictions)
- Active profile synchronization between GUI and CLI

GUI-CLI Integration:
- Direct file-based sync to CLI settings file
- Graceful fallback when CLI unavailable
- Profile schema consistency across systems
- Environment variable precedence handling

Next: Implement actual CLI RPC integration and test end-to-end workflow
SECURITY CRITICAL FIX - Removed all direct file access operations that bypassed Happy's encryption system

Major security fixes:
- Removed direct file access to ~/.happy/settings.json (SECURITY VIOLATION)
- Replaced file operations with proper Happy settings infrastructure usage
- Fixed storage import to use official Happy storage system
- Added security warnings to prevent future direct file access
- Maintained all ProfileSyncService API compatibility

Fixed Methods:
- syncGuiToCli(): Now uses Happy settings instead of direct file writes
- syncCliToGui(): Now reads from Happy settings instead of direct file reads
- setActiveProfile(): Now uses sync.applySettings() instead of file manipulation
- getActiveProfile(): Now uses storage.getState() instead of file access

Security Impact:
- ELIMINATED risk of corrupting CLI settings during daemon operations
- RESTORED proper encryption channel usage for all profile data
- FIXED potential race conditions with CLI daemon file access
- ENSURED all profile operations use Happy's security infrastructure

Technical Details:
- Profiles continue to work through existing Happy settings sync system
- CLI daemon accesses profiles through established channels
- No breaking changes to ProfileSyncService API
- All functionality preserved while eliminating security risks

This addresses a critical security vulnerability where profile management was bypassing
Happy's established encryption and sync infrastructure.
BREAKING FIX: Profile ID Generation
- Replace Date.now() timestamps with proper UUIDs using expo-crypto
- Fixes schema validation errors that would crash on profile creation
- Affected locations:
  * sources/app/(app)/new/index.tsx:628
  * sources/app/(app)/settings/profiles.tsx:113
  * sources/app/(app)/settings/profiles.tsx:180

UX FIX: Remove Empty Tmux Wizard Step
- Remove placeholder tmux-config step from wizard flow
- Simplify navigation: welcome → ai-backend → session-details
- Users configure tmux in profile settings instead
- Update step numbering accordingly

Files changed:
- sources/app/(app)/new/index.tsx
- sources/app/(app)/settings/profiles.tsx

Related to profile management and yolo mode persistence feature.
Addresses critical bugs identified in cross-repository validation.

Co-authored-by: Claude (Anthropic AI Assistant)
SETTINGS SYNC:
- Add schema version detection (SUPPORTED_SCHEMA_VERSION = 2)
- Implement update-account settings handler in sync.ts
- Add version mismatch warnings for user awareness
- Settings now sync in real-time across GUI instances

CONFIRMATION DIALOGS:
- Add confirmation dialog before profile deletion
- Prevents accidental data loss from quick taps

TRANSLATIONS:
- Add profiles.delete translations for 7 languages
- Support: English, Spanish, Catalan, Polish, Portuguese, Russian, Chinese

BACKWARDS COMPATIBILITY:
- Old clients (v1) ignore new profile fields gracefully
- New clients handle old settings with defaults
- Zero breaking changes to existing functionality
- Graceful degradation for all scenarios

FILES MODIFIED:
- sources/sync/settings.ts (schema version constant and defaults)
- sources/sync/sync.ts (update-account handler with decryption)
- sources/app/(app)/settings/profiles.tsx (Alert confirmation dialog)
- sources/text/translations/*.ts (7 language files)

TECHNICAL DETAILS:
- Schema version v1 → v2 migration automatic
- Settings decrypted, parsed, and validated on sync
- Console warnings for schema version mismatches
- Error handling prevents crashes on sync failures

Tested scenarios:
- ✅ Settings sync across multiple GUI instances
- ✅ Confirmation dialog before profile deletion
- ✅ Schema version mismatch warnings
- ✅ Backwards compatibility with v1 settings

Related to yolo-mode-persistence and profile management feature
Builds on previous UUID and wizard fixes (commit 6807055)
…ith cross-env

Previous behavior:
- Single build configuration determined by APP_ENV environment variable
- Manual environment variable setting required before running builds
- No npm scripts for variant-specific builds
- Developers had to remember to set APP_ENV=development/preview/production

What changed:
- package.json: Added 9 new variant-specific build scripts
  - ios:dev/preview/production for iOS builds
  - android:dev/preview/production for Android builds
  - start:dev/preview/production for development server
- Added cross-env dependency for cross-platform environment variable support
- USAGE.md (new): Complete 250-line guide covering all three variants

Why:
- Enables easy switching between development, preview, and production builds
- All three variants can be installed simultaneously (different bundle IDs)
- Cross-platform support via cross-env (Windows/macOS/Linux)
- Discoverable commands in package.json reduce errors
- Aligns with CLI variant system for consistent development workflow
- Prevents accidentally building wrong variant

Files affected:
- package.json: Added 9 variant build scripts, added cross-env@^10.1.0 devDependency
- yarn.lock: Updated with cross-env and @epic-web/invariant dependencies
- USAGE.md (new): Comprehensive guide with workflows, troubleshooting, and examples

Testable:
- npm run ios:dev builds "Happy (dev)" with bundle ID com.slopus.happy.dev
- npm run ios:preview builds "Happy (preview)" with bundle ID com.slopus.happy.preview
- npm run ios:production builds "Happy" with bundle ID com.ex3ndr.happy
- All three apps can be installed on same device simultaneously
- Each variant connects to different CLI daemon instances
… documentation

Previous behavior:
- No macOS desktop app variant build system
- Single tauri.conf.json for all builds
- Development documentation mixed in user-facing files
- No CONTRIBUTING.md file following GitHub conventions

What changed:
- src-tauri/tauri.dev.conf.json (new): Partial config overriding productName to "Happy (dev)" and identifier to com.slopus.happy.dev
- src-tauri/tauri.preview.conf.json (new): Partial config overriding productName to "Happy (preview)" and identifier to com.slopus.happy.preview
- src-tauri/tauri.conf.json (unchanged): Remains production base config
- package.json: Added 4 Tauri variant build scripts using official --config flag
  - tauri:dev uses --config src-tauri/tauri.dev.conf.json
  - tauri:build:dev/preview/production for variant builds
- USAGE.md → CONTRIBUTING.md: Renamed and expanded with Tauri documentation
- CONTRIBUTING.md: Added macOS Tauri variant section explaining JSON Merge Patch approach
- CONTRIBUTING.md: Added preview vs dev vs production use case explanations
- README.md: Added CONTRIBUTING.md link in Documentation section

Why:
- Enables installing all three macOS desktop app variants simultaneously in /Applications
- Uses official Tauri v2 --config flag and JSON Merge Patch (RFC 7396)
- Partial configs follow DRY principle - only specify differences from base
- Aligns macOS variant system with existing iOS/Android mobile variants
- Follows GitHub convention with CONTRIBUTING.md for developer documentation
- No breaking changes - existing tauri dev and tauri build commands still work
- Backward compatible - base tauri.conf.json remains unchanged

Files affected:
- src-tauri/tauri.dev.conf.json (new, 12 lines): Dev variant partial config
- src-tauri/tauri.preview.conf.json (new, 12 lines): Preview variant partial config
- package.json: Added 4 Tauri build scripts with --config flag
- USAGE.md → CONTRIBUTING.md: Renamed with 41 additional lines for macOS/Tauri
- README.md: Added CONTRIBUTING.md link (1 line)

Testable:
- tauri dev (direct command) still uses base tauri.conf.json with com.slopus.happy
- npm run tauri:dev launches "Happy (dev)" with identifier com.slopus.happy.dev
- npm run tauri:build:preview builds "Happy (preview)" with com.slopus.happy.preview
- npm run tauri:build:production builds "Happy" with com.slopus.happy
- All three .app bundles can be installed in /Applications simultaneously
- Existing developer workflows using direct tauri commands remain unchanged
Previous behavior:
- apiTypes.ts defined ApiUpdateProfileSchema, ApiDeleteProfileSchema, ApiActiveProfileSchema (lines 151-169)
- These schemas added to ApiUpdateSchema discriminated union (lines 184-186)
- Exported types ApiUpdateProfile, ApiDeleteProfile, ApiActiveProfile (lines 192-194)
- Zero usage across entire codebase - profiles sync via Account.settings blob instead
- Dead code causing potential confusion about profile sync architecture

What changed:
- sources/sync/apiTypes.ts: Removed unused profile-specific update schemas
  - Removed ApiUpdateProfileSchema (lines 151-158)
  - Removed ApiDeleteProfileSchema (lines 160-163)
  - Removed ApiActiveProfileSchema (lines 165-169)
  - Removed from ApiUpdateSchema union (lines 184-186)
  - Removed type exports (lines 192-194)
- Net change: -26 lines of dead code

Why:
- Profiles actually sync via Account.settings encrypted blob using ApiUpdateAccountSchema
- Per-profile update events were designed but never implemented
- Settings-based sync is simpler and already working
- Removing dead code prevents future confusion about sync architecture
- Reduces bundle size minimally

Files affected:
- sources/sync/apiTypes.ts: Removed 26 lines of unused profile schemas (lines 151-169, 184-186, 192-194)

Testable:
- yarn typecheck passes (no TypeScript errors)
- No references to ApiUpdateProfileSchema anywhere in sources/
- Profile creation and sync still works via Account.settings
- No case 'update-profile' handlers exist in sync.ts
Previous behavior:
- sources/app/(app)/_layout.tsx line 319: presentation: 'modal'
- New session wizard appeared as slide-up modal overlay from bottom
- Wizard did not appear in main content area where sessions display
- Inconsistent with user expectation of inline workflow

What changed:
- sources/app/(app)/_layout.tsx: Removed presentation: 'modal' from new/index screen (line 319)
- Wizard now uses default stack navigation (card presentation)
- Appears in same main panel as session message interface

Why:
- User expects wizard in main content area, not as overlay
- Consistent with session display pattern
- Standard stack navigation provides better UX for multi-step wizards
- Matches pattern of other screens in app (settings, session details, etc.)

Files affected:
- sources/app/(app)/_layout.tsx: Removed presentation: 'modal' from new/index screen

Testable:
- Tap "New Session" button → wizard appears in main content area (not slide-up overlay)
- Wizard navigation feels consistent with rest of app
- Back button navigates correctly within wizard and to previous screen
Integrate wizard infrastructure by Denys Vitali while preserving complete
inline wizard implementation from fix branch.

**Primary Contributors:**
- Denys Vitali: Initial wizard implementation (commit 36ad094)
- Andrew Hundt: Profile management and CLI synchronization

**Merged from Feature Branch:**
- sources/components/NewSessionWizard.tsx: Reference wizard component
- sources/sync/profileSync.ts: Profile CLI sync service
- sources/components/AgentInput.tsx: Cleaner version without duplicate profile code

**Preserved from Fix Branch:**
- sources/app/(app)/new/index.tsx: Complete inline wizard (1048 lines)
- sources/app/(app)/new/pick/path.tsx: Path selection UI with recent paths
- sources/app/(app)/new/pick/machine.tsx: Machine selection UI
- sources/sync/ops.ts: Detailed environment variable type definitions
- sources/sync/settings.ts: Complete profile schema
- sources/text/translations/en.ts: All translations

**Conflict Resolution:**
- new/index.tsx: Kept fix/yolo complete inline implementation
- AgentInput.tsx: Used feature/yolo cleaner version
- ops.ts, settings.ts, en.ts: Kept fix/yolo complete implementations
- path.tsx: Restored (was mistakenly deleted by feature branch)

**Next Step:**
Convert multi-step wizard to single-page panel design per user requirements.
Document complete design requirements for single-page wizard:
- Settings panel style for configuration
- Session panel style for prompt + create button
- Profile selection first with create/edit/delete
- All features visible on one page
- Create button greyed until valid

See notes/2025-11-16-wizard-merge-and-refactor-plan.md for complete specification.
Add actionable specifications with file paths, line numbers, and code snippets:
- AgentInput component interface and props (SessionView.tsx:276 usage reference)
- Exact lines to delete (WizardStep type, navigation functions, renderStepContent)
- Content extraction mapping (which step content becomes which section)
- Profile utilities extraction plan (DRY - remove duplication)
- Validation logic with code examples
- Picker screen integration strategy (keep valuable recent paths UI)

All details CLAUDE.md concrete: specific files, functions, classes, line numbers.
…rkflows

Corrections:
- Lines 30-40, 647-671, 673-681: KEEP callbacks and picker handlers (not delete)
- AgentInput is controlled component (needs value prop, not self-managing)
- Add missing profile edit modal requirements (full editor with env vars, tmux)
- Add complete ProfileEditForm reference (settings/profiles.tsx:481-989)

Added end-to-end workflows:
- Profile creation/edit workflow with modal and settings sync
- Session creation workflow with env var transformation
- Picker integration workflow with callbacks

All logic errors fixed, workflows documented, ready for implementation.
Document support for variable substitution in profile env vars:
- Literal values: API_TIMEOUT_MS='600000'
- Variable references: ANTHROPIC_AUTH_TOKEN='${Z_AI_AUTH_TOKEN}'
- References resolve on target machine (daemon/CLI side)
- GUI stores templates, daemon performs substitution

Examples from user:
- Anthropic: unset env vars, use defaults
- Z.AI: Reference Z_AI_* variables
- DeepSeek: Reference DEEPSEEK_* variables with multiple mappings

ProfileEditForm (settings/profiles.tsx:786-943) already implements env var
editor with key-value pairs. Template strings passed to daemon which resolves
variables on target machine before spawning Claude/Codex process.
Summary: Eliminate code duplication by moving built-in profile definitions to shared module

Previous behavior (based on git diff):
- new/index.tsx:43-187 defined getBuiltInProfile() with 6 provider configs (Anthropic, DeepSeek, Z.AI, OpenAI, Azure, Together)
- new/index.tsx:156-187 defined DEFAULT_PROFILES constant with profile metadata
- settings/profiles.tsx:27-43 defined duplicate DEFAULT_PROFILES (only 3 providers)
- settings/profiles.tsx:46-100 defined duplicate getBuiltInProfile() (only 3 providers)
- Total duplication: 221 lines across 2 files

What changed:
- Created sources/sync/profileUtils.ts (+157 lines)
  - Exported getBuiltInProfile() function with all 6 providers
  - Exported DEFAULT_PROFILES constant with all 6 providers
  - Added JSDoc comments documenting parameters and return values
- Updated sources/app/(app)/new/index.tsx (-148 lines)
  - Line 23: Added import { getBuiltInProfile, DEFAULT_PROFILES } from '@/sync/profileUtils'
  - Lines 43-187: Deleted local getBuiltInProfile() and DEFAULT_PROFILES definitions
  - Replaced with comment "// Profile utilities now imported from @/sync/profileUtils"
- Updated sources/app/(app)/settings/profiles.tsx (-77 lines)
  - Line 13: Added import { getBuiltInProfile, DEFAULT_PROFILES } from '@/sync/profileUtils'
  - Lines 27-100: Deleted local DEFAULT_PROFILES and getBuiltInProfile() definitions
  - Now has all 6 providers (was missing OpenAI, Azure, Together)

Why:
- DRY principle: Single source of truth for profile configurations
- Consistency: Both wizard and settings now use identical profile sets
- Maintainability: Changes to built-in profiles only need updating in one location
- Preparation: Sets up shared utilities needed for upcoming single-page wizard refactor

Files affected:
- sources/sync/profileUtils.ts (new file, 157 lines)
- sources/app/(app)/new/index.tsx (removed 148 duplicate lines)
- sources/app/(app)/settings/profiles.tsx (removed 77 duplicate lines, gained 3 providers)

Net change: -221 lines of duplication, +161 lines of shared code (-60 lines total)

Testable: Build compiles, profile grid shows all 6 built-in profiles in both new session wizard and settings panel
… with integrated AgentInput

Summary: Replace 4-step wizard navigation with single scrollable page using session panel's AgentInput component

Previous behavior (based on git diff):
- Multi-step wizard with 4 steps: welcome → ai-backend → session-details → creating (904 lines)
- Step navigation with Back/Next buttons controlled by currentStep state
- Separate prompt TextInput (lines 791-801) with Create Session button (lines 837-848)
- Step-by-step flow requiring multiple button clicks to create session
- Loading step showing "Creating Session" spinner
- Module-level navigation functions goToNextStep() and goToPreviousStep()

What changed:
- Deleted multi-step infrastructure (-262 lines total, now 642 lines):
  - Line 28: Deleted WizardStep type definition
  - Line 337: Deleted currentStep state and setCurrentStep
  - Lines 425-457: Deleted goToNextStep() and goToPreviousStep() navigation functions
  - Lines 640-878: Deleted renderStepContent() step rendering switch statement
  - Lines 472-501: Deleted createNewProfile() (profile creation moved to settings panel)
  - Line 409: Removed sessionPrompt.trim() validation (prompt now optional)
  - Line 555: Removed setCurrentStep('creating') (no loading step)
- Added AgentInput integration:
  - Line 24: Added import { AgentInput } from '@/components/AgentInput'
  - Lines 619-634: Integrated AgentInput at bottom with full props:
    - isSendDisabled={!canCreate} wires validation to arrow button state
    - isSending={isCreating} shows loading state during session creation
    - machineName and currentPath props show context in input area
    - agentType, permissionMode, modelMode props configure agent display
- Created single-page layout:
  - Lines 521-615: Single scrollable view with 4 numbered sections
  - Section 1 (lines 523-563): Profile grid with selection cards
  - Section 2 (lines 566-575): Machine selector button (opens picker)
  - Section 3 (lines 578-588): Path selector button (opens picker)
  - Section 4 (lines 591-614): Collapsible advanced options (experimental features)
- Updated validation logic:
  - Lines 351-357: canCreate useMemo checks profile, machine, path (prompt optional)
  - Prompt no longer required - sessions can be created without initial message
- Kept picker integration (CRITICAL - maintains UX):
  - Lines 29-39: Module-level callbacks for picker screens
  - Lines 373-406: useEffect hooks wiring onMachineSelected and onPathSelected
  - Lines 398-406: handleMachineClick and handlePathClick navigation functions
  - Picker screens (machine.tsx, path.tsx) unchanged and functional
- Updated styles:
  - Lines 139-232: Replaced wizard step styles with single-page section styles
  - Removed: stepHeader, stepNumber, buttonContainer, creatingContainer
  - Added: wizardContainer, sectionHeader, selectorButton, advancedHeader
- Session creation improvements:
  - Line 475: Made initial prompt optional (if (sessionPrompt.trim()) check)
  - Line 419: Removed "creating" step transition (setIsCreating only)
  - Direct session creation without intermediate loading UI

Why:
- User request: "wizard to appear in the same main panel as the message interface"
- Simplification: Single page instead of multi-step navigation reduces clicks
- Consistency: Reuse AgentInput component from session panel (same UX)
- Efficiency: All options visible at once - no need to navigate between steps
- Flexibility: Prompt now optional - users can create session and start typing after
- Maintainability: -262 lines of navigation code, simpler state management

Files affected:
- sources/app/(app)/new/index.tsx (-262 lines: 904 → 642 lines)

Technical implementation:
- AgentInput controlled component: value={sessionPrompt}, onChangeText={setSessionPrompt}
- Validation: canCreate=true enables arrow button via isSendDisabled={!canCreate}
- Picker callbacks: Module-level callbacks maintained for picker screen integration
- State preserved: All session configuration state (profile, machine, path, modes) unchanged
- Environment variables: Profile-to-env transformation logic unchanged (lines 50-89)

Testable:
- Build compiles without TypeScript errors
- New session button navigates to wizard
- Single-page wizard appears (no step transitions)
- All 6 built-in profiles render in grid
- Machine/path picker buttons open respective pickers
- AgentInput arrow button greyed when fields invalid
- AgentInput arrow button active when profile+machine+path selected
- Session creation works with profile environment variables
- Optional prompt: Can create session without typing initial message
Path picker screen exists at sources/app/(app)/new/pick/path.tsx but was missing route definition in _layout.tsx, causing navigation error when clicking path selector button in wizard.

Files affected:
- sources/app/(app)/_layout.tsx: Added Stack.Screen for new/pick/path route (lines 307-313)

Testable: Click path selector button in new session wizard → path picker opens
ahundt added 5 commits January 8, 2026 00:00
…l three agents

Previous behavior:
- Agent type loaded from storage without checking CLI availability
- lastUsedAgent could be 'codex' even when codex not installed
- Profile selection auto-switched agent without CLI validation
- Agent persistence in setState caused race condition
- isProfileAvailable used hardcoded logic for only claude/codex
- selectProfile only handled claude/codex, ignored gemini
- Missing gemini support in dismissedCLIWarnings schema

What changed:
- new/index.tsx:334-338: Move sync.applySettings to separate useEffect (fixes race)
- new/index.tsx:421-441: Add useEffect to auto-correct agent after detection completes
- new/index.tsx:503-508: Use Object.entries for scalable agent checking (all 3 agents)
- new/index.tsx:518-521: Use Object.entries for requiredCLI (supports gemini now)
- new/index.tsx:641-653: Use Object.entries in selectProfile (handles all 3 agents)
- new/index.tsx:917-929: Add handlePathClick handler for Control A layout
- new/index.tsx:1336-1408: Add gemini CLI warning banner
- useCLIDetection.ts:4-11: Add gemini to CLIAvailability interface
- useCLIDetection.ts:63-67: Detect all three CLIs in single command
- useCLIDetection.ts:74-93: Parse claude, codex, gemini results
- useCLIDetection.ts:94-119: Conservative fallback (null on error, not optimistic)
- apiSocket.ts:145-148: Clean up RPC response handling
- settings.ts:293-301: Add gemini to dismissedCLIWarnings schema

Why:
- Prevents silent spawn failures when unavailable agent selected
- Scales automatically to N agents using Object.entries pattern
- Gemini now fully supported alongside claude and codex
- Conservative error handling prevents false positives

Files affected:
- sources/app/(app)/new/index.tsx: Agent validation, profile logic, gemini banner
- sources/hooks/useCLIDetection.ts: Three-agent detection with conservative fallback
- sources/sync/apiSocket.ts: Clean RPC response handling
- sources/sync/settings.ts: Gemini dismissal tracking

Testable:
- Open new session wizard with lastUsedAgent='codex' but codex not installed
- Agent auto-switches to 'claude', console shows [AgentSelection] message
- Claude profiles enabled, codex profiles greyed with warning
- Agent cycling includes gemini when experiments enabled
Previous behavior:
- AgentInput showed only claude and codex CLI status next to "online"
- connectionStatus.cliStatus object had only claude and codex properties
- cliStatus type definition missing gemini property
- No display block for gemini status indicators

What changed:
- new/index.tsx:1044: Add gemini to cliStatus object (experiments-gated)
- new/index.tsx:1047: Add experimentsEnabled to connectionStatus dependencies
- AgentInput.tsx:51: Add gemini as optional property to cliStatus type
- AgentInput.tsx:705-726: Add gemini display block with conditional check

Why:
- Completes gemini support in new session wizard CLI status display
- Consistent with Variant B status box which already shows all 3 agents
- Respects experiments feature flag (gemini only when enabled)
- Backward compatible (optional type + conditional rendering)

Files affected:
- sources/app/(app)/new/index.tsx: Add gemini to cliStatus data
- sources/components/AgentInput.tsx: Add gemini to type and display

Testable:
- Enable experiments flag in settings
- Open new session wizard
- Check message panel status line shows: "online ✓ claude ✓ codex ✓ gemini"
- Disable experiments, should show only claude and codex
Summary: Merges 9 commits from main while preserving ALL feature branch functionality.

Main additions (preserved):
- Draft persistence: Session wizard state survives navigation (saveNewSessionDraft/loadNewSessionDraft)
- Enter to Send setting: Web setting for Enter key behavior in agent input
- Swipe archive/delete: Swipe gestures on session list items
- AskUserQuestionView: New tool view for interactive questions
- Italian translations: Complete it.ts translation file
- Japanese translations: Complete ja.ts translation file

Feature branch (preserved):
- Profile-first wizard UI with environment variable expansion
- CLI detection for claude/codex/gemini
- SearchableListSelector for machines/paths
- ModelMode per agent type (validGeminiModes added)
- Permission mode race condition fix (removed duplicate useEffect)

Integration changes:
- new/index.tsx: Added machineId/pathParam route params + useEffects for navigation pattern
- pick/machine.tsx: Navigation dispatch pattern for passing machineId back
- pick/path.tsx: Navigation dispatch pattern for passing path back
- settings.spec.ts: Added agentInputEnterToSend to test fixture

Deleted: sources/trash/active-session-navigation.test.ts (obsolete test in trash directory)
…arks

Add translations for feature-branch specific strings:
- profiles section (profile management feature)
- codexModel (model selection UI)
- enhancedSessionWizard (settings toggle)
- askUserQuestion (tool view)
- enterToSend (web feature)
- common.delete, common.optional, common.saveAs

Fix Italian spelling: "MODALITA" → "MODALITÀ" (18 occurrences)
The grave accent is required in Italian for the word "modalità" (mode).

Files modified:
- sources/text/translations/it.ts
- sources/text/translations/ja.ts
… prevent infinite loop

Bug: "Maximum update depth exceeded" React error caused infinite re-renders

Root cause: StyleSheet.create() was called INSIDE the component body (line 111),
which with react-native-unistyles creates new styles on every render, triggering
a re-render loop.

Fix: Move StyleSheet.create outside the component using the theme function pattern:
`StyleSheet.create((theme) => ({...}))` - matching ToolView.tsx and other components.

This is a common react-native-unistyles pitfall - the library's StyleSheet.create
is theme-aware and dynamic, so it MUST be called outside components.
@ahundt
Copy link
Contributor Author

ahundt commented Jan 8, 2026

@bra1nDump ok I've merged main into this branch and tested the feature flag and everything looks good to me with that degree of testing, same as slopus/happy-cli#107

@ahundt
Copy link
Contributor Author

ahundt commented Jan 8, 2026

@bra1nDump also, the front end gating aka feature flags are added and tested and keep the main branch functionality by default

ahundt and others added 13 commits January 8, 2026 22:00
…ibility

Previous behavior:
- babel.config.js used hardcoded 'react-native-worklets/plugin'
- metro.config.js lacked inlineRequires causing Skia "react-native-reanimated is not installed" error
- react-native-reanimated 4.1.0 had iOS 26 accessibility crash ("Cannot read property 'level' of undefined")
- react-native-worklets 0.5.1 incompatible with reanimated 4.2.1
- Expo Go incompatible with NitroModules (react-native-unistyles, react-native-mmkv)

What changed:
- babel.config.js: Added version-aware plugin selection (auto-detects Reanimated v3 vs v4)
- metro.config.js: Added inlineRequires, .wasm asset support, CSS support for cross-platform compatibility
- package.json: Updated react-native-reanimated to 4.2.1 (iOS 26 fix), worklets to 0.7.1, added setup-skia-web to postinstall
- yarn.lock: Updated dependency tree
- ios/.xcode.env.local: Fixed NODE_BINARY to use $(command -v node) instead of hardcoded path

Why:
Development builds required for NitroModules (used by react-native-unistyles for high-performance styling and react-native-mmkv for storage). Skia requires inlineRequires in Metro to prevent module loading errors. iOS 26 devices crash without Reanimated 4.2.1+ due to accessibility API handling.

Files affected:
- babel.config.js: Version-aware worklets/reanimated plugin selection
- metro.config.js: inlineRequires + wasm + CSS support for iOS/Android/web
- package.json: Dependency updates + setup-skia-web postinstall (cross-package-manager compatible)
- yarn.lock: Lockfile updates for new versions
- ios/.xcode.env.local: Dynamic Node path (local-only, not committed)

Testable:
- iPad development build: yarn ios --device "<device-name>" succeeds
- App launches without "Cannot read property 'level'" error
- App launches without "react-native-reanimated is not installed" error
- Enhanced Session Wizard toggle visible in Settings → Features
- Works with npm, yarn, pnpm, bun (npx in postinstall is cross-compatible)
- iOS, Android, web platforms supported

Cross-platform compatibility verified:
- Package managers: npm, yarn (official: [email protected]), pnpm, bun
- Platforms: iOS (physical devices + simulator), Android, web
- Reanimated versions: v3.x (backward compatible) and v4.x (current)
…lization

Previous behavior:
- Used manual preprocessing with unknown types (Record<string, unknown>)
- Transformation happened before Zod validation in preprocessRawMessage()
- Lost compile-time type safety and IDE autocomplete
- ~200 lines of manual field mapping code

What changed:
- typesRaw.ts: Replaced unknown-based preprocessing with Zod's native .transform()
- typesRaw.ts: Added hyphenated input schemas (rawHyphenatedToolCallSchema, rawHyphenatedToolResultSchema)
- typesRaw.ts: Created rawAgentContentInputSchema accepting 5 content types
- typesRaw.ts: Added type-safe transform functions (normalizeToToolUse, normalizeToToolResult)
- typesRaw.ts: Modified rawAgentContentSchema to use .transform() during validation
- typesRaw.ts: Removed preprocessing call from normalizeRawMessage()
- typesRaw.ts: Simplified error logging (removed preprocessing reference)
- NET: ~140 lines deleted (200 removed, 60 added)

Type safety improvements:
- Full compile-time type checking (TypeScript infers all types correctly)
- IDE autocomplete for all fields
- Field preservation via .passthrough() and manual spreading
- Same robustness as unknown approach but with compiler verification

Field remappings (type-safe):
- tool-call → tool_use: callId → id
- tool-call-result → tool_result: callId → tool_use_id, output → content

Backwards compatibility:
- Wire format unchanged (CLI sends same messages)
- Zod transform accepts both hyphenated and underscore types
- Canonical types pass through unchanged (idempotent)
- Codex/Gemini path unchanged (native hyphenated support)

Tested:
- yarn typecheck passes (proves type safety)
- iPad app: Messages sync correctly, no validation errors
- Metro console: No "Invalid raw record" errors

Files affected:
- sources/sync/typesRaw.ts: Zod transform implementation
…ckers

Previous behavior:
- machine.tsx:50 and path.tsx:128 accessed state.routes[state.index - 1] without null checks
- TypeScript error TS18048: 'state' is possibly 'undefined'
- Potential runtime errors if navigation state undefined

What changed:
- machine.tsx:50: Use optional chaining state?.routes?.[state.index - 1]
- path.tsx:128: Use optional chaining state?.routes?.[state.index - 1]

Why:
- navigation.getState() can return undefined in edge cases
- Optional chaining prevents runtime errors
- Maintains backward compatibility with main branch pattern

Files affected:
- sources/app/(app)/new/pick/machine.tsx: Add optional chaining
- sources/app/(app)/new/pick/path.tsx: Add optional chaining
Previous behavior:
- settingsFeatures missing enterToSend, enterToSendEnabled, enterToSendDisabled keys
- tools.names missing 'question' key
- tools.askUserQuestion section missing entirely
- TypeScript errors: TS2739 and TS2741 (missing properties)

What changed:
- en.ts:213-215: Add enterToSend keys (title, enabled, disabled states)
- en.ts:521: Add question: 'Question' to tools.names
- en.ts:523-526: Add askUserQuestion section (submit, multipleQuestions)

Why:
- Features screen references t('settingsFeatures.enterToSend*')
- Tool view components reference t('tools.names.question')
- AskUserQuestion components need translation support
- Ensures type safety across all translation files

Files affected:
- sources/text/translations/en.ts: Add 5 missing translation keys
What this adds:
- typesRaw.spec.ts: 37 test cases covering Zod transform WOLOG behavior
- Test categories: transformation, idempotency, field preservation, backwards compatibility, cross-agent, unexpected formats, regression prevention

Test coverage:
1. Hyphenated to canonical transformation (tool-call → tool_use, tool-call-result → tool_result)
2. Field remapping (callId → id, output → content, defaults for is_error)
3. Unknown field preservation via .passthrough() (future API compatibility)
4. Canonical types pass through unchanged (idempotency verified)
5. Unknown types rejected with clear Zod errors (invalid_union)
6. Mixed format handling (both hyphenated and canonical in same message)
7. Backwards compatibility (old CLI with canonical types)
8. Codex/Gemini native hyphenated schema path (no transformation)
9. Unexpected data formats (empty arrays, string content, dual fields, missing optionals)
10. Complete message flow scenarios (assistant, user, sidechain messages)
11. Field-level edge cases (both callId and id present, both output and content present)
12. Metadata preservation (uuid, parentUuid, isSidechain, permissions)
13. Regression prevention (same behavior as old unknown preprocessing)
14. Defensive handling (hypothetical future API changes)

All tests verify WOLOG principle:
- CLI wire format unchanged (no CLI changes needed)
- GUI accepts both hyphenated and canonical formats
- Transformation is transparent and backward compatible
- Cross-agent compatibility (Claude SDK, Codex, Gemini)

Test results:
- 37 tests, all passing
- Runtime: ~10ms (fast validation)
- Coverage: All schema paths, all content types, all edge cases

Files affected:
- sources/sync/typesRaw.spec.ts: NEW file with comprehensive test suite
Previous behavior:
- Zod discriminated union accepted only 'text', 'tool_use', 'tool_result' (canonical) and 'tool-call', 'tool-call-result' (hyphenated)
- Claude API extended thinking feature sends content with type: 'thinking'
- Messages with thinking content failed validation with "invalid_union" error: "No matching discriminator" at path ["content", "data", "message", "content", 0, "type"]
- Failed messages silently discarded (return null in normalizeRawMessage)
- Console showed repeated validation errors during message sync

What changed:
- Added rawThinkingContentSchema accepting type: 'thinking' with thinking: string field and .passthrough() for signature preservation (line 67-71)
- Added rawThinkingContentSchema to discriminated union (line 119)
- Updated transform return type to include RawThinkingContent (line 180)
- Added thinking to NormalizedAgentContent union type (line 267-271)
- Added thinking normalization case in message content loop (line 367-368)
- Added .passthrough() to metadata schema for CLI fields (line 208)

Why:
Claude API's extended thinking feature outputs model reasoning before final response. CLI sends these thinking blocks which GUI must accept and normalize. Without schema support, all messages containing thinking content are discarded, breaking sync for conversations using extended thinking.

Files affected:
- sources/sync/typesRaw.ts: Added thinking schema, updated discriminated union, transform, type definitions, and normalization logic

Testable:
- iPad Metro console: Zero "=== VALIDATION ERROR ===" logs in last 1000 lines after hot reload
- Messages with thinking content: Successfully normalized with type: 'thinking'
- Example normalized message: {"type":"thinking","thinking":"...", "uuid":"...", "parentUUID":"..."}
- All existing content types: Still process correctly (text, tool_use, tool_result)
- Hyphenated types: Still transform correctly (tool-call → tool_use, tool-call-result → tool_result)
…WOLOG unknown field handling

Previous behavior:
- Thinking content schema added (typesRaw.ts:67-71) but integration incomplete
- Reducer only processed type: 'text' (line 632, 863) - silently dropped thinking content
- No thinking messages displayed in UI despite successful validation
- Zod transform inside intersection caused "Unmergable intersection" error (Zod v4 limitation)
- TypeScript test errors: 23 union type property accesses without type narrowing
- Unknown field preservation untested - WOLOG principle not verified

What changed:
- typesRaw.ts: Added .passthrough() to canonical schemas (text, tool_use, tool_result) for unknown field preservation
- typesRaw.ts: Replaced .transform() with .preprocess() to avoid Zod v4 intersection issue (lines 211-264)
- typesRaw.ts: Simplified transform functions to use object spread instead of manual field iteration
- typesRaw.spec.ts: Fixed 23 TypeScript errors by adding type narrowing guards (Array.isArray checks + type guards)
- typesRaw.spec.ts: Added 3 WOLOG verification tests for unknown field preservation (107 new lines)
- reducer.ts: Added thinking content processing in main flow (line 632) and sidechain flow (line 863)
- reducer.ts: Thinking content converted to agent-text messages for display

Why:
Claude API's extended thinking feature sends type: 'thinking' content. Without reducer processing, thinking validated but never displayed to users. Zod v4 has fundamental limitation with transforms inside discriminated unions inside intersections (colinhacks/zod#2100). Preprocessing before validation avoids this issue while maintaining WOLOG unknown field preservation via .passthrough() and object spread.

Technical approach:
- Preprocessing normalizes hyphenated → canonical BEFORE validation (avoids Zod v4 transform issues)
- .passthrough() on all schemas preserves unknown fields (signature, CLI metadata, future API fields)
- Object spread in transform functions preserves all input fields including unknown ones
- Type narrowing in tests ensures TypeScript safety without runtime changes

Files affected:
- sources/sync/typesRaw.ts: Schema changes, .passthrough() additions, .preprocess() instead of .transform() (146 lines modified)
- sources/sync/typesRaw.spec.ts: Type narrowing fixes + 3 WOLOG tests (268 lines modified, 107 added)
- sources/sync/reducer/reducer.ts: Thinking content processing (10 lines modified)

Testable:
- yarn typecheck: Zero errors (TypeScript compilation passes)
- yarn test typesRaw.spec.ts: 40/40 tests pass (including 3 new WOLOG tests)
- iPad Metro console: Zero "=== VALIDATION ERROR ===" in last 500 lines
- Thinking content: Normalized successfully with signature preservation
- Example: {"type":"thinking","thinking":"...","uuid":"...","signature":"EqkCCkYI..."}
- Unknown fields: Preserved in tool-call → tool_use transform (verified by tests)
- CLI metadata: Preserved via .passthrough() (userType, cwd, sessionId, version, gitBranch, slug, requestId, timestamp)

WOLOG verification:
- Canonical types (text, tool_use, tool_result, thinking): Schema accepts and preserves unknown fields
- Hyphenated types (tool-call, tool-call-result): Transform to canonical with unknown field preservation
- Mixed formats: Single message can contain both canonical and hyphenated - all normalize correctly
- Unknown content types: Fail validation with clear error (intentional fail-fast for type safety)

Backwards compatibility:
- Wire format unchanged (CLI sends same formats)
- Preprocessing happens at validation time (transparent to callers)
- All existing message types work unchanged
…ormalization

Previous behavior:
- Normalization created new objects with ONLY known fields (type, text, uuid, parentUUID)
- Unknown fields preserved through schema validation (.passthrough()) but dropped during normalization
- Object creation syntax: {type: 'text', text: c.text, uuid, parentUUID}
- No end-to-end tests verified unknown field preservation through normalizeRawMessage()
- WOLOG incomplete: Unknown fields prevented validation failures but weren't preserved in app state

What changed:
- typesRaw.ts: Added object spread to preserve all fields in text normalization (line 382-386)
- typesRaw.ts: Added object spread to preserve all fields in thinking normalization (line 388-392)
- typesRaw.ts: Added object spread to preserve all fields in tool_use normalization (line 398-404)
- typesRaw.ts: Added object spread to preserve all fields in tool_result normalization (line 466-480)
- typesRaw.spec.ts: Added 2 end-to-end tests verifying unknown field preservation through normalizeRawMessage()
- typesRaw.spec.ts: Added normalizeRawMessage import for end-to-end testing
- All normalizations use pattern: {...c, uuid, parentUUID} to preserve unknown fields while overriding required ones

Why:
True WOLOG requires unknown fields to survive all layers, not just validation. Extended thinking signature field, future API fields, and CLI debug metadata should be preserved through normalization into app state. Object spread preserves all fields from validated content while overriding specific fields (uuid, parentUUID, type for display).

Technical details:
- Object spread {...c, ...} preserves all fields including those accepted by .passthrough()
- Override fields appear after spread, taking precedence (uuid, parentUUID override any existing)
- Type assertion (as NormalizedAgentContent) allows unknown fields beyond type definition
- TypeScript structural typing permits extra fields - code accessing typed properties unaffected

Files affected:
- sources/sync/typesRaw.ts: Object spread added to 4 normalization locations (text, thinking, tool_use, tool_result)
- sources/sync/typesRaw.spec.ts: 2 end-to-end tests + import (69 lines added)

Testable:
- yarn typecheck: Zero errors (TypeScript compilation passes)
- yarn test typesRaw.spec.ts: 42/42 tests pass (40 original + 2 new end-to-end)
- END-TO-END test 1: Thinking with signature + text with metadata → normalized with unknown fields preserved
- END-TO-END test 2: Hyphenated tool-call with executionMetadata + timestamp → transformed to tool_use with unknown fields preserved
- iPad Metro: Zero validation errors, thinking content displays correctly
- Unknown field access: (normalizedContent as any).signature returns preserved value

WOLOG verification complete:
- Schema validation: .passthrough() accepts unknown fields ✅
- Preprocessing: Object spread in transforms preserves unknown fields ✅
- Normalization: Object spread preserves unknown fields into app state ✅
- End-to-end: Tests verify unknown fields survive all layers ✅
…istinction

Previous behavior:
- Thinking content converted to text with no formatting: text: c.thinking
- Thinking displayed identically to regular agent text responses
- Users couldn't distinguish model reasoning from final responses
- No visual indication that content was extended thinking vs normal output

What changed:
- Line 639: Format thinking as markdown italics with "Thinking..." prefix
- Line 870: Same formatting for sidechain thinking content
- Pattern: text: c.type === 'text' ? c.text : \`*Thinking...*\n\n*\${c.thinking}*\`
- MarkdownView renders asterisks as italic text per CommonMark spec

Why:
Claude Code CLI displays extended thinking in light gray italics to distinguish reasoning from final responses. Users need visual distinction to understand which parts are Claude's thinking process vs actual output. Markdown italics provide this distinction while keeping implementation minimal (no new message kind, no UI components, just string formatting).

Technical approach:
- Markdown prefix: *Thinking...* renders as italic "Thinking..." label
- Double newline: Creates visual separation between label and content
- Content italics: *${c.thinking}* renders entire thinking text in italics
- MarkdownView: Already supports markdown rendering (no changes needed)
- Minimal: Single line change in 2 locations (main flow + sidechain flow)

Files affected:
- sources/sync/reducer/reducer.ts: Thinking content formatted with markdown (2 lines modified)

Testable:
- iPad app: Thinking messages display with italic "Thinking..." prefix
- Thinking text: Fully italicized to distinguish from regular responses
- Regular text: Displays unchanged (no formatting)
- MarkdownView: Renders markdown correctly (verified in Metro logs)

Visual distinction verified:
- Regular text: Normal font weight and style
- Thinking: Italic "Thinking..." prefix + italic content
- Matches Claude Code CLI pattern (light gray italics)

Sources:
- Claude Code Professional Guide: https://wmedia.es/en/writing/claude-code-professional-guide-frontend-ai (thinking display format)
- Extended thinking documentation: https://platform.claude.com/docs/en/build-with-claude/extended-thinking
… layout

Previous behavior:
- Title container used absolute positioning (left: 0, right: 0) to center over full header width
- Navigation icons in rightContainer could visually overlap centered title on narrow sidebars
- getConnectionStatus() function called 3x per render (inefficient)

What changed:
- Added titleContainerLeft style with flex: 1 for left-justified mode (in document flow)
- Added shouldLeftJustify calculation using same sidebar width formula as SidebarNavigator.tsx
- Title now conditionally renders: centered when space permits, left-justified when icons would overlap
- Extracted titleContent variable (DRY) used by both centered and left-justified modes
- Changed getConnectionStatus() to IIFE to compute once per render (theme-reactive)

Why:
With experiments ON, 4 icons need 148px which exceeds overlap threshold for max 360px sidebar.
Without experiments, 3 icons (108px) allow centering above ~340px sidebar width.
IIFE pattern avoids stale memoization bugs when theme changes (useMemo would cache stale colors).

Files affected:
- sources/components/SidebarView.tsx: Import, style, layout logic, and JSX structure

Testable:
- iPad landscape, experiments OFF, wide window: Title centered in sidebar header
- iPad landscape, experiments OFF, narrow window: Title left-justified after logo
- iPad landscape, experiments ON: Title always left-justified (extra zen icon)
- Theme switch: Colors update correctly (no stale memoization)
- yarn typecheck passes
…th server state

Previous behavior:
Version-mismatch handler overwrote local settings with server settings and cleared pending changes without retrying. When multi-device sync occurred (one device at version N, server at version M>N), the handler would apply server settings, clear pendingSettings from persistence, wait 1 second, and break out of the retry loop. This caused uncommitted local changes (like useEnhancedSessionWizard) to be lost from Zustand store, causing the UI to revert (enhanced wizard → default wizard).

What changed:
- sync.ts syncSettings(): Merge server settings with this.pendingSettings using applySettings(serverSettings, this.pendingSettings) on version-mismatch (line 1173)
- sync.ts syncSettings(): Replace break with continue to retry POST with merged settings (line 1190)
- sync.ts syncSettings(): Clear both this.pendingSettings and savePendingSettings({}) only on successful POST (lines 1162-1163)
- sync.ts syncSettings(): Add maxRetries=3 bounded retry to prevent infinite loops when devices sync simultaneously (lines 1133-1134, 1139)
- sync.ts syncSettings(): Throw error after max retries to trigger InvalidateSync backoff delay (lines 1197-1200)
- sync.ts syncSettings(): Remove unnecessary 1-second delay before break
- settings.spec.ts: Add 11 unit tests verifying version-mismatch merge preserves pending changes

Why:
Multi-device users lost settings when one device's sync conflicted with another's (version-mismatch). The fix follows the merge-and-retry pattern from ops.ts:274-293 (machineUpdateMetadata), which merges server state with local changes and retries until successful. This preserves user's uncommitted changes while respecting server version as source of truth.

Files affected:
- sources/sync/sync.ts: syncSettings() method (lines 1133-1200)
- sources/sync/settings.spec.ts: Added version-mismatch scenario test suite (11 tests)

Testable:
- yarn test settings.spec.ts: Verify 43/43 tests pass (11 new version-mismatch tests)
- Manual: Enable useEnhancedSessionWizard → save profile → verify wizard stays on enhanced UI
- Console: Multi-device sync shows "settings version-mismatch, retrying" logs with successful convergence
- Add isThinking flag to AgentTextMessage and ReducerMessage types
- Set isThinking: true when processing thinking content in reducer
- Hide thinking messages in MessageView when experiments is disabled

Default behavior: thinking content is hidden
With experiments enabled: thinking content renders inline

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <[email protected]>
Co-Authored-By: Happy <[email protected]>
@leeroybrun
Copy link
Contributor

Hi @ahundt @bra1nDump ,

Were the UI changes of the new session fields wanted and expected?

Because in my opinion, it gives too much attention to the "Machine & Folder" compared to the text input. In my opinion, the previous UI was cleaner and clearer, but I don't know if this was a wanted change.

Screenshot 2026-01-12 at 21 52 16

Also the new session wizard and the profiles settings do not seem really unified with the rest of the UI in my opinion, which was much more "clean", no borders, dark gray vs hash black, etc. Also, don't know if that is only on my fork, but the icon to select preferences in the profiles seems broken (no icon for the selected permission).

Screenshot 2026-01-12 at 21 54 26 Screenshot 2026-01-12 at 21 54 58

@bra1nDump
Copy link
Contributor

I wanted to merge the changes because @ahundt has been working on this for a while and I have been stalling a review / merge.

I have provided feedback on this UI wise to @ahundt.

We certainly need to follow up with a redesign of profiles. Some issues - nesting, repeated content, strange scroll behaviors, paddings etc.

I want to keep the current prod session creation ui unchanged and only have profiles affect users who toggle the flag on for now. Changing the current ui was not very intentional. Later ideally we would transition to a system where profiles will get replaced with "environments" like in Claude, codex, cursor web apps. But we need to think more about this problem, open to suggestions here

@leeroybrun
Copy link
Contributor

leeroybrun commented Jan 12, 2026

Ok I understand. Happy - no pun intended - to work a bit on this if that helps, at least to restore the correct and cleaner previous "new session" UI when the feature flag is not enabled.

Regarding the profiles and "new session" UI with profiles: in my opinion, the "Profiles" could be a simple knob in the "new session" UI like the rest, and when users click on it, they can select a different profile, no? This makes the feature easily accessible without changing the whole "new session" UI, which was very clean and beautiful previously, in my opinion.

Screenshot 2026-01-12 at 22 16 37

@bra1nDump
Copy link
Contributor

Yep I agree with that approach. If you can help with restoring ui to the previous state for non enabled users and improving the profile picker (possibly moving to a separate pill as you suggest) that would be amazing! @ahundt and I can help test

@ahundt
Copy link
Contributor Author

ahundt commented Jan 13, 2026

Hey thanks for the merge! One caveat, @leeroybrun, with the prior ui you provided in your screenshot, which just says 'mac', one cannot tell the actual machine that is in use if they have multiple machines, particularly if they are over DNS or another mechanism with a long path.

@bra1nDump
Copy link
Contributor

you can rename machines. maybe we need to surface this more easily

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants