-
Notifications
You must be signed in to change notification settings - Fork 0
Sync upstream main 12-01-2016 #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Add comprehensive tmux utilities (adapted from Python reference) - Support tmux session spawning when TMUX_SESSION_NAME is set in profiles - Automatically detect tmux availability and fall back to regular spawning - Create new windows in existing tmux sessions with descriptive names - Include proper environment variable injection for both tmux and regular spawning - Update TrackedSession interface to support tmux session tracking - Add Andrew Hundt copyright to tmux utilities This enables users to see their Claude/Codex sessions directly in tmux terminals when tmux session names are configured in their profiles, while maintaining full backward compatibility when tmux is not available. Files affected: - src/utils/tmux.ts: Complete tmux utilities with session management - src/daemon/run.ts: Integrated tmux spawning with fallback logic - src/daemon/types.ts: Added tmuxSessionId to TrackedSession - src/modules/common/registerCommonHandlers.ts: Added tmux environment variables - src/persistence.ts: Profile persistence with tmux settings
…ux utilities Summary: - Integrated unified AI backend profile system from happy app with daemon session spawning - Enhanced tmux utilities with comprehensive TypeScript typing and session identifier validation - Replaced manual environment variable filtering with profile-based agent compatibility system Previous behavior: - Manual environment variable filtering based on agent type with hardcoded variable lists - Basic tmux utilities with limited type safety and session management - Flat profile configuration structure in persistence layer - No validation for tmux session identifiers or window operations What changed: - src/daemon/run.ts: Replaced manual environment filtering with getProfileEnvironmentVariablesForAgent() using profile compatibility validation, updated tmux session name handling to use profile-based environment variables - src/persistence.ts: Updated AIBackendProfile interface to match happy app schema with nested agent configurations, added validateProfileForAgent() and getProfileEnvironmentVariables() helper functions, implemented profile versioning system - src/utils/tmux.ts: Added comprehensive TypeScript typing with TmuxSessionIdentifier interface, TmuxControlSequence and TmuxWindowOperation union types, implemented parseTmuxSessionIdentifier() and formatTmuxSessionIdentifier() for validation, enhanced TmuxUtilities class with strongly typed methods Why: - Enable seamless profile synchronization between happy app and happy-cli - Provide type-safe tmux session management with automatic session/window parsing - Add robust validation for tmux operations to prevent runtime errors - Support multiple AI providers through unified profile system - Improve developer experience with comprehensive TypeScript types and error handling Files affected: - src/daemon/run.ts: Profile integration in session spawning - src/persistence.ts: Unified profile schema and helper functions - src/utils/tmux.ts: Enhanced tmux utilities with TypeScript typing Testable: - Spawn sessions with different AI provider profiles and verify correct environment variables - Test tmux session creation with automatic session identifier parsing and validation - Verify profile compatibility validation prevents incompatible agent configurations - Test enhanced tmux utilities with strong typing and error handling
CRITICAL FIX - CLI now uses exact same Zod validation as GUI for data integrity Major schema fixes: - Replaced plain TypeScript interfaces with Zod schemas matching GUI exactly - Added runtime validation for all profile data using validateProfile() function - Ensured identical field validation (UUIDs, string lengths, regex patterns) - Added proper environment variable name validation with regex - Implemented same default values and field requirements as GUI Schema Consistency Achieved: - AIBackendProfileSchema now identical between GUI and CLI - All configuration schemas (Anthropic, OpenAI, Azure, TogetherAI) match exactly - Environment variable validation with regex pattern matching - Profile compatibility schema with proper boolean defaults - Same version validation and compatibility checking functions Data Integrity Impact: - PREVENTS invalid profile data from corrupting CLI operations - ENSURES consistent validation across GUI and CLI boundaries - GUARDS against malformed profiles breaking session creation - MAINTAINS type safety throughout the entire system Validation Implementation: - Added validateProfile() function using Zod safeParse with proper error reporting - Updated updateProfiles() to validate all incoming profile data - Preserved all existing CLI functionality while adding safety - Backward compatible with existing valid profile data This addresses a critical data integrity risk where the CLI could accept invalid profile data that would cause session creation failures or system instability.
…atibility
SCHEMA MIGRATION:
- Add SUPPORTED_SCHEMA_VERSION = 2 constant
- Add schemaVersion field to Settings interface
- Implement automatic v1 → v2 migration on settings load
- Migration adds profiles[] and localEnvironmentVariables{}
VALIDATION:
- Validate profiles using AIBackendProfileSchema (Zod) on load
- Skip invalid profiles with warning (don't crash)
- Log clear error messages for debugging
- Merge defaults for any missing fields
VERSION DETECTION:
- Warn when settings schema newer than supported version
- Clear upgrade messages logged for users
- Ensure schema version always written on save
- Compatible with GUI schema version system
ERROR HANDLING:
- Graceful profile validation with try-catch per profile
- Invalid profiles skipped, not crashed
- Settings file corruption handled with fallback to defaults
- All errors logged with context
BACKWARDS COMPATIBILITY:
- Old settings (v1) automatically migrated to v2
- Missing fields filled with defaults
- Unknown fields preserved (forward compatibility)
- Zero breaking changes to existing functionality
FILES MODIFIED:
- src/persistence.ts:
* Added SUPPORTED_SCHEMA_VERSION constant
* Updated Settings interface with schemaVersion
* Added migrateSettings() function
* Enhanced readSettings() with migration and validation
* Updated writeSettings() to ensure schema version
* Added logger import for warnings
TECHNICAL DETAILS:
- Uses Zod AIBackendProfileSchema for validation
- Per-profile validation loop with error recovery
- Schema version defaults to 1 for old files
- Migration is idempotent and safe to re-run
Tested scenarios:
- ✅ V1 → V2 migration with profile array creation
- ✅ Invalid profile graceful handling with warnings
- ✅ Schema version mismatch warnings
- ✅ Backwards compatibility with v1 settings
- ✅ writeSettings always includes schema version
Related to yolo-mode-persistence and profile management feature
Coordinated with GUI schema version system (both use v2)
Previous behavior: - README.md contained 83 lines of developer-only documentation (lines 49-131) - Development setup instructions buried in user-facing README - USAGE.md existed but not discoverable from README - Mixed audience (end users + developers) in single file What changed: - USAGE.md → CONTRIBUTING.md: Renamed for conventional structure - CONTRIBUTING.md: Now titled "Contributing to Happy CLI" - README.md: Removed 83-line development section - README.md: Added concise "Contributing" section linking to CONTRIBUTING.md - README.md: Now focused solely on end-user installation and usage Why: - Follows standard open-source convention (CONTRIBUTING.md) - Clearer separation: README for users, CONTRIBUTING for developers - More discoverable (CONTRIBUTING.md is GitHub convention) - Reduces cognitive load for new users - README now concise at 60 lines (was 143 lines) Files affected: - README.md: Removed development section, added Contributing link (saved 80 lines) - USAGE.md → CONTRIBUTING.md: Renamed with updated title - No content changes to development docs (only reorganized) Testable: - README.md is now 60 lines (was 143) - README.md contains link to CONTRIBUTING.md - CONTRIBUTING.md has same content as old USAGE.md - GitHub automatically shows CONTRIBUTING.md in contributor guidelines
…entVariables
Previous behavior:
- Daemon ignored options.environmentVariables sent from GUI (lines 270-312)
- Always used CLI local settings.activeProfileId regardless of GUI selection
- GUI profile selection in wizard was completely non-functional
- User selected DeepSeek profile in GUI but daemon used CLI default profile
- Silent failure - no error message or logging to indicate wrong profile active
- Wrong precedence: { ...profileEnvVars, ...extraEnv } meant auth overwrote profile settings
What changed:
- src/daemon/run.ts (lines 269-327): Restructured environment variable handling
- Split into authEnv and profileEnv for clear precedence
- Check options.environmentVariables first (GUI profile)
- Fallback to CLI local settings.activeProfileId only if no GUI profile
- Final merge: { ...profileEnv, ...authEnv } protects auth tokens
- Added extensive logging: info for GUI profile, debug for CLI profile, debug for final keys
- Log keys only (not values) for security
- src/persistence.ts (line 287): Changed logger.error to logger.warn (Logger has no error method)
Why:
- Fixes critical UX bug where GUI profile selection was silently ignored
- Implements correct precedence: GUI profile > CLI local profile > none
- Protects authentication tokens from being overridden by profile settings
- Enables debugging via logs to verify which profile source is active
- Backward compatible: CLI-only workflows without GUI still use local activeProfileId
- Forward compatible: GUI profiles work when provided
Technical details:
- options.environmentVariables is optional field from SpawnSessionOptions (registerCommonHandlers.ts:124)
- GUI sends it via machineSpawnNewSession in ops.ts:183
- Daemon receives it but was ignoring it until this fix
- Auth env vars (CLAUDE_CODE_OAUTH_TOKEN, CODEX_HOME) must never be overridden by profiles
- Profile env vars can customize API endpoints (ANTHROPIC_BASE_URL) and tmux settings (TMUX_SESSION_NAME)
Files affected:
- src/daemon/run.ts: Restructured env var handling (lines 269-327, +19 lines, restructured logic)
- src/persistence.ts: Fixed logger call (line 287, logger.error → logger.warn)
Testable:
- GUI selects DeepSeek profile → daemon logs "Using GUI-provided profile (N vars)"
- CLI-only spawn → daemon logs "No GUI profile provided, loading CLI local active profile"
- Auth token always present in final extraEnv even if profile sets same key
- Logs show env var keys (not values) for debugging without exposing secrets
Previous behavior: - Only happy binary available globally - Developers needed npm run dev:variant for development version - No global command to quickly access dev data directory - Required cd to project directory to use npm scripts What changed: - package.json: Added happy-dev to bin field (line 13) - bin/happy-dev.mjs (new, 41 lines): Wrapper binary setting HAPPY_HOME_DIR to ~/.happy-dev and HAPPY_VARIANT to dev - Follows same pattern as bin/happy.mjs but sets dev environment - Sets environment before importing dist/index.mjs Why: - Enables global happy-dev command from anywhere on system - Automatic environment: HAPPY_HOME_DIR=~/.happy-dev, HAPPY_VARIANT=dev - Consistent with happy and happy-mcp global binaries - No need to cd to project directory or use npm scripts - Simpler workflow: happy-dev daemon start vs npm run dev:daemon:start Files affected: - package.json: Added happy-dev binary (line 13) - bin/happy-dev.mjs (new, 41 lines): Dev variant wrapper binary Testable: - npm install -g . creates /opt/homebrew/bin/happy-dev symlink - happy-dev --version shows "🔧 DEV MODE - Data: ~/.happy-dev" - happy-dev daemon status checks ~/.happy-dev/daemon.state.json - happy (stable) and happy-dev can be used simultaneously
…ile schema Summary: Extend profile schema to store default permission and model modes What changed: - Lines 82-85: Added defaultPermissionMode and defaultModelMode optional fields - Both fields are strings (validated at usage time) - Schemas remain backward compatible (optional fields) Why: - Permission mode should be profile-specific, not session-specific - Each AI provider profile can have appropriate default permissions - Matches GUI schema extension for consistency Files affected: - src/persistence.ts: AIBackendProfileSchema updated Testable: Profiles with permission modes can be created and loaded
Summary: Profiles can now store default session type (simple vs worktree) What changed: - Line 82: Added defaultSessionType field to AIBackendProfileSchema - Type: z.enum(['simple', 'worktree']).optional() Why: - Session type should be profile-specific like permission mode - DeepSeek might default to 'worktree', Anthropic to 'simple', etc. - Matches GUI schema for compatibility Files affected: - src/persistence.ts Testable: Profiles with session types can be saved and loaded
…for all spawn modes
Previous behavior:
- Tmux mode: ${VAR} expansion worked via shell (export KEY="${VAR}";)
- Non-tmux mode: ${VAR} passed as LITERAL STRING to Node.js spawn
- Z.AI and DeepSeek profiles broken in non-tmux mode
- ANTHROPIC_AUTH_TOKEN="${Z_AI_AUTH_TOKEN}" sent literally to session
- Claude CLI received string "${Z_AI_AUTH_TOKEN}" instead of actual key
- Multi-backend sessions impossible without tmux
What changed:
- src/utils/expandEnvVars.ts: New utility for ${VAR} expansion
- expandEnvironmentVariables() function with regex replacement
- Replaces ${VARNAME} with process.env[VARNAME]
- Keeps ${VAR} unchanged if variable not found (debugging)
- Works for any Record<string, string> environment object
- src/daemon/run.ts:
- Line 24: Import expandEnvironmentVariables
- Line 327: Changed const to let for extraEnv (will be reassigned)
- Lines 330-334: Expand ${VAR} after merging profileEnv + authEnv
- Expansion happens BEFORE tmux vs non-tmux decision
- Both spawn modes now receive expanded values
- Added debug logging for before/after expansion
How ${VAR} expansion works now:
1. GUI profile has: ANTHROPIC_AUTH_TOKEN=${Z_AI_AUTH_TOKEN}
2. Daemon receives: profileEnv = { ANTHROPIC_AUTH_TOKEN: "${Z_AI_AUTH_TOKEN}" }
3. Daemon merges: extraEnv = { ...profileEnv, ...authEnv }
4. Daemon expands: expandEnvironmentVariables(extraEnv, process.env)
- Finds ${Z_AI_AUTH_TOKEN} in value
- Looks up process.env.Z_AI_AUTH_TOKEN
- Replaces ${Z_AI_AUTH_TOKEN} with actual value
- Result: { ANTHROPIC_AUTH_TOKEN: "sk-real-key" }
5. Spawn (both modes) gets: ANTHROPIC_AUTH_TOKEN=sk-real-key (actual value)
Why:
- Multi-backend sessions: Run Anthropic + Z.AI + DeepSeek simultaneously
- Works in all modes: tmux and non-tmux both get expanded values
- Security: Credentials stay in daemon environment, not transmitted from GUI
- Flexibility: Same daemon serves multiple backends via profile selection
- Debugging: Undefined variables kept as ${VAR} (shows what's missing)
Files affected:
- src/utils/expandEnvVars.ts: Variable expansion utility (new file)
- src/daemon/run.ts: Apply expansion before spawning sessions
Testable:
- Launch daemon: Z_AI_AUTH_TOKEN=sk-abc happy daemon start
- Create session with Z.AI profile (has ANTHROPIC_AUTH_TOKEN=${Z_AI_AUTH_TOKEN})
- Non-tmux mode: Session receives ANTHROPIC_AUTH_TOKEN=sk-abc (expanded)
- Tmux mode: Still works as before
- Create second session with Anthropic profile (no substitution)
- Both sessions run simultaneously with different backends
…vironment
Previous behavior:
- Undefined variables silently kept as ${VAR} placeholders
- Session spawned with ANTHROPIC_AUTH_TOKEN="${Z_AI_AUTH_TOKEN}" (literal string)
- Claude CLI failed with cryptic authentication error
- No indication what went wrong
What changed:
- src/utils/expandEnvVars.ts:
- Line 1: Import logger
- Line 34: Track undefinedVars array during expansion
- Lines 43-44: Add varName to undefinedVars when not found in sourceEnv
- Lines 52-59: Log warning if any variables undefined
- Lists all undefined variables
- Shows exact commands to set them
- Example: "Set these in daemon environment: Z_AI_AUTH_TOKEN=<your-value>"
Warning output example:
```
[EXPAND ENV] Undefined variables referenced in profile environment: Z_AI_AUTH_TOKEN, Z_AI_BASE_URL
[EXPAND ENV] Session may fail to authenticate. Set these in daemon environment before launching:
[EXPAND ENV] Z_AI_AUTH_TOKEN=<your-value>
[EXPAND ENV] Z_AI_BASE_URL=<your-value>
```
Why:
- Easy to use correctly: Clear error message explains what's missing
- Hard to use incorrectly: Warning appears immediately, before session fails
- Actionable: Shows exact variable names and how to set them
- Debugging: Users know to check daemon launch command
Files affected:
- src/utils/expandEnvVars.ts: Warning logging for undefined variables
Testable:
- Launch daemon without Z_AI_AUTH_TOKEN
- Create session with Z.AI profile
- Warning logged: "Undefined variables: Z_AI_AUTH_TOKEN"
- Session spawns but authentication fails (expected)
- User knows to relaunch daemon with Z_AI_AUTH_TOKEN=...
…nt switching Previous behavior: - Single data directory (~/.happy/) shared by all instances - No way to run stable and development versions simultaneously - Manual environment variable management required - No visual feedback about which version is running What changed: - scripts/env-wrapper.cjs: Cross-platform wrapper setting HAPPY_HOME_DIR per variant - scripts/setup-dev.cjs: One-command automated setup creating ~/.happy/ and ~/.happy-dev/ - package.json: Added npm scripts for stable/dev variant management - npm run stable/dev:variant for any command - npm run stable/dev:daemon:start/stop/status for quick daemon control - npm run stable/dev:auth for authentication - npm run setup:dev for initial setup - src/configuration.ts: Variant validation and visual indicators (✅ STABLE / 🔧 DEV) - .gitignore: Added .envrc and .happy-dev/ to ignore list - README.md: Added comprehensive development section with usage examples - USAGE.md: Complete workflow guide with troubleshooting - .envrc.example: Optional direnv configuration for automatic switching Why: - Enables concurrent stable and development versions with complete data isolation - Prevents development work from interfering with production workflows - Cross-platform support via Node.js (Windows/macOS/Linux) - Visual feedback ensures users always know which variant is active - Discoverable commands in package.json reduce cognitive load - Optional direnv integration for power users Files affected: - scripts/env-wrapper.cjs (new): Environment wrapper with visual feedback - scripts/setup-dev.cjs (new): Automated setup script - package.json: Added 16 new npm scripts for variant management - src/configuration.ts: Added variant validation (lines 59-74) - .gitignore: Added .envrc and .happy-dev/ - README.md: Added 83-line development section - USAGE.md (new): 191-line comprehensive guide - .envrc.example (new): Optional direnv configuration Testable: - npm run setup:dev creates both ~/.happy/ and ~/.happy-dev/ - npm run stable:daemon:status shows ✅ STABLE and ~/.happy/ data location - npm run dev:daemon:status shows 🔧 DEV and ~/.happy-dev/ data location - Both daemons can run simultaneously with separate ports and state - Visual indicators always display which variant is active
… unspecified
Previous behavior:
- Empty TMUX_SESSION_NAME treated as undefined
- if (!tmuxSessionName) → disabled tmux
- Always fell back to regular shell spawn
- Tmux checkbox in GUI didn't work with empty field
What changed:
- src/daemon/run.ts:
- Lines 340-342: Added comment explaining empty string behavior
- Line 346: Changed if (!tmuxSessionName) to if (tmuxSessionName === undefined)
- Empty string ('') now valid for tmux mode
- Line 353: Changed if (useTmux && tmuxSessionName) to if (useTmux && tmuxSessionName !== undefined)
- Line 355-356: sessionDesc shows "current/most recent session" when empty
- src/utils/tmux.ts (spawnInTmux function):
- Lines 739-760: Session name resolution logic
- undefined or empty string → query existing sessions
- Run: tmux list-sessions -F '#{session_name}'
- If sessions exist → use first one from list
- If no sessions → create/use "happy" session
- Specific name → use that session (create if needed)
- Line 740: sessionName !== undefined && sessionName !== '' check
- Lines 748-759: Query and select first existing session
- Line 765: ensureSessionExists after resolution (concrete name)
- Line 768: Always uses -t flag with resolved session name
Execution flow (empty session name):
1. GUI sends: TMUX_SESSION_NAME='' (empty string from profile)
2. Daemon: tmuxSessionName = '' → useTmux = true (not undefined)
3. spawnInTmux called with sessionName=''
4. Query: tmux list-sessions -F '#{session_name}'
5. If exists: Use first session (e.g., "work")
6. If none: Use "happy"
7. Create window in resolved session
8. Track with concrete session name
Why:
- Respects existing tmux setup: Uses user's first session
- Smart fallback: Creates "happy" only when needed
- Deterministic: First session is predictable
- Easy to use correctly: Empty field = smart default
- Hard to use incorrectly: Always has concrete session name for tracking
Files affected:
- src/daemon/run.ts: Handle empty string for tmux
- src/utils/tmux.ts: Query and select first existing session
Testable:
- Have tmux session "work" running
- Create session with empty TMUX_SESSION_NAME
- Session spawns in window in "work" session
- No tmux sessions running
- Create session → spawns in new "happy" session
CRITICAL FIXES:
- tmux.ts:377: Fixed JavaScript array indexing bug ([-1] doesn't work in JS)
* Before: parts[1].split('/')[-1] → always undefined
* After: Proper array.length-1 indexing
* Impact: TMUX environment parsing now works correctly
- daemon/run.ts:336: Fixed overly strict auth variable validation
* Only validates variables that ARE SET (agent-agnostic)
* Handles Claude OAuth (no ANTHROPIC_AUTH_TOKEN needed)
* Handles Codex-only sessions (no Claude vars needed)
* Impact: Sessions no longer fail unnecessarily with OAuth
TEST COVERAGE ADDED:
- src/utils/tmux.test.ts: 51 tests for session identifier parsing/validation
* Tests the critical array indexing fix
* Pure unit tests (no tmux installation required)
- src/utils/expandEnvVars.test.ts: 17 tests for ${VAR} expansion
* Auth token expansion patterns
* Partial expansion handling
* Malformed reference handling
DOCUMENTATION:
- CONTRIBUTING.md: Added "Testing Profile Sync Between GUI and CLI" section
* Step-by-step profile sync testing workflow
* Schema compatibility testing guidelines
* Common issues and troubleshooting
- persistence.ts: Added clarifying comments for schema versions
* CURRENT_PROFILE_VERSION: Semver for AIBackendProfile schema
* SUPPORTED_SCHEMA_VERSION: Integer for Settings migrations
TEST RESULTS:
✓ 51 tmux tests pass (9ms)
✓ 17 env expansion tests pass (35ms)
✓ Total: 173 tests passed
Files affected:
- src/utils/tmux.ts: Fixed array indexing bug
- src/daemon/run.ts: Fixed auth validation logic
- src/persistence.ts: Added schema version comments
- CONTRIBUTING.md: Added profile sync testing docs
- src/utils/tmux.test.ts: New test file (51 tests)
- src/utils/expandEnvVars.test.ts: New test file (17 tests)
Fixes maintainer code review blocking issues.
…ling CRITICAL FIX: - src/persistence.ts:147: Fixed empty string tmux sessionName filtering * Before: if (profile.tmuxConfig.sessionName) → WRONG (truthy check) * After: if (profile.tmuxConfig.sessionName !== undefined) → CORRECT * Impact: Empty string now correctly passed to sessions (matches GUI behavior) Previous behavior: - GUI documents: empty string = "use current/most recent session" - GUI correctly uses !== undefined check (settings.ts:171) - CLI incorrectly used truthy check, dropped empty strings - Result: GUI-created profiles with sessionName="" silently lost feature What changed: - CLI now matches GUI logic exactly - Empty strings preserved and passed to TMUX_SESSION_NAME - Users can use "current/most recent session" feature from GUI Why: Cross-repository compatibility - CLI must handle all valid GUI profile configurations. Empty string is a documented valid value meaning "use current/most recent tmux session". CROSS-REPO ANALYSIS: - Added CROSS_REPO_COMPATIBILITY_REPORT.md documenting: * 100% schema compatibility verification (17 fields, 7 sub-schemas) * Complete API communication flow analysis * Security properties (encryption, validation) * 3 additional bugs found in GUI repo requiring separate fixes Files affected: - src/persistence.ts: Fixed getProfileEnvironmentVariables() tmux sessionName check - CROSS_REPO_COMPATIBILITY_REPORT.md: NEW comprehensive compatibility analysis Identified similar bugs in GUI repo (require separate fixes): - sources/sync/settings.ts:172: tmpDir has same truthy check bug - sources/utils/sessionUtils.ts:84: Array .pop() without null check - sources/utils/parseToken.ts:5: Token parsing without validation Cross-repository validation ensures GUI and CLI maintain 100% compatibility.
Remove CROSS_REPO_COMPATIBILITY_REPORT.md as it's a one-off analysis document not meant for long-term repository maintenance. The analysis findings have been incorporated into code fixes and CONTRIBUTING.md documentation.
…ve AI backends Fixed critical bug where environment variables from AI backend profiles (Z.AI, DeepSeek, etc.) were not being passed to CLI sessions launched in tmux mode, causing authentication failures and preventing alternative backends from working. Previous behavior: - spawnInTmux() accepted env parameter but never used it (src/utils/tmux.ts:729) - daemon/run.ts attempted workaround with export statements in command string - Environment variables were not set in tmux window's environment - Z.AI, DeepSeek, and other alternative backends failed to authenticate in tmux mode What changed: - src/utils/tmux.ts:790-815: Implemented environment variable handling via tmux -e flag - src/utils/tmux.ts:200-213: Made env a separate parameter for clarity and efficiency - src/utils/tmux.ts:806-810: Added comprehensive escaping (backslashes, quotes, dollar signs, backticks) - src/utils/tmux.ts:793-802: Added validation for undefined values and invalid variable names - src/daemon/run.ts:378-393: Removed redundant export statements, pass env as third parameter - Only profile environment variables passed (not process.env) for efficiency Why: - Tmux windows inherit environment from tmux server, not from client command - Must use tmux's -e flag to set variables in window environment - Proper escaping prevents shell injection attacks - Validation ensures robust handling of NodeJS.ProcessEnv type (string | undefined) - Passing only profile vars avoids 50+ unnecessary -e flags and command-line length issues Files affected: - src/utils/tmux.ts: Fixed spawnInTmux() to use env parameter, added escaping and validation - src/daemon/run.ts: Updated to use new spawnInTmux() signature with env as third parameter Testable: - Launch session with Z.AI profile in tmux mode: ANTHROPIC_AUTH_TOKEN now correctly set - Launch session with DeepSeek profile in tmux mode: environment variables pass through - Environment variables with special characters (quotes, $vars, `backticks`) properly escaped - Invalid/undefined environment variables skipped with warning logs
Fixed critical bug where RPC handler received environment variables from GUI but dropped them before calling spawnSession, causing all alternative AI backends (Z.AI, DeepSeek, etc.) to fail authentication and fall back to default Anthropic credentials.
Previous behavior:
- RPC handler extracted 6 fields from params but not environmentVariables (src/api/apiMachine.ts:105)
- Passed only {directory, sessionId, machineId, approvedNewDirectoryCreation, agent, token} to spawnSession
- environmentVariables field was in params but not destructured or forwarded (line 112)
- Daemon logs showed "Spawning session with params: {7 environmentVariables}" but then "Final environment variable keys (0)"
- GUI sent 7 environment variables, RPC handler received them, but lost them before spawning
- All sessions spawned with empty environment, causing authentication to fall back to default credentials
- Z.AI sessions connected to Anthropic instead of Z.AI (responded "Sonnet 4.5" instead of "GLM-4.6")
What changed:
- src/api/apiMachine.ts:105: Added environmentVariables to destructuring assignment
- src/api/apiMachine.ts:112: Added environmentVariables to spawnSession call parameters
- Now all 7 fields are extracted and forwarded to spawnSession function
Why:
- RPC handler is the bridge between GUI (sending environmentVariables via WebSocket) and daemon spawn logic
- Destructuring assignment must extract ALL fields from params to avoid silent data loss
- Missing field extraction is a common JavaScript bug - object has field but it's not being used
- This bug completely broke GUI profile selection for alternative backends (Z.AI, DeepSeek, Azure, etc.)
Files affected:
- src/api/apiMachine.ts: Added environmentVariables to destructuring and function call (2 locations)
Testable:
- Select Z.AI profile in GUI, create session, ask "what model are you" → should respond "GLM-4.6" not "Sonnet 4.5"
- Daemon logs should show "Using GUI-provided profile environment variables (7 vars)"
- Daemon logs should show "Final environment variable keys (before expansion) (7): ..." not "(0):"
- Alternative backends (DeepSeek, Azure OpenAI) should now authenticate correctly
…ntax
Summary: Profile environment variables now properly expand ${VAR:-default} syntax to use default values when daemon environment variables are not set.
Previous behavior: The expandEnvironmentVariables function only supported simple ${VAR} syntax. When profiles used bash parameter expansion with default values like ${Z_AI_BASE_URL:-https://api.z.ai/api/anthropic}, the function would try to look up the entire string "Z_AI_BASE_URL:-https://api.z.ai/api/anthropic" as a variable name, fail, and keep the unexpanded placeholder.
What changed:
- Parse ${VAR:-default} expressions to separate variable name from default value
- Look up only the variable name in daemon's process.env
- If variable exists, use its value (even if empty string)
- If variable doesn't exist but default provided, use the default value
- If variable doesn't exist and no default, keep placeholder and warn
- Add logging to show which variables are expanded vs using defaults
- Add warning when variable is set but empty (common mistake)
- Mask sensitive values (containing "token", "key", "secret") in logs
Why: Z.AI and other built-in profiles use ${VAR:-default} syntax to provide sensible defaults while allowing daemon environment variables to override them. Without this fix, these profiles would fail to work unless ALL variables were set in the daemon's environment, defeating the purpose of defaults.
Files affected:
- src/utils/expandEnvVars.ts: Enhanced expansion logic with bash parameter expansion support and better logging
Testable: Create session with Z.AI profile. Check daemon logs show "Using default value for Z_AI_BASE_URL" if Z_AI_BASE_URL not set in daemon environment, or "Expanded Z_AI_BASE_URL from daemon env" if set.
…s and acceptEdits modes by removing hardcoded override and strengthening schema validation Previous behavior (based on git diff): - claudeRemote.ts:114 forced all non-'plan' permission modes to 'default' via ternary operator - persistence.ts:85 accepted any string for defaultPermissionMode (z.string().optional()) - api/types.ts:232 accepted any string for message-level permissionMode (z.string().optional()) - User selections of 'bypassPermissions' or 'acceptEdits' were silently overridden to 'default' - Invalid permission modes could be stored in profiles and messages without validation What changed: - claudeRemote.ts:114 - removed ternary operator, now passes through initial.mode.permissionMode directly to SDK - persistence.ts:85 - changed schema from z.string().optional() to z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan']).optional() - api/types.ts:232 - changed schema from z.string().optional() to z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan', 'read-only', 'safe-yolo', 'yolo']).optional() Why: - The hardcoded override in claudeRemote.ts was the root cause preventing bypassPermissions and acceptEdits from working - SDK QueryOptions interface (src/claude/sdk/types.ts:169) accepts all four Claude modes: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' - Validation already occurs upstream in runClaude.ts:170-182 against the whitelist ['default', 'acceptEdits', 'bypassPermissions', 'plan'] - Schema validation strengthening prevents invalid modes from being stored, matching happy app's MessageMetaSchema (sources/sync/typesMessageMeta.ts:6) - Ensures consistency across the communication pipeline between happy app and happy-cli - Codex pathway already correctly passes through mode without override (verified in runCodex.ts:621,629) Files affected: - src/claude/claudeRemote.ts:114 - removed hardcoded permission mode override to 'default' - src/persistence.ts:85 - strengthened AIBackendProfile schema validation for Claude modes - src/api/types.ts:232 - strengthened MessageMeta schema validation for all modes (Claude + Codex) Testable: - In happy app, select bypassPermissions mode for a session - Send message requiring tool approval - Verify tools are auto-approved without permission prompts (bypass behavior) - Verify mode persists across multiple messages - Test all modes: default, acceptEdits, bypassPermissions, plan - Verify TypeScript compilation passes (yarn typecheck)
…e and Codex modes
Previous behavior (based on git diff):
- types.ts:3 imported PermissionMode from '@/claude/loop' which only included 4 Claude modes
- AgentState.completedRequests[].mode used this Claude-only type but handles both Claude and Codex sessions
- Created type/runtime mismatch: TypeScript type allowed 4 modes but Zod schema validated 7 modes
- Codex modes ('read-only', 'safe-yolo', 'yolo') were not included in the imported type
What changed:
- types.ts:3-8 - removed import of Claude-specific PermissionMode type
- types.ts:8 - defined complete PermissionMode type with all 7 modes (Claude + Codex)
- Added documentation comment noting it must match MessageMetaSchema.permissionMode enum
Why:
- AgentState type is used by both Claude and Codex sessions, so needs complete type definition
- The Zod schema MessageMetaSchema.permissionMode already correctly validates all 7 modes
- This eliminates type/runtime inconsistency where TypeScript type was narrower than runtime validation
- Ensures type safety for Codex permission modes in AgentState.completedRequests
Files affected:
- src/api/types.ts:3-8 - defined complete PermissionMode type instead of importing Claude-only type
Testable:
- Verify TypeScript compilation passes (yarn typecheck)
- Type system now correctly allows all 7 permission modes in AgentState
- Matches MessageMetaSchema enum: ['default', 'acceptEdits', 'bypassPermissions', 'plan', 'read-only', 'safe-yolo', 'yolo']
Previous behavior: - new-window command had duplicate -t flags (manual + automatic) - Working directory (cwd option) was completely ignored - Command execution used unreliable two-step process (create window, then send-keys) What changed: - Removed manual -t from createWindowArgs, let executeTmuxCommand add it via session parameter - Added -c flag support to set working directory from options.cwd - Changed to single-step: pass command directly to new-window (executes on creation) - Removed unreliable send-keys step Why: - Duplicate -t flags caused tmux to reject the command - Missing cwd meant sessions started in wrong directory - Direct command passing is more reliable than send-keys Files affected: - src/utils/tmux.ts:785-836 - Complete rewrite of window creation logic Testable: - Create tmux session with profile in specific directory - Window should appear in correct tmux session - Happy CLI should start in specified working directory - Environment variables should be properly set
Previous behavior:
- setTimeout hack created fake session ID after 2 seconds
- No actual connection between tmux spawn and Happy CLI session
- GUI received meaningless session ID that couldn't communicate with process
- Complex webhook matching systems attempted to solve this problem
What changed:
- tmux.ts: Added -P -F "#{pane_pid}" to spawnInTmux() to get real PID immediately
- daemon/run.ts: Use tmux PID in existing pidToAwaiter pattern (exact same as regular sessions)
- Tmux sessions now follow identical flow to regular non-tmux sessions
- No more setTimeout hacks or complex webhook matching needed
Why this is better:
- Uses tmux's native PID retrieval capability (-P flag)
- Follows existing daemon patterns exactly (pidToTrackedSession + pidToAwaiter)
- Minimal changes (only 38 insertions, 27 deletions)
- No race conditions or complex correlation logic
- Happy CLI webhook matches by PID just like regular sessions
- GUI gets real session ID that works for communication
Files affected:
- src/utils/tmux.ts:827-859 - Added -P flag and PID extraction
- src/daemon/run.ts:396-445 - Use tmux PID in existing awaiter pattern
Testable:
- Create session with tmux enabled should work immediately
- Session ID should be real and communication should work
- Should follow same flow as non-tmux sessions
CRITICAL FIX: tmux sessions were missing daemon's environment variables
Previous issue:
- Regular sessions: env = { ...process.env, ...extraEnv }
- Tmux sessions: only got extraEnv variables via -e flags
- Missing daemon's expanded auth variables (e.g., ANTHROPIC_AUTH_TOKEN from Z_AI_AUTH_TOKEN)
- This would cause tmux sessions to fail authentication despite profile configuration
What changed:
- daemon/run.ts: Pass complete environment (process.env + extraEnv) to tmux sessions
- tmux.ts: Updated comment to clarify environment variable inheritance
- Added proper TypeScript filtering for process.env values
- Ensures tmux sessions have identical environment to regular sessions
Why this matters:
- Happy CLI expects ANTHROPIC_AUTH_TOKEN environment variable
- expandEnvironmentVariables() converts Z_AI_AUTH_TOKEN → ANTHROPIC_AUTH_TOKEN in daemon
- Without passing daemon's env, tmux sessions would only get profile's ${VAR} placeholders
- Now tmux sessions work identically to regular sessions for all auth backends
Files affected:
- src/daemon/run.ts:389-399 - Pass complete environment to tmux
- src/utils/tmux.ts:797-825 - Updated environment variable inheritance comment
Testable:
- Z.AI profile in tmux should now have ANTHROPIC_AUTH_TOKEN properly set
- All tmux sessions should work identically to regular sessions
- Environment variable expansion should work in both regular and tmux modes
…available Previous behavior: CLI crashed with unhandled 404 error when machine registration endpoint missing on server What changed: - src/api/api.ts (lines 123-184): wrap axios.post() in try-catch block - Catch only 404 errors specifically (axios.isAxiosError && status === 404) - Return local machine object with same encryption keys when 404 occurs - All other errors (network, auth, 5xx) rethrow unchanged to preserve existing error handling Why: Server endpoint changed to api.cluster-fluster.com (commit 0e2577c) but new server lacks POST /v1/machines endpoint. Mobile app uses GET /v1/machines (works), but CLI uses POST (returns 404). This enables development to continue when server endpoint unavailable. Security analysis: - Encryption keys derived identically in both paths (this.credential.encryption.machineKey) - Return type unchanged: Promise<Machine> with same fields - No plaintext exposure (404 path sends nothing to network - more secure) - MachineMetadata contains only JSON-serializable strings (no data loss from skipping encrypt/decrypt round-trip) - Version fields (0) are standard initial values for first sync - dataEncryptionKey skipped (expected - used for remote session sync, not needed when server unavailable) Caller impact: - runClaude.ts:68 - return value unused (zero impact) - runCodex.ts:93 - return value unused (zero impact) - daemon/run.ts:643 - uses machine.id, encryptionKey, encryptionVariant (all provided identically) Testable: Run `happy-dev` - should show warning "Machine registration endpoint not available (404)" and continue instead of crashing
…ck mapping Previous behavior: Codex crashed with undefined approvalPolicy/sandbox when receiving Claude-specific permission modes (bypassPermissions, acceptEdits, plan) from GUI What changed: - runClaude.ts (lines 174-189): Map Codex modes to Claude equivalents with defensive fallback - yolo → bypassPermissions (full access equivalent) - safe-yolo → default (conservative: ask for permissions) - read-only → default (Claude lacks read-only, ask for permissions) - Unknown modes → default with warning log - runCodex.ts (lines 67, 146): Use shared PermissionMode type from api/types for cross-agent compatibility - runCodex.ts (lines 150-158): Remove restrictive validation, accept all permission modes (will be mapped in switch) - runCodex.ts (lines 616-644): Add defensive fallback cases for Claude modes - bypassPermissions → approval='on-failure', sandbox='danger-full-access' (yolo equivalent) - acceptEdits → approval='on-request', sandbox='workspace-write' (let model decide) - plan → approval='untrusted', sandbox='workspace-write' (conservative) - default case for unknown modes Why: GUI could send incompatible permission modes in edge cases (backward compatibility, saved sessions, manual API calls). Without defensive fallback, Codex received undefined approvalPolicy/sandbox causing session failures. Mappings prioritize safety while preserving closest semantic equivalents. Semantic mapping rationale: - bypassPermissions ≈ yolo: Both skip all permissions with full access - acceptEdits ≈ on-request approval: Let model decide when to ask (closest to auto-approve edits) - plan ≈ untrusted: Conservative fallback (Codex lacks planning mode) - safe-yolo ≈ default: Conservative fallback (different failure-handling semantics) - read-only → default: Claude lacks read-only mode, use safe interactive default Files affected: - src/claude/runClaude.ts: Add Codex→Claude mode mapping - src/codex/runCodex.ts: Add Claude→Codex mode mapping + use shared PermissionMode type Testable: Send bypassPermissions mode to Codex session - should map to yolo behavior (danger-full-access + on-failure) instead of crashing
….71.0 - Bump claude-code from 2.0.24 to 2.0.53 (latest) - Bump sdk from 0.65.0 to 0.71.0 - Fix MessageQueue.ts to import SDK types from local module instead of @anthropic-ai/claude-code (SDK entrypoint was removed in 2.0.25) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
…istence-profile-integration-01WqaAvCxRr6eWW2Wu33e8xP
Adds environment variable for explicit Claude CLI path override. Detection priority: HAPPY_CLAUDE_PATH > PATH > npm > Bun > Homebrew > native This allows users to specify a custom Claude CLI location without modifying their PATH.
Merges feature/bun-support-claude-cli-detection branch with: - Bun runtime detection and CLI discovery - HAPPY_CLAUDE_PATH env var override - PATH fallback from tiann's PR slopus#83 (stdio suppression, existence check) - Source detection (npm, Bun, Homebrew, native installer) Detection priority: HAPPY_CLAUDE_PATH > PATH > npm > Bun > Homebrew > native Also fixes test issues: - apiSession.test.ts: vi.hoisted pattern for mock hoisting - claudeLocal.test.ts: check spawn args instead of array mutation - runtime.test.ts: ESM imports instead of require() Credit: @tiann (slopus#83)
Previous behavior: Test checked if original claudeArgs array was mutated to remove --continue, but filter() creates a new array. What changed: Check mockSpawn.mock.calls[0][1] (actual spawn arguments) instead of the original claudeArgs array. Why: The implementation uses filter() which returns a new array rather than mutating the original. The test should verify what actually gets passed to spawn, not internal implementation details.
When --resume is passed without a session ID, Happy now delegates to
Claude's native session picker instead of auto-finding the last session.
Changes:
- extractFlag: return {found: false} when value required but missing
- Spawn args: check for --resume in claudeArgs before adding --session-id
This preserves all existing behavior:
- --resume <id>: extracts ID, calls onSessionFound(), passes to Claude
- --continue: still auto-finds last session
- New sessions: still generate UUID
Fixes picker mode for interactive session selection.
Merges the minimal --resume picker fix into the feature branch. Resolves conflicts in claudeLocal.test.ts by keeping both test changes.
…etOrCreateSession() Previous behavior: getOrCreateSession() at src/api/api.ts:120 threw on all HTTP 5xx errors, causing Happy to crash with "Error: Failed to get or create session: Request failed with status code 500" instead of continuing in offline mode. What changed: - src/api/api.ts:120-132 - Added 5xx error handling before final throw - Check if error is axios error with response status >= 500 - Call connectionState.fail() with error details - Return null to enable offline mode with auto-reconnect - Matches existing pattern from getOrCreateMachine() lines 234-242 - src/api/api.test.ts:174-218 - Added 3 new test cases - Test 500 Internal Server Error returns null - Test 503 Service Unavailable returns null - Verify existing throw behavior for non-5xx errors Why: When Happy server returns transient errors (500, 503, etc.), users should be able to continue in local mode instead of experiencing a crash. This matches the established pattern where getOrCreateMachine() already handles 5xx errors gracefully by returning a minimal machine object. Files affected: - src/api/api.ts - getOrCreateSession() error handling (14 lines added) - src/api/api.test.ts - Test coverage for 5xx errors (46 lines added) Testing: - TDD Red: New tests failed with "Failed to get or create session: Unknown error" - TDD Green: All 9 api.test.ts tests pass after fix - Full suite: 146 tests pass, no regressions - Manual: happy --resume with server 500 error continues in offline mode Impact: - Users can now use Happy when server has transient errors - Offline mode with auto-reconnect activates automatically for 5xx errors - 401/403/400 errors still throw (require user action, not offline-eligible) - Preserves existing behavior for network errors (ECONNREFUSED, ETIMEDOUT)
…new-session-wizard-ux-improvements-merged
Summary: Integrates PR slopus#99 (hook-based session tracking) and PR slopus#98 (Gemini/ACP integration) while preserving all offline mode and robustness features from the feature branch, plus DRY improvements. What changed: - Session hook server system for reliable Claude session ID capture - Gemini backend as alternative to Claude/Codex with ACP integration - Fixed memory leak in session.ts (stored interval reference + cleanup()) - Added socket connection check in apiSession.ts - Created shared offlineSessionStub.ts utility (DRY refactor) - Added gemini to profile compatibility validation - Preserved: offline mode, 5xx handling, profile system, tmux integration Files affected: - src/claude/runClaude.ts: Hook server + offline fallback integration - src/claude/session.ts: RAII cleanup, callback system, hookSettingsPath - src/claude/loop.ts: hookSettingsPath threading - src/claude/claudeLocal.ts: Hook parameters - src/claude/claudeRemote.ts: mapToClaudeMode() preserved - src/api/api.ts: Graceful 5xx handling preserved - src/api/apiSession.ts: Socket check restored - src/daemon/run.ts: Gemini added to agent selection - src/persistence.ts: Added gemini to ProfileCompatibilitySchema - src/utils/offlineSessionStub.ts: New shared DRY utility - src/codex/runCodex.ts: Updated to use shared offline stub - src/gemini/runGemini.ts: Added offline mode with shared stub - src/agent/*, src/gemini/*: New from main (no conflicts) - scripts/session_hook_forwarder.cjs: Hook forwarder from main Testable: npm test (266 pass, 8 fail - baseline unchanged)
Summary: Extract shared patterns between Codex and Gemini backends into
reusable base classes and utilities, eliminating ~886 lines of duplicate
code across permission handlers, reasoning processors, and session setup.
Previous behavior:
- Codex and Gemini had nearly identical permission handlers (~178/244 lines)
- Codex and Gemini had nearly identical reasoning processors (~263/279 lines)
- Session metadata creation duplicated in runCodex.ts and runGemini.ts
- Offline reconnection setup duplicated in both run files
- PermissionMode type duplicated in gemini/types.ts
- Missing connectionState.setBackend('Gemini') call
What changed:
- package.json: Update scripts to use $npm_execpath for package manager portability
- src/utils/BasePermissionHandler.ts: New abstract base class for permission handlers
- src/utils/BaseReasoningProcessor.ts: New abstract base class for reasoning processors
- src/utils/createSessionMetadata.ts: Extract shared session metadata creation
- src/utils/setupOfflineReconnection.ts: Extract offline reconnection pattern
- src/codex/utils/permissionHandler.ts: Simplified to 57 lines extending base
- src/gemini/utils/permissionHandler.ts: Simplified to 141 lines extending base
- src/codex/utils/reasoningProcessor.ts: Simplified to 44 lines extending base
- src/gemini/utils/reasoningProcessor.ts: Simplified to 47 lines extending base
- src/codex/runCodex.ts: Use createSessionMetadata + setupOfflineReconnection
- src/gemini/runGemini.ts: Add connectionState.setBackend + use shared utilities
- src/gemini/types.ts: Remove duplicate PermissionMode, import from @/api/types
Why:
- Single source of truth for permission/reasoning behavior
- Easier to add new backends (just extend base classes)
- Reduced maintenance burden from duplicate code drift
- Package manager portability for npm/yarn/bun users
Files affected:
- 4 new utility files in src/utils/
- 8 modified files with simplified implementations
- Net: 886 lines removed, 667 lines added = 219 lines saved
Testable: yarn build && yarn test (242 passed, 12 skipped)
… issues Previous behavior: - Test files using module-scope mock variables failed with "Cannot access before initialization" because vi.mock() factories are hoisted above variable declarations - expandEnvVars.test.ts failed with "logger.warn is not a function" due to missing logger mock - claude_version_utils.test.ts failed on macOS due to /tmp symlink to /private/tmp - Permission handlers held stale session references after onSessionSwap during offline reconnection, causing RPC handlers to register on disconnected sessions - reset() method had potential race condition with concurrent calls and Map iteration during mutation What changed: - Use vi.hoisted() in api.test.ts, apiSession.test.ts, claudeLocal.test.ts to ensure mock functions are available when vi.mock() factories execute - Add logger mock with all methods (debug, warn, info, error) to expandEnvVars.test.ts - Use fs.realpathSync() in claude_version_utils.test.ts to normalize macOS symlinks - Add updateSession() method to BasePermissionHandler for session reference updates - Update onSessionSwap callbacks in runCodex.ts and runGemini.ts to call permissionHandler.updateSession() when session is swapped - Add isResetting guard to reset() for idempotent behavior - Snapshot Map entries before iteration in reset() to prevent mutation during loop Why: - Vitest hoists vi.mock() factories to top of file, requiring vi.hoisted() for mock function declarations that the factory references - Logger mock needed because expandEnvVars imports and uses logger - macOS creates /tmp as symlink to /private/tmp, causing path comparison failures - After offline reconnection, onSessionSwap swaps the session but permission handlers still referenced the old disconnected session - Concurrent cleanup operations could cause undefined behavior in reset() Files affected: - src/api/api.test.ts: vi.hoisted() for mockPost, mockIsAxiosError - src/api/apiSession.test.ts: vi.hoisted() for mockIo - src/claude/claudeLocal.test.ts: vi.hoisted() for mockSpawn, mockClaudeFindLastSession - src/utils/expandEnvVars.test.ts: Add vi.mock for @/ui/logger - scripts/claude_version_utils.test.ts: Use realpathSync for path comparison - src/utils/BasePermissionHandler.ts: Add updateSession(), idempotent reset() - src/codex/runCodex.ts: Call permissionHandler.updateSession() in onSessionSwap - src/gemini/runGemini.ts: Call permissionHandler.updateSession() in onSessionSwap Test results: 242 passed, 12 skipped (daemon integration tests skip when server unreachable)
…e Code versions Previous behavior: --continue flag created new sessions instead of resuming existing ones. Two bugs caused this: 1. claudeCheckSession() only validated sessions with 'uuid' field (Claude Code 2.1.x format), rejecting sessions from older Claude Code versions that use 'messageId' or 'leafUuid' fields 2. claudeLocal.ts checked hasUserSessionControl AFTER extracting session flags, preventing --resume from being added to Claude CLI arguments in hook mode Result: claudeFindLastSession() found valid session ID, but --resume flag was not passed to Claude Code, causing new session creation instead of resumption. What changed: - src/claude/utils/claudeCheckSession.ts: Accept uuid, messageId, leafUuid fields + error logging - Lines 20-30: Parse JSON once, check uuid OR messageId OR leafUuid (with length > 0) - Line 21: Skip empty lines explicitly (not parse errors) - Line 32: Log malformed JSON errors per project convention (claudeRemoteLauncher.ts pattern) - Line 38: Log final validation result (valid/invalid) for transparency - src/claude/utils/claudeFindLastSession.ts: Add error logging + update documentation - Line 5: Import logger module - Line 10: Update JSDoc to document all supported field types (uuid, messageId, leafUuid) - Line 49: Log errors in catch block per project logging convention - src/claude/claudeLocal.ts: Fix --resume flag passing in hook mode - Lines 184-189: Remove hasUserSessionControl condition that blocked --resume - Add --resume to args when startFrom exists (user flags already extracted, must re-add) - Fix: hasUserSessionControl set before extraction but checked after, blocking --resume - src/claude/utils/claudeCheckSession.test.ts: Add 17 unit tests covering all scenarios - Claude 2.1.x sessions (uuid field), older Claude sessions (messageId), summaries (leafUuid) - Edge cases (empty strings, null, missing fields, malformed JSON, empty files) - File system edge cases (non-existent sessions, large files) - src/claude/utils/claudeFindLastSession.test.ts: Add 14 integration tests - Session finding with multiple formats, mtime sorting, UUID filtering - Mixed scenario tests (valid + invalid sessions) - scripts/test-continue-fix.sh: Add manual verification script for session finder Why: Claude Code changed message ID field names between versions. Older Claude Code uses 'messageId', version 2.1.x uses 'uuid', summary lines use 'leafUuid'. Session validation must check all three field types for backwards compatibility with sessions created by any Claude Code version. In hook mode, session flags are extracted from claudeArgs array before spawning Claude CLI. The hasUserSessionControl variable was set before extraction but checked after when building args, incorrectly preventing --resume from being added even though flags were removed. Added error logging to all catch blocks following project convention (claudeRemoteLauncher.ts, claudeLocalLauncher.ts pattern). Files affected: - src/claude/utils/claudeCheckSession.ts: Multi-format validation + logging (14 lines modified) - src/claude/utils/claudeFindLastSession.ts: Error logging + JSDoc (3 lines modified) - src/claude/claudeLocal.ts: Fix hook mode --resume passing (4 lines modified) - src/claude/utils/claudeCheckSession.test.ts: Comprehensive unit tests (188 lines, new) - src/claude/utils/claudeFindLastSession.test.ts: Integration tests (210 lines, new) - scripts/test-continue-fix.sh: Manual verification script (79 lines, new) Testable: - Run unit/integration tests: npm test -- src/claude/utils/ - Test --continue behavior: happy --continue "test message" - Verify --resume in args: grep "Args.*--resume" logs - Expected: Session resumes correctly (not new session created)
Previous behavior:
- Bash handler validated all cwd paths against working directory
- validatePath("/", workingDir) failed security check (/ outside working dir)
- CLI detection command sent with cwd="/" was rejected
- Handler returned { success: false, error: "Access denied..." } immediately
- Never executed "command -v claude/codex/gemini"
- GUI received failure, set all cliAvailability to null (unknown)
- Profile greying used wrong logic: codex profiles enabled, claude greyed
What changed:
- registerCommonHandlers.ts:157: Add special case for cwd="/" in validation
- registerCommonHandlers.ts:169: Map cwd="/" to undefined (shell default)
- registerCommonHandlers.ts:172: Log actual cwd value used in options
- registerCommonHandlers.ts:173-186: Add complete result logging (executing, result)
- registerCommonHandlers.ts:204-224: Add timeout and error logging
- RpcHandlerManager.ts:72-77: Add handler call/return/response logging
- codexMcpClient.ts:21-48: Return null when codex not installed (conservative)
- codexMcpClient.ts:92-105: Add clear error when codex CLI missing
Why:
- CLI detection needs to run with shell default PATH (cwd="/")
- Security preserved: all non-root paths still validated
- Follows Node.js convention: undefined cwd = shell default
- Enables proper CLI detection for claude, codex, gemini
Files affected:
- src/modules/common/registerCommonHandlers.ts: Path validation special case + logging
- src/api/rpc/RpcHandlerManager.ts: RPC lifecycle logging
- src/codex/codexMcpClient.ts: Null return on detection failure + clear error
Testable:
- Open new session wizard in GUI
- Daemon log shows: "Shell command executing... { cwd: undefined }"
- Daemon log shows: "Shell command result: { success: true, exitCode: 0 }"
- GUI console shows: "Parsed CLI status: { claude: true, codex: false, ... }"
- Claude profiles enabled, codex profiles greyed with warning banner
…verflow Previous behavior: - Custom deepEqual with unbounded recursion (no circular ref protection) - Infinite recursion when tool inputs contained circular references - Stack overflow in background Claude sessions: "RangeError: Maximum call stack size exceeded" - Wrapper function in utils/deepEqual.ts used only in one place What changed: - permissionHandler.ts:8: Import isDeepStrictEqual directly from node:util - permissionHandler.ts:14: Remove deepEqual import (no longer needed) - permissionHandler.ts:269: Use isDeepStrictEqual directly at call site - deepEqual.ts: Now unused (can be deleted in cleanup) Why: - DRY: No wrapper needed, use Node.js built-in directly - Zero new dependencies (node:util is built-in) - Handles circular refs safely (tested: returns true for circular objects) - More performant (C++ implementation vs JavaScript) - Idiomatic Node.js/TypeScript practice - WOLOG: Works identically for all tool types Files affected: - src/claude/utils/permissionHandler.ts: Use isDeepStrictEqual directly - src/utils/deepEqual.ts: Now unused (wrapper removed) Testable: - Run Claude in bypass permissions mode with complex tool calls - High token contexts (150k+) should work without stack overflow - Tool call matching still works correctly
Previous behavior: - Session.cleanup() was never called after session loop completed, causing keepAliveInterval to run indefinitely (memory leak) - sendClaudeSessionMessage() did not check socket.connected before emitting, causing messages to be silently lost when disconnected What changed: - src/claude/runClaude.ts: Add currentSession?.cleanup() calls in both the SIGINT handler (line 381) and after the main loop completes (line 457) to properly release intervals and callbacks - src/api/apiSession.ts: Add socket.connected check before emit in sendClaudeSessionMessage() to prevent silent message loss and log a debug message when messages cannot be sent Why: Prevents memory leaks from orphaned intervals and makes message delivery failures visible in logs for debugging
Adds support for specifying the JavaScript runtime used to spawn Claude Code. The new --js-runtime flag accepts 'node' (default) or 'bun'. Usage: happy --js-runtime bun Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ux-improvements-merged Feature/new session wizard ux improvements merged
📝 WalkthroughWalkthroughThis pull request introduces a comprehensive development environment setup system, offline session resilience, and centralized permission/reasoning handling. Changes include new development scripts, environment configuration utilities, backend-agnostic offline reconnection with exponential backoff, permission and reasoning processor base classes for code reuse, runtime detection utilities, tmux session management, and extensive test coverage across all new components. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant CLI as CLI
participant API as API Client
participant OfflineRec as Offline Reconnection
participant SessionStub as Session Stub
participant RealSession as Real Session
User->>CLI: Start agent (server unavailable)
CLI->>API: getOrCreateSession()
API-->>CLI: null (network error)
CLI->>OfflineRec: setupOfflineReconnection()
OfflineRec->>SessionStub: createOfflineSessionStub()
SessionStub-->>CLI: Offline session (sessionId: offline-*)
CLI->>User: Begin session (offline mode)
OfflineRec->>OfflineRec: Start exponential backoff loop
loop Health check & retry (~5s–60s)
OfflineRec->>API: Health check request
alt Server recovers
API-->>OfflineRec: Success
OfflineRec->>OfflineRec: Cancel backoff
OfflineRec->>API: getOrCreateSession()
API-->>RealSession: Real session
OfflineRec->>CLI: onSessionSwap(realSession)
CLI->>CLI: Update session reference
CLI->>User: Notify reconnection success
else Network still down
API-->>OfflineRec: Network error
OfflineRec->>OfflineRec: Increase backoff delay
else Auth failure (401)
API-->>OfflineRec: Auth error
OfflineRec->>User: Notify: re-authenticate required
OfflineRec->>OfflineRec: Cancel reconnection
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
src/utils/spawnHappyCLI.ts (1)
89-104: Apply runtime-specific CLI flags instead of using Node.js-only flags for Bun.Bun does not support the
--no-warningsCLI flag (only--no-deprecation). Passing--no-warningsto Bun may cause unexpected behavior. Conditionally apply these flags based on the runtime to ensure compatibility:♻️ Suggested conditional flag handling
- // Use the same Node.js flags that the wrapper script uses - const nodeArgs = [ - '--no-warnings', - '--no-deprecation', - entrypoint, - ...args - ]; + // Apply runtime-specific flags + const runtimeArgs = isBun() + ? [entrypoint, ...args] // Bun doesn't support --no-warnings + : ['--no-warnings', '--no-deprecation', entrypoint, ...args];src/codex/codexMcpClient.ts (1)
240-250: Documentation doesn't match implementation.The docstring at line 242 states the function "Returns null if codex is not installed" but
getCodexMcpCommand()has return typestringand always returns either'mcp-server'or'mcp', nevernull. The null check is actually performed inconnect()usingversionInfo.raw.📝 Suggested fix for documentation
/** * Get the correct MCP subcommand based on installed codex version * Versions >= 0.43.0-alpha.5 use 'mcp-server', older versions use 'mcp' - * Returns null if codex is not installed or version cannot be determined + * Defaults to 'mcp-server' if version cannot be determined */src/index.ts (1)
489-495: Potential Windows compatibility issue withexecFileSyncon a.cjsscript.On Windows,
execFileSynccannot rely on the shebang (#!/usr/bin/env node) to determine the interpreter. The script may fail to execute because Windows doesn't recognize shebangs natively.🔧 Suggested fix for cross-platform compatibility
try { - const claudeHelp = execFileSync(claudeCliPath, ['--help'], { encoding: 'utf8' }) + const claudeHelp = execFileSync(process.execPath, [claudeCliPath, '--help'], { encoding: 'utf8' }) console.log(claudeHelp) } catch (e) {Alternatively, use
shell: true:- const claudeHelp = execFileSync(claudeCliPath, ['--help'], { encoding: 'utf8' }) + const claudeHelp = execFileSync(claudeCliPath, ['--help'], { encoding: 'utf8', shell: true })src/persistence.ts (1)
384-386: Potential data loss:updateSettingsbypasses schema version enforcement.
updateSettingswrites via directwriteFile(line 385) without callingwriteSettings, so theschemaVersionenforcement fromwriteSettingsis bypassed. If an updater returns settings withoutschemaVersion, the file could be written without it.🔧 Proposed fix: Ensure schemaVersion is always written
// Write atomically using rename - await writeFile(tmpFile, JSON.stringify(updated, null, 2)); + const settingsWithVersion = { + ...updated, + schemaVersion: updated.schemaVersion ?? SUPPORTED_SCHEMA_VERSION + }; + await writeFile(tmpFile, JSON.stringify(settingsWithVersion, null, 2)); await rename(tmpFile, configuration.settingsFile); // Atomic on POSIX
🤖 Fix all issues with AI agents
In @docs/bug-fix-plan-2025-01-15-athundt.md:
- Around line 146-157: The mock returned by mockSpawn.mockReturnValue defines
the `on` property twice which overwrites the first; remove the duplicate `on:
vi.fn()` so the process mock has a single `on: vi.fn()` and keep `addListener`,
`removeListener`, `kill`, plus the stream mocks `stdout: { on: vi.fn() }`,
`stderr: { on: vi.fn() }`, and `stdin: { on: vi.fn(), end: vi.fn() }` to
preserve expected event handlers.
In @scripts/claude_version_utils.test.ts:
- Around line 35-38: The test for detectSourceFromPath is asserting 'npm' for a
path that lacks "claude-code" and therefore should return 'PATH'; either change
the expected value to 'PATH' for the current test input (the path containing
"@babel/core") or alter the test path to include "claude-code" under
node_modules (so detectSourceFromPath will legitimately return 'npm'); update
the assertion correspondingly and keep the test name accurate to reflect which
scenario is being validated.
In @scripts/ripgrep_launcher.cjs:
- Around line 39-55: The loop that tries each command uses execFileSync in
ripgrep_launcher.cjs but passes stdio: 'ignore', so execFileSync cannot capture
stdout into the result; change the execFileSync call in that loop (the one
creating const result = execFileSync(cmd, args, {...})) to allow capturing
output by removing stdio: 'ignore' or setting stdio: 'pipe' (keep encoding:
'utf8'), then use result.trim().split('\n') as before to extract the first path;
keep the existing try/catch behavior so failed commands still fall through.
In @scripts/test-continue-fix.sh:
- Around line 27-66: The script currently calls readdirSync(projectDir) without
verifying projectDir exists; add an existence check (e.g., using existsSync or
try/catch around statSync) immediately before the readdirSync call and if the
directory is missing log a clear error and exit non‑zero; update the block that
builds files (look for the readdirSync(projectDir) line and the projectDir
variable) to early-return/exit with a helpful message when the directory does
not exist so the subsequent file reads do not throw an unhelpful exception.
In @src/api/api.test.ts:
- Around line 221-237: The console.log spy is created after calling
api.getOrCreateSession so it misses any logs; move the vi.spyOn(console, 'log')
call (and its mockImplementation) to before invoking api.getOrCreateSession in
the "should re-throw non-connection errors" test (keep consoleSpy variable name
and restore it after the assertion), then perform the
expect(consoleSpy).not.toHaveBeenCalledWith(...) assertion to ensure no
offline-mode message was logged; keep mockPost mocked to reject with authError
and ensure consoleSpy.mockRestore() runs afterwards.
In @src/api/api.ts:
- Around line 202-210: The Machine type's metadata property is declared as
MachineMetadata but code in the deserialization (the Machine object
construction) assigns either a decrypted value or null; update the Machine type
declaration so metadata: MachineMetadata | null to match the runtime behavior
(similar to how daemonState is already nullable), and run type checks to ensure
any code consuming Machine handles the nullable metadata.
In @src/api/apiSession.ts:
- Around line 197-201: The send methods are inconsistent about handling a
disconnected socket: some (the Claude send block) early-return on
!this.socket.connected while sendCodexMessage logs but still emits and
sendAgentMessage/sendSessionEvent lack checks; add a private helper (e.g.,
canSend(context: string): boolean) that checks this.socket.connected and calls
logger.debug(`[API] Socket not connected, cannot send ${context}. Message will
be lost`) then returns false, and use that helper at the start of
sendCodexMessage, sendAgentMessage, sendSessionEvent and the Claude send method
to uniformly early-return when it returns false, ensuring no emits happen when
disconnected.
In @src/claude/claudeLocal.ts:
- Around line 173-189: The code inside the opts.hookSettingsPath false branch
redeclares hasResumeFlag, shadowing the outer variable and risking confusion;
remove the inner declaration and use the existing hasResumeFlag (or rename
consistently) when checking for resume flags in the block that builds args (the
branch that pushes '--session-id' or '--resume'), i.e., replace `const
hasResumeFlag = ...` with a reference to the outer hasResumeFlag so the check
`else if (!hasResumeFlag && newSessionId)` uses the intended variable.
In @src/claude/runClaude.ts:
- Around line 131-148: The AbortController created inline when calling
claudeLocal is never aborted; create a named AbortController (e.g., const
abortController = new AbortController()) before the try, pass
abortController.signal to claudeLocal instead of new AbortController().signal,
and wire process signal handlers (process.on('SIGINT') and
process.on('SIGTERM')) to call abortController.abort() and perform the same
cleanup (reconnection.cancel() and stopCaffeinate()) so the offline session can
be interrupted; keep the existing finally block to ensure cleanup and remove any
unused inline AbortController.
In @src/daemon/run.ts:
- Line 3: Remove the unused import of the tmp module from the top of the file:
delete the line importing "tmp" (the import statement "import * as tmp from
'tmp';") in src/daemon/run.ts so the unused symbol is not retained.
In @src/utils/tmux.ts:
- Around line 447-459: The code in executeTmuxCommand builds fullCmd and
unconditionally appends '-t' when COMMANDS_SUPPORTING_TARGET matches, which
causes duplicate '-t' for commands whose templates in WIN_OPS already include
'-t'. Fix it by detecting whether the assembled cmd already contains a '-t' flag
(e.g., check cmd.includes('-t') or inspect the last token of cmd) before pushing
'-t' and the target; reference COMMANDS_SUPPORTING_TARGET, WIN_OPS,
executeTmuxCommand, baseCmd and fullCmd and ensure you only append '-t' and
target when no '-t' is already present.
- Around line 376-410: The TMUX parsing code incorrectly treats parts[1] as a
path and extracts a session/window from it; instead treat TMUX as
"socket_path,server_pid,pane_id" (so socket_path = parts[0], server_pid =
parts[1], pane = parts[2]) and do not derive session/window from parts[1]. Fix
the logic in the TMUX parsing function (the block using tmuxEnv, parts,
socketPath, sessionAndWindow, pane) to: 1) stop splitting parts[1] by '/' and
stop using it as session; 2) set socket_path = parts[0] and pane = parts[2]; and
3) obtain real session and window by querying tmux (e.g., use TMUX_PANE
environment variable if available or run tmux display-message -p
'#{session_name}' and '#{window_index}' via a safe exec wrapper) and include
fallback handling/logging on failure before returning { session, window, pane,
socket_path } or null.
🧹 Nitpick comments (39)
src/claude/utils/path.test.ts (2)
5-18: Consider using Vitest'svi.stubEnv()for environment variable manipulation.Vitest provides
vi.stubEnv()andvi.unstubAllEnvs()as the idiomatic approach for stubbing environment variables. This avoids reassigning the entireprocess.envobject and is automatically restored after each test.♻️ Suggested refactor using Vitest utilities
-import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { getProjectPath } from './path'; import { join } from 'node:path'; -// Store original env -const originalEnv = { ...process.env }; - describe('getProjectPath', () => { beforeEach(() => { - // Reset process.env to a clean state - make a fresh copy each time - process.env = { ...originalEnv }; - delete process.env.CLAUDE_CONFIG_DIR; + vi.unstubAllEnvs(); }); afterEach(() => { - // Restore original env - process.env = { ...originalEnv }; + vi.unstubAllEnvs(); }); it('should replace slashes with hyphens in the project path', () => { - process.env.CLAUDE_CONFIG_DIR = '/test/home/.claude'; + vi.stubEnv('CLAUDE_CONFIG_DIR', '/test/home/.claude');
20-54: Unit tests using Unix absolute paths may fail if run on Windows CI.Windows is explicitly tested in the smoke-test workflow, but unit tests are not currently run in CI. The tests hardcode Unix absolute paths (e.g.,
/Users/steve/projects/my-app), and on Windows,resolve()would prepend the drive letter (e.g.,C:\), causing the regex replacement to produceC-Users-steve-projects-my-appinstead of the expected-Users-steve-projects-my-app.If unit tests are run on Windows in the future, they will fail. Consider using relative paths or platform-aware path construction for these tests to ensure they pass across platforms.
CONTRIBUTING.md (1)
267-273: Add language specifier to fenced code block.The fenced code block is missing a language specifier, which helps with syntax highlighting and accessibility.
📝 Suggested fix
1. **Create profile in GUI:** - ``` + ```text - Open Happy mobile/web app - Settings → AI Backend Profiles - Create new profile with custom environment variables - Note the profile ID ```scripts/claude_version_utils.cjs (1)
104-109: Remove redundantrequire('path')statement.The
pathmodule is already imported at line 16. This redundant require adds unnecessary overhead.♻️ Suggested fix
function detectSourceFromPath(resolvedPath) { const normalized = resolvedPath.toLowerCase(); - const path = require('path'); - - // Use path.normalize() for proper cross-platform path handling - const normalizedPath = path.normalize(resolvedPath).toLowerCase(); + // Use path.normalize() for proper cross-platform path handling + const normalizedPath = require('path').normalize(resolvedPath).toLowerCase();Or better, just use the already-imported
path:function detectSourceFromPath(resolvedPath) { - const normalized = resolvedPath.toLowerCase(); - const path = require('path'); - // Use path.normalize() for proper cross-platform path handling - const normalizedPath = path.normalize(resolvedPath).toLowerCase(); + const normalizedPath = path.normalize(resolvedPath).toLowerCase();src/configuration.ts (1)
71-78: Console output may interfere with programmatic usage.The unconditional
console.logon startup could interfere with CLI output parsing when users pipe results to other tools or expect JSON output. Consider checking for TTY or adding an environment variable to suppress the banner.♻️ Suggested improvement
// Visual indicator on CLI startup (only if not daemon process to avoid log clutter) - if (!this.isDaemonProcess) { + if (!this.isDaemonProcess && process.stdout.isTTY) { if (variant === 'dev') { console.log('\x1b[33m🔧 DEV MODE\x1b[0m - Data: ' + this.happyHomeDir) } else { console.log('\x1b[32m✅ STABLE MODE\x1b[0m - Data: ' + this.happyHomeDir) } }src/utils/__tests__/runtime.test.ts (2)
5-7: Unnecessary mock clearing.
vi.clearAllMocks()has no effect since this test suite doesn't set up any mocks. Consider removing it or replacing with logic to reset the cached runtime state if test isolation is a concern.
9-35: Tests are effectively skipped on non-matching runtimes.The conditional guards mean these tests only execute assertions when running on the matching runtime. In standard CI (Node.js), only the Node.js test will actually assert anything. Consider adding a comment clarifying this is intentional, or using
it.skipIffor clearer intent.♻️ Example using skipIf for clarity
- it('detects Node.js runtime correctly', () => { - // Test actual runtime detection - if (process.versions.node && !process.versions.bun && !process.versions.deno) { - expect(getRuntime()).toBe('node'); - expect(isNode()).toBe(true); - expect(isBun()).toBe(false); - expect(isDeno()).toBe(false); - } - }); + it.skipIf(!process.versions.node || process.versions.bun || process.versions.deno)( + 'detects Node.js runtime correctly', + () => { + expect(getRuntime()).toBe('node'); + expect(isNode()).toBe(true); + expect(isBun()).toBe(false); + expect(isDeno()).toBe(false); + } + );bin/happy-dev.mjs (1)
17-19: Duplicated environment variable setup.The environment variables are set identically in both branches. Consider extracting to avoid duplication.
♻️ Suggested refactor
+// Set development environment variables +process.env.HAPPY_HOME_DIR = join(homedir(), '.happy-dev'); +process.env.HAPPY_VARIANT = 'dev'; + if (!hasNoWarnings || !hasNoDeprecation) { // Re-execute with the flags const __filename = fileURLToPath(import.meta.url); const scriptPath = join(dirname(__filename), '../dist/index.mjs'); - // Set development environment variables - process.env.HAPPY_HOME_DIR = join(homedir(), '.happy-dev'); - process.env.HAPPY_VARIANT = 'dev'; - try { execFileSync( process.execPath, @@ ... @@ } else { // Already have the flags, import normally - // Set development environment variables - process.env.HAPPY_HOME_DIR = join(homedir(), '.happy-dev'); - process.env.HAPPY_VARIANT = 'dev'; - await import('../dist/index.mjs'); }Also applies to: 37-38
src/utils/offlineSessionStub.ts (1)
34-53: Type assertion bypasses compile-time safety.The
as unknown as ApiSessionClientcast means that ifApiSessionClientinterface changes (new required methods added), this stub won't fail at compile time, potentially causing runtime errors.Consider using
satisfiesor implementing an explicit type check to catch interface drift:♻️ Suggested improvement for type safety
-export function createOfflineSessionStub(sessionTag: string): ApiSessionClient { - return { +export function createOfflineSessionStub(sessionTag: string): ApiSessionClient { + const stub: ApiSessionClient = { sessionId: `offline-${sessionTag}`, sendCodexMessage: () => {}, sendClaudeSessionMessage: () => {}, keepAlive: () => {}, sendSessionEvent: () => {}, sendSessionDeath: () => {}, updateLifecycleState: () => {}, requestControlTransfer: async () => {}, flush: async () => {}, close: async () => {}, updateMetadata: () => {}, updateAgentState: () => {}, onUserMessage: () => {}, rpcHandlerManager: { registerHandler: () => {} - } - } as unknown as ApiSessionClient; + } as ApiSessionClient['rpcHandlerManager'] + }; + return stub; }If the full interface is too complex, at minimum add a comment documenting that this stub must be updated when
ApiSessionClientchanges.src/utils/__tests__/runtimeIntegration.test.ts (2)
4-14: Caching test may not verify internal caching behavior.ES modules are cached by the runtime, so re-importing
../runtime.jsreturns the same module instance. BothgetRuntimeandgetRuntime2reference the identical function, making this test always pass regardless of whethergetRuntime()implements internal caching.To truly test the internal caching (the
cachedRuntimevariable inruntime.ts), you would need to either:
- Export a cache-clearing function for testing
- Use
vi.resetModules()between imports to force fresh module evaluationThat said, the test still validates consistency, which has value.
50-54: Duplicate test assertion.This test (
'provides correct runtime type') duplicates the assertion already made in line 13 of the first test. Consider removing this test or combining them.src/api/apiSession.test.ts (1)
59-66: Unused variableclient.The
clientvariable is assigned but never used. The test works because the constructor registers event listeners, but consider either:
- Prefixing with underscore:
const _client = ...- Adding a comment explaining the variable is intentionally unused
♻️ Minor fix
it('should emit correct events on socket connection', () => { - const client = new ApiSessionClient('fake-token', mockSession); + // Client instantiation triggers event listener registration + new ApiSessionClient('fake-token', mockSession); // Should have set up event listeners expect(mockSocket.on).toHaveBeenCalledWith('connect', expect.any(Function));scripts/__tests__/ripgrep_launcher.test.ts (2)
8-93: Consider simplifying test structure by removing redundantexpect().not.toThrow()wrappers.The outer
expect(() => { ... }).not.toThrow()pattern is unnecessary here since:
fs.readFileSyncwill throw if the file doesn't exist, causing the test to fail naturally- The inner
expect().toContain()assertions don't throw on failure—they report assertion errorsAdditionally,
fsandpatharerequire()'d repeatedly in each test. Extract them once at the module level.♻️ Suggested simplification
import { describe, it, expect, vi, beforeEach } from 'vitest'; +import * as fs from 'fs'; +import * as path from 'path'; + +const launcherContent = fs.readFileSync( + path.join(__dirname, '../ripgrep_launcher.cjs'), + 'utf8' +); describe('Ripgrep Launcher Runtime Compatibility', () => { beforeEach(() => { vi.clearAllMocks(); }); it('has correct file structure', () => { - expect(() => { - const fs = require('fs'); - const path = require('path'); - const content = fs.readFileSync(path.join(__dirname, '../ripgrep_launcher.cjs'), 'utf8'); - - expect(content).toContain('#!/usr/bin/env node'); - expect(content).toContain('ripgrepMain'); - expect(content).toContain('loadRipgrepNative'); - }).not.toThrow(); + expect(launcherContent).toContain('#!/usr/bin/env node'); + expect(launcherContent).toContain('ripgrepMain'); + expect(launcherContent).toContain('loadRipgrepNative'); }); // Apply similar pattern to remaining tests... });
4-6:vi.clearAllMocks()is unused.None of these tests create or use mocks, so this
beforeEachhook has no effect. Consider removing it to reduce noise.src/api/types.ts (1)
4-16: Good documentation of the type and its SDK boundary mapping.The JSDoc clearly explains the dual-mode nature (Claude vs Codex) and the mapping rules. The reminder to keep this in sync with
MessageMetaSchema.permissionModeis helpful.Consider extracting the enum values to a constant array to ensure single-source-of-truth:
♻️ Optional: Single source of truth for enum values
const PERMISSION_MODES = ['default', 'acceptEdits', 'bypassPermissions', 'plan', 'read-only', 'safe-yolo', 'yolo'] as const; export type PermissionMode = typeof PERMISSION_MODES[number]; // Then in MessageMetaSchema: permissionMode: z.enum(PERMISSION_MODES).optional(),src/utils/runtime.ts (2)
9-10: Consider exposing a cache reset for testability.The module-level cache prevents repeated detection but makes unit tests difficult to verify different runtime scenarios in isolation. Consider adding an internal reset function (e.g.,
_resetForTesting) or using a different pattern that allows test mocking.♻️ Optional: Add test reset capability
// Cache result after first detection (performance optimization) let cachedRuntime: Runtime | null = null; + +/** @internal Reset cache for testing only */ +export function _resetRuntimeCache(): void { + cachedRuntime = null; +}
30-44: Consider usingimport { process } from "node:process"instead of the globalprocessfor Deno compatibility.Deno exposes the
processglobal by default, so the optional chaining (process?.versions?.bun) is safe and functions correctly. However, Deno's linters discourage using the globalprocessand recommend importing from"node:process"instead. This is a minor style preference rather than a functional issue, but following this convention improves consistency with Deno best practices.src/claude/utils/permissionMode.ts (1)
19-26: Consider handling undefined or unknown modes more explicitly.The fallback
mode as ClaudeSdkPermissionModewill pass through any string, including invalid/typo modes like'defualt'. SincePermissionModeis a union type from@/api/types, this should be safe at compile time, but runtime values could slip through if the type isn't enforced at boundaries.The current implementation is pragmatic and works well for the documented 7 modes. If stricter validation is desired later, consider logging unknown modes or defaulting to
'default'.src/utils/serverConnectionErrors.test.ts (1)
244-256: Misleading comment about onCleanup behavior.The comment on lines 252-253 states that
onCleanupshould "only be called once per cancel() call," but the assertion on line 254 expects it to be called 3 times (once for eachcancel()invocation). This is actually correct behavior per the implementation, but the comment phrasing is confusing.Consider clarifying the comment:
- // onCleanup should still only be called once per cancel() call - // (cancel sets cancelled=true, preventing further action) + // onCleanup is called on each cancel() invocation + // (idempotent cancellation - safe to call multiple times) expect(onCleanup).toHaveBeenCalledTimes(3);scripts/ripgrep_launcher.cjs (1)
70-76: Minor:/opt/homebrew/bin/rgis macOS-specific, not Linux.The
/opt/homebrew/bin/rgpath in the Linux fallback list is Homebrew on Apple Silicon, not a Linux path. While harmless (the path won't exist on Linux), it's misleading.♻️ Suggested cleanup
} else if (process.platform === 'linux') { commonPaths.push( '/usr/bin/rg', - '/usr/local/bin/rg', - '/opt/homebrew/bin/rg' + '/usr/local/bin/rg' ); }src/utils/expandEnvVars.test.ts (1)
239-249: Consider adding a comment explaining theundefined as anycast.The cast is intentional to test edge case handling when source is undefined, but a brief comment would clarify this for future readers.
♻️ Suggested improvement
it('should handle undefined source environment gracefully', () => { const envVars = { TARGET: '${MISSING}' }; - // undefined source should fall back to process.env + // Test edge case: undefined source should fall back to process.env + // Using `as any` to bypass TypeScript and test runtime behavior const result = expandEnvironmentVariables(envVars, undefined as any);docs/bug-fix-plan-2025-01-15-athundt.md (1)
12-14: Add language specifiers to fenced code blocks.Several code blocks are missing language specifiers, which affects syntax highlighting and accessibility. Based on markdownlint hints.
♻️ Suggested fixes
Line 12: Add
textorplaintextfor the error message block.
Lines 25 and 38: Addtextorplaintextfor the pathway diagrams.-``` +```text Error: --session-id cannot be used with --continue or --resume-``` +```text user: happy --continue → index.ts (claudeArgs = ["--continue"])Also applies to: 25-35, 38-44
src/claude/utils/claudeFindLastSession.test.ts (1)
68-88: Unnecessaryasynckeyword.The test function is marked
asyncbut doesn't useawait. While harmless, it's inconsistent with other tests in this file.Suggested fix
- it('should find most recent session by mtime (uuid format)', async () => { + it('should find most recent session by mtime (uuid format)', () => {scripts/link-dev.cjs (1)
107-122: Broad string match for symlink validation.The check
linkTarget.includes('happy-cli')at line 111 could match unintended paths (e.g.,/some/happy-cli-fork/bin/happy-dev). Consider using a more precise check.Suggested improvement
- if (linkTarget === binSource || linkTarget.includes('happy-cli')) { + // Check exact match or if it points to our project's bin directory + if (linkTarget === binSource || linkTarget.includes('/happy-cli/bin/')) {src/claude/claudeLocal.ts (1)
54-79: In-place mutation ofopts.claudeArgsmay cause unexpected side effects.The
extractFlaghelper mutatesopts.claudeArgsviafilter()reassignment. This side effect could surprise callers who don't expect their input to be modified. Consider working on a copy or documenting this behavior explicitly.Alternative: work on a local copy
+ // Work on a copy to avoid mutating the original array + let claudeArgs = opts.claudeArgs ? [...opts.claudeArgs] : []; + const extractFlag = (flags: string[], withValue: boolean = false): { found: boolean; value?: string } => { - if (!opts.claudeArgs) return { found: false }; + if (!claudeArgs.length) return { found: false }; for (const flag of flags) { - const index = opts.claudeArgs.indexOf(flag); + const index = claudeArgs.indexOf(flag); if (index !== -1) { - if (withValue && index + 1 < opts.claudeArgs.length) { - const nextArg = opts.claudeArgs[index + 1]; + if (withValue && index + 1 < claudeArgs.length) { + const nextArg = claudeArgs[index + 1]; if (!nextArg.startsWith('-')) { const value = nextArg; - opts.claudeArgs = opts.claudeArgs.filter((_, i) => i !== index && i !== index + 1); + claudeArgs = claudeArgs.filter((_, i) => i !== index && i !== index + 1); return { found: true, value }; } } if (!withValue) { - opts.claudeArgs = opts.claudeArgs.filter((_, i) => i !== index); + claudeArgs = claudeArgs.filter((_, i) => i !== index); return { found: true }; } return { found: false }; } } return { found: false }; };src/utils/setupOfflineReconnection.ts (1)
99-102: Consider using logger instead of console.log for consistency.The
onNotifycallback usesconsole.logdirectly. For consistency with the rest of the codebase that uses theloggerutility, consider whether this should uselogger.infoinstead, especially since the comment mentions it "matches Claude's behavior."src/codex/runCodex.ts (1)
121-140: Forward reference pattern forpermissionHandleris unusual but functional.The
permissionHandleris declared at line 125 but assigned at line 465. This works because JavaScript hoistsletdeclarations, but the variable is in the temporal dead zone until assigned. TheonSessionSwapcallback checksif (permissionHandler)which is safe, but this pattern could be confusing.Consider documenting this pattern or restructuring to assign immediately after session setup if possible.
src/gemini/utils/permissionHandler.ts (1)
46-84: Consider extractingwriteToolsconstant outside the switch.The
writeToolsarray on line 76 is declared inside the switch case. While this works, extracting it as a class-level constant would improve readability and avoid re-allocation on each call.Suggested refactor
+const WRITE_TOOLS = ['write', 'edit', 'create', 'delete', 'patch', 'fs-edit']; + export class GeminiPermissionHandler extends BasePermissionHandler { // ... private shouldAutoApprove(toolName: string, toolCallId: string, input: unknown): boolean { // ... switch (this.currentPermissionMode) { // ... case 'read-only': - const writeTools = ['write', 'edit', 'create', 'delete', 'patch', 'fs-edit']; - const isWriteTool = writeTools.some(wt => toolName.toLowerCase().includes(wt)); + const isWriteTool = WRITE_TOOLS.some(wt => toolName.toLowerCase().includes(wt)); return !isWriteTool;src/daemon/run.ts (2)
379-465: Tmux spawning includes full process.env which may be excessive.Lines 400-405 copy the entire
process.envintotmuxEnv. While this ensures completeness, it may include sensitive variables or create unnecessarily large environment blocks. Consider filtering to only essential variables if this becomes problematic.
584-588: Unreachable code path may indicate logic gap.This return statement claims to be unreachable, but it can be reached if:
useTmuxistrueandtmuxSessionName !== undefinedon line 379- Tmux spawn succeeds (
tmuxResult.successis true) on line 416- But then the function returns from inside the promise on line 441-461
The code after line 466 (
if (!useTmux)) would not execute in this case. However, if tmux spawn fails anduseTmuxis set tofalseon line 464, then the regular spawn on line 469 executes.The comment is correct - this code should never execute. Consider adding a
throwinstead of a return for defensive programming.Suggested change
- // This should never be reached, but TypeScript requires a return statement - return { - type: 'error', - errorMessage: 'Unexpected error in session spawning' - }; + // This should never be reached - if we get here, there's a logic error + throw new Error('Unexpected code path in session spawning - this indicates a bug');src/persistence.ts (5)
109-111: Consider adding a guard for undefined compatibility field.If
profile.compatibilityis undefined (e.g., from malformed data bypassing Zod), this will throw. While Zod parsing should prevent this, defensive coding would be safer.♻️ Suggested defensive check
export function validateProfileForAgent(profile: AIBackendProfile, agent: 'claude' | 'codex' | 'gemini'): boolean { - return profile.compatibility[agent]; + return profile.compatibility?.[agent] ?? false; }
183-195: Potential issue:validateProfileVersionreturnsfalsefor valid profiles without explicit version.Line 186 returns
falsewhenprofile.versionis falsy, but the schema at line 103 sets a default of'1.0.0'. After Zod parsing,versionshould always be present. However, if this function is called on raw data before validation, it may produce unexpected results.💡 Consider documenting expected input state
// Profile version validation +// Note: Should only be called on Zod-validated profiles where `version` is guaranteed export function validateProfileVersion(profile: AIBackendProfile): boolean {
316-322: Redundant nullish coalescing:schemaVersionshould always be set.At line 319,
settings.schemaVersion ?? SUPPORTED_SCHEMA_VERSIONis applied, butreadSettings()already ensuresschemaVersionis present via the merge withdefaultSettings. This is harmless but slightly redundant.
618-623:setActiveProfiledoesn't validate that the profile ID exists.Setting an active profile ID that doesn't exist in
profileswould silently succeed but leave the system in an inconsistent state. Consider adding a validation step.♻️ Suggested validation
export async function setActiveProfile(profileId: string): Promise<void> { - await updateSettings(settings => ({ - ...settings, - activeProfileId: profileId - })); + await updateSettings(settings => { + const profileExists = settings.profiles.some(p => p.id === profileId); + if (!profileExists) { + logger.warn(`⚠️ Setting active profile to non-existent ID: ${profileId}`); + } + return { + ...settings, + activeProfileId: profileId + }; + }); }
649-667:getEnvironmentVariablesandgetProfileEnvironmentVariableshave overlapping logic.Both functions iterate over
profile.environmentVariablesto build aRecord<string, string>. Consider reusinggetProfileEnvironmentVariablesingetEnvironmentVariablesto reduce duplication and ensure consistency.♻️ Suggested refactor
export async function getEnvironmentVariables(profileId: string): Promise<Record<string, string>> { const settings = await readSettings(); const profile = settings.profiles.find(p => p.id === profileId); if (!profile) return {}; - // Start with profile's environment variables (new schema) - const envVars: Record<string, string> = {}; - if (profile.environmentVariables) { - profile.environmentVariables.forEach(envVar => { - envVars[envVar.name] = envVar.value; - }); - } + // Start with profile's environment variables using shared helper + const envVars = getProfileEnvironmentVariables(profile); // Override with CLI-local cached environment variables const localEnvVars = settings.localEnvironmentVariables[profileId] || {}; Object.assign(envVars, localEnvVars); return envVars; }src/utils/serverConnectionErrors.ts (1)
203-210: Clarify that the 4th parameter toexponentialBackoffDelayis NOT a retry limit.The comment at line 204 helps, but the parameter name
maxFailureCountin the imported function might confuse readers. Consider adding a note in the JSDoc header (lines 116-120) explicitly stating retries are unlimited.src/utils/tmux.ts (2)
139-141: Session name validation may be too restrictive.The regex
/^[a-zA-Z0-9._-]+$/rejects session names starting with digits, but tmux does allow numeric session names (e.g.,0,1). However, this is likely intentional for safety. Document the restriction if it's by design.
800-825: Environment variable escaping may be insufficient for all shell edge cases.The escaping at lines 816-820 handles backslashes, double quotes, dollar signs, and backticks. However, it may not handle all edge cases like newlines or null bytes in values. Consider validating values more strictly or using a shell-safe quoting library.
🔧 Consider adding newline validation
+ // Reject values with newlines as they could break command structure + if (value.includes('\n') || value.includes('\r')) { + logger.warn(`[TMUX] Skipping environment variable with newline: ${key}`); + continue; + } + // Escape value for shell safety // Must escape: backslashes, double quotes, dollar signs, backticks const escapedValue = valuepackage.json (1)
69-87: Comment-as-section-headers pattern is unconventional but functional.Using
"// ==== Section ====": ""as visual separators in scripts is creative. Note that these are technically valid npm scripts that do nothing when run. Consider whether this could cause confusion.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (71)
.envrc.example.gitignoreCONTRIBUTING.mdREADME.mdbin/happy-dev.mjsdocs/bug-fix-plan-2025-01-15-athundt.mdpackage.jsonscripts/__tests__/ripgrep_launcher.test.tsscripts/claude_version_utils.cjsscripts/claude_version_utils.test.tsscripts/env-wrapper.cjsscripts/link-dev.cjsscripts/ripgrep_launcher.cjsscripts/setup-dev.cjsscripts/test-continue-fix.shsrc/api/api.test.tssrc/api/api.tssrc/api/apiMachine.tssrc/api/apiSession.test.tssrc/api/apiSession.tssrc/api/rpc/RpcHandlerManager.tssrc/api/types.tssrc/claude/claudeLocal.test.tssrc/claude/claudeLocal.tssrc/claude/claudeRemote.tssrc/claude/claudeRemoteLauncher.tssrc/claude/loop.tssrc/claude/runClaude.tssrc/claude/sdk/utils.tssrc/claude/session.tssrc/claude/utils/claudeCheckSession.test.tssrc/claude/utils/claudeCheckSession.tssrc/claude/utils/claudeFindLastSession.test.tssrc/claude/utils/claudeFindLastSession.tssrc/claude/utils/path.test.tssrc/claude/utils/permissionHandler.tssrc/claude/utils/permissionMode.test.tssrc/claude/utils/permissionMode.tssrc/codex/codexMcpClient.tssrc/codex/runCodex.tssrc/codex/utils/permissionHandler.tssrc/codex/utils/reasoningProcessor.tssrc/commands/auth.tssrc/configuration.tssrc/daemon/run.tssrc/daemon/types.tssrc/gemini/runGemini.tssrc/gemini/types.tssrc/gemini/utils/permissionHandler.tssrc/gemini/utils/reasoningProcessor.tssrc/index.tssrc/modules/common/registerCommonHandlers.tssrc/persistence.tssrc/ui/auth.tssrc/ui/logger.tssrc/utils/BasePermissionHandler.tssrc/utils/BaseReasoningProcessor.tssrc/utils/__tests__/runtime.test.tssrc/utils/__tests__/runtimeIntegration.test.tssrc/utils/createSessionMetadata.tssrc/utils/deepEqual.tssrc/utils/expandEnvVars.test.tssrc/utils/expandEnvVars.tssrc/utils/offlineSessionStub.tssrc/utils/runtime.tssrc/utils/serverConnectionErrors.test.tssrc/utils/serverConnectionErrors.tssrc/utils/setupOfflineReconnection.tssrc/utils/spawnHappyCLI.tssrc/utils/tmux.test.tssrc/utils/tmux.ts
💤 Files with no reviewable changes (1)
- src/utils/deepEqual.ts
🧰 Additional context used
🧬 Code graph analysis (39)
src/utils/createSessionMetadata.ts (2)
src/api/types.ts (1)
Metadata(311-339)src/projectPath.ts (1)
projectPath(6-10)
src/utils/serverConnectionErrors.test.ts (1)
src/utils/serverConnectionErrors.ts (5)
startOfflineReconnection(140-230)connectionState(338-338)printOfflineWarning(343-346)isNetworkError(243-245)NETWORK_ERROR_CODES(237-240)
src/api/api.ts (4)
src/api/types.ts (2)
Session(180-189)Machine(226-234)src/utils/serverConnectionErrors.ts (1)
isNetworkError(243-245)src/configuration.ts (1)
configuration(90-90)src/lib.ts (2)
configuration(13-13)logger(12-12)
src/api/types.ts (1)
src/claude/loop.ts (1)
PermissionMode(12-12)
scripts/setup-dev.cjs (1)
scripts/env-wrapper.cjs (4)
fs(20-20)require(17-17)path(18-18)os(19-19)
src/ui/auth.ts (2)
src/configuration.ts (1)
configuration(90-90)src/api/encryption.ts (1)
encodeBase64(9-14)
src/utils/tmux.test.ts (1)
src/utils/tmux.ts (7)
parseTmuxSessionIdentifier(123-160)TmuxSessionIdentifierError(115-120)TmuxSessionIdentifier(108-112)formatTmuxSessionIdentifier(163-176)validateTmuxSessionIdentifier(1008-1018)buildTmuxSessionIdentifier(1023-1052)TmuxUtilities(356-933)
src/utils/spawnHappyCLI.ts (2)
scripts/ripgrep_launcher.cjs (1)
runtime(119-119)src/utils/runtime.ts (1)
isBun(51-51)
src/claude/utils/claudeFindLastSession.test.ts (1)
src/claude/utils/claudeFindLastSession.ts (1)
claudeFindLastSession(16-52)
src/api/rpc/RpcHandlerManager.ts (1)
src/api/encryption.ts (2)
encodeBase64(9-14)encrypt(180-186)
src/index.ts (1)
src/claude/claudeLocal.ts (1)
claudeCliPath(15-15)
src/utils/expandEnvVars.test.ts (1)
src/utils/expandEnvVars.ts (1)
expandEnvironmentVariables(29-96)
src/claude/sdk/utils.ts (1)
src/utils/runtime.ts (1)
isBun(51-51)
src/claude/claudeLocal.test.ts (1)
src/claude/claudeLocal.ts (1)
claudeLocal(17-329)
src/utils/__tests__/runtime.test.ts (1)
src/utils/runtime.ts (4)
getRuntime(16-48)isNode(52-52)isBun(51-51)isDeno(53-53)
src/utils/setupOfflineReconnection.ts (2)
src/api/types.ts (2)
Metadata(311-339)AgentState(341-363)src/utils/serverConnectionErrors.ts (1)
startOfflineReconnection(140-230)
src/claude/session.ts (1)
src/claude/runClaude.ts (1)
JsRuntime(32-32)
src/utils/__tests__/runtimeIntegration.test.ts (1)
src/utils/runtime.ts (4)
getRuntime(16-48)isNode(52-52)isBun(51-51)isDeno(53-53)
src/api/api.test.ts (2)
src/api/api.ts (1)
ApiClient(13-401)src/utils/serverConnectionErrors.ts (1)
connectionState(338-338)
src/daemon/run.ts (4)
src/persistence.ts (2)
validateProfileForAgent(109-111)getProfileEnvironmentVariables(113-160)src/utils/expandEnvVars.ts (1)
expandEnvironmentVariables(29-96)src/utils/tmux.ts (2)
isTmuxAvailable(945-953)getTmuxUtilities(938-943)src/daemon/types.ts (1)
TrackedSession(11-22)
src/claude/utils/claudeFindLastSession.ts (2)
src/claude/utils/path.ts (1)
getProjectPath(4-8)src/claude/utils/claudeCheckSession.ts (1)
claudeCheckSession(6-41)
src/configuration.ts (1)
scripts/env-wrapper.cjs (1)
variant(37-37)
src/claude/utils/claudeCheckSession.test.ts (1)
src/claude/utils/claudeCheckSession.ts (1)
claudeCheckSession(6-41)
scripts/__tests__/ripgrep_launcher.test.ts (1)
scripts/ripgrep_launcher.cjs (4)
fs(15-15)require(29-29)require(91-91)path(14-14)
src/api/apiSession.ts (1)
src/ui/logger.ts (1)
logger(235-235)
src/codex/runCodex.ts (6)
src/api/types.ts (1)
PermissionMode(16-16)src/utils/serverConnectionErrors.ts (1)
connectionState(338-338)src/utils/createSessionMetadata.ts (1)
createSessionMetadata(65-89)src/codex/utils/permissionHandler.ts (1)
CodexPermissionHandler(22-58)src/utils/setupOfflineReconnection.ts (1)
setupOfflineReconnection(77-110)src/daemon/controlClient.ts (1)
notifyDaemonSessionStarted(62-70)
src/modules/common/registerCommonHandlers.ts (1)
scripts/ripgrep_launcher.cjs (2)
result(41-44)result(92-95)
src/claude/runClaude.ts (5)
src/api/types.ts (2)
PermissionMode(16-16)Session(180-189)src/utils/serverConnectionErrors.ts (2)
connectionState(338-338)startOfflineReconnection(140-230)src/claude/utils/sessionScanner.ts (1)
createSessionScanner(20-143)src/claude/claudeLocal.ts (1)
claudeLocal(17-329)src/claude/session.ts (1)
Session(7-185)
src/utils/serverConnectionErrors.ts (1)
src/utils/time.ts (2)
delay(1-3)exponentialBackoffDelay(5-8)
src/claude/utils/path.test.ts (1)
src/claude/utils/path.ts (1)
getProjectPath(4-8)
scripts/claude_version_utils.test.ts (1)
scripts/claude_version_utils.cjs (5)
result(62-65)result(453-453)path(16-16)path(106-106)fs(17-17)
src/persistence.ts (3)
src/configuration.ts (1)
configuration(90-90)src/ui/logger.ts (1)
logger(235-235)scripts/env-wrapper.cjs (1)
env(65-70)
src/claude/claudeLocal.ts (1)
src/claude/utils/claudeFindLastSession.ts (1)
claudeFindLastSession(16-52)
scripts/claude_version_utils.cjs (2)
scripts/claude_local_launcher.cjs (2)
path(26-26)require(71-71)scripts/claude_remote_launcher.cjs (1)
require(14-14)
src/claude/loop.ts (1)
src/claude/runClaude.ts (1)
JsRuntime(32-32)
src/utils/BasePermissionHandler.ts (1)
src/api/types.ts (1)
AgentState(341-363)
src/gemini/runGemini.ts (5)
src/utils/serverConnectionErrors.ts (1)
connectionState(338-338)src/utils/createSessionMetadata.ts (1)
createSessionMetadata(65-89)src/api/apiSession.ts (1)
ApiSessionClient(14-453)src/utils/setupOfflineReconnection.ts (1)
setupOfflineReconnection(77-110)src/daemon/controlClient.ts (1)
notifyDaemonSessionStarted(62-70)
src/codex/utils/permissionHandler.ts (1)
src/ui/logger.ts (1)
logger(235-235)
src/claude/claudeRemote.ts (3)
src/claude/runClaude.ts (1)
JsRuntime(32-32)src/claude/sdk/types.ts (1)
QueryOptions(157-178)src/claude/utils/systemPrompt.ts (1)
systemPrompt(30-38)
🪛 Gitleaks (8.30.0)
src/utils/expandEnvVars.test.ts
[high] 226-226: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
[high] 234-234: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
🪛 markdownlint-cli2 (0.18.1)
CONTRIBUTING.md
268-268: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
docs/bug-fix-plan-2025-01-15-athundt.md
12-12: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
25-25: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
38-38: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
63-63: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: smoke-test-windows (20)
- GitHub Check: smoke-test-windows (24)
| mockSpawn.mockReturnValue({ | ||
| stdio: [null, null, null, null], | ||
| on: vi.fn(), | ||
| addListener: vi.fn(), | ||
| removeListener: vi.fn(), | ||
| kill: vi.fn(), | ||
| on: vi.fn(), | ||
| stdout: { on: vi.fn() }, | ||
| stderr: { on: vi.fn() }, | ||
| stdin: { on: vi.fn(), end: vi.fn() } | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate on property in mock object.
The mock object has on: vi.fn() defined twice (lines 149 and 152), which means the first definition is silently overwritten.
🐛 Proposed fix
mockSpawn.mockReturnValue({
stdio: [null, null, null, null],
- on: vi.fn(),
addListener: vi.fn(),
removeListener: vi.fn(),
kill: vi.fn(),
on: vi.fn(),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| mockSpawn.mockReturnValue({ | |
| stdio: [null, null, null, null], | |
| on: vi.fn(), | |
| addListener: vi.fn(), | |
| removeListener: vi.fn(), | |
| kill: vi.fn(), | |
| on: vi.fn(), | |
| stdout: { on: vi.fn() }, | |
| stderr: { on: vi.fn() }, | |
| stdin: { on: vi.fn(), end: vi.fn() } | |
| }); | |
| }); | |
| mockSpawn.mockReturnValue({ | |
| stdio: [null, null, null, null], | |
| addListener: vi.fn(), | |
| removeListener: vi.fn(), | |
| kill: vi.fn(), | |
| on: vi.fn(), | |
| stdout: { on: vi.fn() }, | |
| stderr: { on: vi.fn() }, | |
| stdin: { on: vi.fn(), end: vi.fn() } | |
| }); | |
| }); |
🤖 Prompt for AI Agents
In @docs/bug-fix-plan-2025-01-15-athundt.md around lines 146 - 157, The mock
returned by mockSpawn.mockReturnValue defines the `on` property twice which
overwrites the first; remove the duplicate `on: vi.fn()` so the process mock has
a single `on: vi.fn()` and keep `addListener`, `removeListener`, `kill`, plus
the stream mocks `stdout: { on: vi.fn() }`, `stderr: { on: vi.fn() }`, and
`stdin: { on: vi.fn(), end: vi.fn() }` to preserve expected event handlers.
| it('should detect npm with different scoped packages', () => { | ||
| const result = detectSourceFromPath('C:/Users/test/AppData/Roaming/npm/node_modules/@babel/core/cli.js'); | ||
| expect(result).toBe('npm'); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test assertion appears incorrect for non-claude-code paths.
This test uses a @babel/core path which doesn't contain claude-code. Based on the detectSourceFromPath implementation (lines 126-128), the function requires both node_modules and claude-code in the path to return 'npm'. This path should return 'PATH' instead.
🐛 Suggested fix
Either update the expected result:
it('should detect npm with different scoped packages', () => {
const result = detectSourceFromPath('C:/Users/test/AppData/Roaming/npm/node_modules/@babel/core/cli.js');
- expect(result).toBe('npm');
+ expect(result).toBe('PATH');
});Or use a claude-code path if testing npm detection is the intent:
- it('should detect npm with different scoped packages', () => {
- const result = detectSourceFromPath('C:/Users/test/AppData/Roaming/npm/node_modules/@babel/core/cli.js');
+ it('should detect npm on Windows with AppData path', () => {
+ const result = detectSourceFromPath('C:/Users/test/AppData/Roaming/npm/node_modules/@anthropic-ai/claude-code/cli.js');
expect(result).toBe('npm');
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('should detect npm with different scoped packages', () => { | |
| const result = detectSourceFromPath('C:/Users/test/AppData/Roaming/npm/node_modules/@babel/core/cli.js'); | |
| expect(result).toBe('npm'); | |
| }); | |
| it('should detect npm with different scoped packages', () => { | |
| const result = detectSourceFromPath('C:/Users/test/AppData/Roaming/npm/node_modules/@babel/core/cli.js'); | |
| expect(result).toBe('PATH'); | |
| }); |
🤖 Prompt for AI Agents
In @scripts/claude_version_utils.test.ts around lines 35 - 38, The test for
detectSourceFromPath is asserting 'npm' for a path that lacks "claude-code" and
therefore should return 'PATH'; either change the expected value to 'PATH' for
the current test input (the path containing "@babel/core") or alter the test
path to include "claude-code" under node_modules (so detectSourceFromPath will
legitimately return 'npm'); update the assertion correspondingly and keep the
test name accurate to reflect which scenario is being validated.
| const variant = process.argv[2]; | ||
| const command = process.argv[3]; | ||
| const args = process.argv.slice(4); | ||
|
|
||
| if (!variant || !VARIANTS[variant]) { | ||
| console.error('Usage: node scripts/env-wrapper.js <stable|dev> <command> [...args]'); | ||
| console.error(''); | ||
| console.error('Variants:'); | ||
| console.error(' stable - Production-ready version (data: ~/.happy/)'); | ||
| console.error(' dev - Development version (data: ~/.happy-dev/)'); | ||
| console.error(''); | ||
| console.error('Examples:'); | ||
| console.error(' node scripts/env-wrapper.js stable daemon start'); | ||
| console.error(' node scripts/env-wrapper.js dev auth login'); | ||
| process.exit(1); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing validation for required command argument.
If a user runs node scripts/env-wrapper.js stable without a command, command will be undefined. This gets stringified to "undefined" when passed to spawn(), causing confusing behavior.
🔧 Suggested fix
const variant = process.argv[2];
const command = process.argv[3];
const args = process.argv.slice(4);
-if (!variant || !VARIANTS[variant]) {
+if (!variant || !VARIANTS[variant] || !command) {
console.error('Usage: node scripts/env-wrapper.js <stable|dev> <command> [...args]');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const variant = process.argv[2]; | |
| const command = process.argv[3]; | |
| const args = process.argv.slice(4); | |
| if (!variant || !VARIANTS[variant]) { | |
| console.error('Usage: node scripts/env-wrapper.js <stable|dev> <command> [...args]'); | |
| console.error(''); | |
| console.error('Variants:'); | |
| console.error(' stable - Production-ready version (data: ~/.happy/)'); | |
| console.error(' dev - Development version (data: ~/.happy-dev/)'); | |
| console.error(''); | |
| console.error('Examples:'); | |
| console.error(' node scripts/env-wrapper.js stable daemon start'); | |
| console.error(' node scripts/env-wrapper.js dev auth login'); | |
| process.exit(1); | |
| } | |
| const variant = process.argv[2]; | |
| const command = process.argv[3]; | |
| const args = process.argv.slice(4); | |
| if (!variant || !VARIANTS[variant] || !command) { | |
| console.error('Usage: node scripts/env-wrapper.js <stable|dev> <command> [...args]'); | |
| console.error(''); | |
| console.error('Variants:'); | |
| console.error(' stable - Production-ready version (data: ~/.happy/)'); | |
| console.error(' dev - Development version (data: ~/.happy-dev/)'); | |
| console.error(''); | |
| console.error('Examples:'); | |
| console.error(' node scripts/env-wrapper.js stable daemon start'); | |
| console.error(' node scripts/env-wrapper.js dev auth login'); | |
| process.exit(1); | |
| } |
| for (const { cmd, args } of commands) { | ||
| try { | ||
| const result = execFileSync(cmd, args, { | ||
| encoding: 'utf8', | ||
| stdio: 'ignore' | ||
| }); | ||
|
|
||
| if (result) { | ||
| const paths = result.trim().split('\n').filter(Boolean); | ||
| if (paths.length > 0) { | ||
| return paths[0].trim(); | ||
| } | ||
| } | ||
| } catch { | ||
| // Command failed, try next one | ||
| continue; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
stdio: 'ignore' prevents capturing command output.
When stdio is set to 'ignore', execFileSync cannot capture the command output. The result variable will be an empty Buffer or undefined, causing the path detection to always fail and fall through to the common paths fallback.
🐛 Proposed fix
try {
const result = execFileSync(cmd, args, {
encoding: 'utf8',
- stdio: 'ignore'
+ stdio: ['pipe', 'pipe', 'ignore']
});🤖 Prompt for AI Agents
In @scripts/ripgrep_launcher.cjs around lines 39 - 55, The loop that tries each
command uses execFileSync in ripgrep_launcher.cjs but passes stdio: 'ignore', so
execFileSync cannot capture stdout into the result; change the execFileSync call
in that loop (the one creating const result = execFileSync(cmd, args, {...})) to
allow capturing output by removing stdio: 'ignore' or setting stdio: 'pipe'
(keep encoding: 'utf8'), then use result.trim().split('\n') as before to extract
the first path; keep the existing try/catch behavior so failed commands still
fall through.
| const files = readdirSync(projectDir) | ||
| .filter(f => f.endsWith('.jsonl')) | ||
| .map(f => { | ||
| const sessionId = f.replace('.jsonl', ''); | ||
| if (!uuidPattern.test(sessionId)) return null; | ||
| const sessionFile = join(projectDir, f); | ||
| const sessionData = readFileSync(sessionFile, 'utf-8').split('\\n'); | ||
| // Use NEW validation logic (with multi-format support) | ||
| const hasGoodMessage = sessionData.some((v) => { | ||
| try { | ||
| const parsed = JSON.parse(v); | ||
| return (typeof parsed.uuid === 'string' && parsed.uuid.length > 0) || | ||
| (typeof parsed.messageId === 'string' && parsed.messageId.length > 0) || | ||
| (typeof parsed.leafUuid === 'string' && parsed.leafUuid.length > 0); | ||
| } catch (e) { | ||
| return false; | ||
| } | ||
| }); | ||
| if (!hasGoodMessage) return null; | ||
| return { | ||
| name: f, | ||
| sessionId: sessionId, | ||
| mtime: statSync(sessionFile).mtime.getTime() | ||
| }; | ||
| }) | ||
| .filter(f => f !== null) | ||
| .sort((a, b) => b.mtime - a.mtime); | ||
| console.log('Valid sessions found:', files.length); | ||
| if (files.length > 0) { | ||
| console.log('Most recent session ID:', files[0].sessionId); | ||
| } else { | ||
| console.log('ERROR: No valid sessions found'); | ||
| process.exit(1); | ||
| } | ||
| " || { echo " ✗ Session finder test failed"; exit 1; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add existence check before reading project directory.
The script will fail with an unhelpful error if the project directory doesn't exist (e.g., for new users or fresh installs). Consider checking if the directory exists before attempting to read it.
🐛 Proposed fix
const projectDir = join(claudeConfigDir, 'projects', projectId);
+const { existsSync } = require('fs');
+if (!existsSync(projectDir)) {
+ console.log('No project directory found at:', projectDir);
+ console.log('This is expected for fresh installs with no prior sessions.');
+ process.exit(0);
+}
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\$/i;🤖 Prompt for AI Agents
In @scripts/test-continue-fix.sh around lines 27 - 66, The script currently
calls readdirSync(projectDir) without verifying projectDir exists; add an
existence check (e.g., using existsSync or try/catch around statSync)
immediately before the readdirSync call and if the directory is missing log a
clear error and exit non‑zero; update the block that builds files (look for the
readdirSync(projectDir) line and the projectDir variable) to early-return/exit
with a helpful message when the directory does not exist so the subsequent file
reads do not throw an unhelpful exception.
| if (!opts.hookSettingsPath) { | ||
| // Offline mode: We control session ID | ||
| const hasResumeFlag = opts.claudeArgs?.includes('--resume') || opts.claudeArgs?.includes('-r'); | ||
| if (startFrom) { | ||
| // Resume existing session (Claude preserves the session ID) | ||
| args.push('--resume', startFrom) | ||
| } else if (!hasResumeFlag && newSessionId) { | ||
| // New session with our generated UUID | ||
| args.push('--session-id', newSessionId) | ||
| } | ||
| } else { | ||
| // Normal mode with hook: Add --resume if we found a session to resume | ||
| // (Flags have been extracted, so we re-add --resume with the session ID we found) | ||
| if (startFrom) { | ||
| args.push('--resume', startFrom); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable shadowing: hasResumeFlag redeclared inside block.
Line 175 declares const hasResumeFlag which shadows the outer variable declared at line 37. This could lead to confusion and bugs if the outer variable's value is expected here.
Suggested fix
if (!opts.hookSettingsPath) {
// Offline mode: We control session ID
- const hasResumeFlag = opts.claudeArgs?.includes('--resume') || opts.claudeArgs?.includes('-r');
+ const hasResumeFlagInArgs = opts.claudeArgs?.includes('--resume') || opts.claudeArgs?.includes('-r');
if (startFrom) {
// Resume existing session (Claude preserves the session ID)
args.push('--resume', startFrom)
- } else if (!hasResumeFlag && newSessionId) {
+ } else if (!hasResumeFlagInArgs && newSessionId) {
// New session with our generated UUID
args.push('--session-id', newSessionId)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!opts.hookSettingsPath) { | |
| // Offline mode: We control session ID | |
| const hasResumeFlag = opts.claudeArgs?.includes('--resume') || opts.claudeArgs?.includes('-r'); | |
| if (startFrom) { | |
| // Resume existing session (Claude preserves the session ID) | |
| args.push('--resume', startFrom) | |
| } else if (!hasResumeFlag && newSessionId) { | |
| // New session with our generated UUID | |
| args.push('--session-id', newSessionId) | |
| } | |
| } else { | |
| // Normal mode with hook: Add --resume if we found a session to resume | |
| // (Flags have been extracted, so we re-add --resume with the session ID we found) | |
| if (startFrom) { | |
| args.push('--resume', startFrom); | |
| } | |
| } | |
| if (!opts.hookSettingsPath) { | |
| // Offline mode: We control session ID | |
| const hasResumeFlagInArgs = opts.claudeArgs?.includes('--resume') || opts.claudeArgs?.includes('-r'); | |
| if (startFrom) { | |
| // Resume existing session (Claude preserves the session ID) | |
| args.push('--resume', startFrom) | |
| } else if (!hasResumeFlagInArgs && newSessionId) { | |
| // New session with our generated UUID | |
| args.push('--session-id', newSessionId) | |
| } | |
| } else { | |
| // Normal mode with hook: Add --resume if we found a session to resume | |
| // (Flags have been extracted, so we re-add --resume with the session ID we found) | |
| if (startFrom) { | |
| args.push('--resume', startFrom); | |
| } | |
| } |
🤖 Prompt for AI Agents
In @src/claude/claudeLocal.ts around lines 173 - 189, The code inside the
opts.hookSettingsPath false branch redeclares hasResumeFlag, shadowing the outer
variable and risking confusion; remove the inner declaration and use the
existing hasResumeFlag (or rename consistently) when checking for resume flags
in the block that builds args (the branch that pushes '--session-id' or
'--resume'), i.e., replace `const hasResumeFlag = ...` with a reference to the
outer hasResumeFlag so the check `else if (!hasResumeFlag && newSessionId)` uses
the intended variable.
| try { | ||
| await claudeLocal({ | ||
| path: workingDirectory, | ||
| sessionId: null, | ||
| onSessionFound: (id) => { offlineSessionId = id; }, | ||
| onThinkingChange: () => {}, | ||
| abort: new AbortController().signal, | ||
| claudeEnvVars: options.claudeEnvVars, | ||
| claudeArgs: options.claudeArgs, | ||
| mcpServers: {}, | ||
| allowedTools: [] | ||
| }); | ||
| } finally { | ||
| reconnection.cancel(); | ||
| stopCaffeinate(); | ||
| } | ||
| process.exit(0); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AbortController signal is never triggered in offline mode.
The AbortController created on line 137 is passed to claudeLocal but never has its abort() method called. If the user wants to interrupt the offline session (e.g., via Ctrl+C), the signal won't be triggered. Consider wiring up signal handlers or using the same abort pattern as the main flow.
Suggested improvement
+ const offlineAbortController = new AbortController();
+
+ // Handle Ctrl+C in offline mode
+ const offlineCleanup = () => {
+ offlineAbortController.abort();
+ reconnection.cancel();
+ stopCaffeinate();
+ };
+ process.on('SIGINT', offlineCleanup);
+ process.on('SIGTERM', offlineCleanup);
+
try {
await claudeLocal({
path: workingDirectory,
sessionId: null,
onSessionFound: (id) => { offlineSessionId = id; },
onThinkingChange: () => {},
- abort: new AbortController().signal,
+ abort: offlineAbortController.signal,
claudeEnvVars: options.claudeEnvVars,
claudeArgs: options.claudeArgs,
mcpServers: {},
allowedTools: []
});
} finally {
+ process.off('SIGINT', offlineCleanup);
+ process.off('SIGTERM', offlineCleanup);
reconnection.cancel();
stopCaffeinate();
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| await claudeLocal({ | |
| path: workingDirectory, | |
| sessionId: null, | |
| onSessionFound: (id) => { offlineSessionId = id; }, | |
| onThinkingChange: () => {}, | |
| abort: new AbortController().signal, | |
| claudeEnvVars: options.claudeEnvVars, | |
| claudeArgs: options.claudeArgs, | |
| mcpServers: {}, | |
| allowedTools: [] | |
| }); | |
| } finally { | |
| reconnection.cancel(); | |
| stopCaffeinate(); | |
| } | |
| process.exit(0); | |
| } | |
| const offlineAbortController = new AbortController(); | |
| // Handle Ctrl+C in offline mode | |
| const offlineCleanup = () => { | |
| offlineAbortController.abort(); | |
| reconnection.cancel(); | |
| stopCaffeinate(); | |
| }; | |
| process.on('SIGINT', offlineCleanup); | |
| process.on('SIGTERM', offlineCleanup); | |
| try { | |
| await claudeLocal({ | |
| path: workingDirectory, | |
| sessionId: null, | |
| onSessionFound: (id) => { offlineSessionId = id; }, | |
| onThinkingChange: () => {}, | |
| abort: offlineAbortController.signal, | |
| claudeEnvVars: options.claudeEnvVars, | |
| claudeArgs: options.claudeArgs, | |
| mcpServers: {}, | |
| allowedTools: [] | |
| }); | |
| } finally { | |
| process.off('SIGINT', offlineCleanup); | |
| process.off('SIGTERM', offlineCleanup); | |
| reconnection.cancel(); | |
| stopCaffeinate(); | |
| } | |
| process.exit(0); |
🤖 Prompt for AI Agents
In @src/claude/runClaude.ts around lines 131 - 148, The AbortController created
inline when calling claudeLocal is never aborted; create a named AbortController
(e.g., const abortController = new AbortController()) before the try, pass
abortController.signal to claudeLocal instead of new AbortController().signal,
and wire process signal handlers (process.on('SIGINT') and
process.on('SIGTERM')) to call abortController.abort() and perform the same
cleanup (reconnection.cancel() and stopCaffeinate()) so the offline session can
be interrupted; keep the existing finally block to ensure cleanup and remove any
unused inline AbortController.
| @@ -1,5 +1,6 @@ | |||
| import fs from 'fs/promises'; | |||
| import os from 'os'; | |||
| import * as tmp from 'tmp'; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
rg -n 'tmp\.' src/daemon/run.tsRepository: leeroybrun/happy-cli
Length of output: 46
Remove unused tmp import.
The tmp module imported on line 3 is not used anywhere in this file and should be removed.
🤖 Prompt for AI Agents
In @src/daemon/run.ts at line 3, Remove the unused import of the tmp module from
the top of the file: delete the line importing "tmp" (the import statement
"import * as tmp from 'tmp';") in src/daemon/run.ts so the unused symbol is not
retained.
| // Parse TMUX environment: /tmp/tmux-1000/default,4219,0 | ||
| try { | ||
| const parts = tmuxEnv.split(','); | ||
| if (parts.length >= 3) { | ||
| const socketPath = parts[0]; | ||
| // Extract last component from path (JavaScript doesn't support negative array indexing) | ||
| const pathParts = parts[1].split('/'); | ||
| const sessionAndWindow = pathParts[pathParts.length - 1] || parts[1]; | ||
| const pane = parts[2]; | ||
|
|
||
| // Extract session name from session.window format | ||
| let session: string; | ||
| let window: string; | ||
| if (sessionAndWindow.includes('.')) { | ||
| const parts = sessionAndWindow.split('.', 2); | ||
| session = parts[0]; | ||
| window = parts[1] || "0"; | ||
| } else { | ||
| session = sessionAndWindow; | ||
| window = "0"; | ||
| } | ||
|
|
||
| return { | ||
| session, | ||
| window, | ||
| pane, | ||
| socket_path: socketPath | ||
| }; | ||
| } | ||
| } catch (error) { | ||
| logger.debug('[TMUX] Failed to parse TMUX environment variable:', error); | ||
| } | ||
|
|
||
| return null; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
TMUX environment variable format tmux server socket pid
💡 Result:
Short answer: the TMUX environment variable holds the server socket path up to the first comma; everything after the first comma is internal metadata used by tmux. To get the server PID and socket explicitly use tmux formats, e.g.:
- socket path: tmux display -p '#{socket_path}'. [1][2]
- server PID: tmux display -p '#{pid}'. [1][2]
Notes: tmux also sets TMUX_PANE for the current pane id (e.g. %1). [1]
Sources:
[1] tmux Wiki — Advanced Use (Special environment variables).
[2] tmux Wiki — Formats / manpage (format variables #{socket_path} and #{pid}).
TMUX environment variable parsing logic is fundamentally incorrect.
The TMUX environment variable format is socket_path,server_pid,pane_id, where parts[1] is the server PID (a number like "12345"), not a path. Lines 382–383 incorrectly attempt to split parts[1] by / and extract the session name from it. This fails because:
parts[1]is a numeric PID, not a filesystem path- The session name is not stored in the
TMUXvariable at all - The code returns the server PID as the session name, which is incorrect
To obtain the actual session name and window, use tmux commands like tmux display-message -p '#{session_name}' or the TMUX_PANE environment variable (format: %pane_id), not the TMUX variable itself.
🤖 Prompt for AI Agents
In @src/utils/tmux.ts around lines 376 - 410, The TMUX parsing code incorrectly
treats parts[1] as a path and extracts a session/window from it; instead treat
TMUX as "socket_path,server_pid,pane_id" (so socket_path = parts[0], server_pid
= parts[1], pane = parts[2]) and do not derive session/window from parts[1]. Fix
the logic in the TMUX parsing function (the block using tmuxEnv, parts,
socketPath, sessionAndWindow, pane) to: 1) stop splitting parts[1] by '/' and
stop using it as session; 2) set socket_path = parts[0] and pane = parts[2]; and
3) obtain real session and window by querying tmux (e.g., use TMUX_PANE
environment variable if available or run tmux display-message -p
'#{session_name}' and '#{window_index}' via a safe exec wrapper) and include
fallback handling/logging on failure before returning { session, window, pane,
socket_path } or null.
| // Non-send-keys commands | ||
| const fullCmd = [...baseCmd, ...cmd]; | ||
|
|
||
| // Add target specification for commands that support it | ||
| if (cmd.length > 0 && COMMANDS_SUPPORTING_TARGET.has(cmd[0])) { | ||
| let target = targetSession; | ||
| if (window) target += `:${window}`; | ||
| if (pane) target += `.${pane}`; | ||
| fullCmd.push('-t', target); | ||
| } | ||
|
|
||
| return this.executeCommand(fullCmd); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the file structure and find the WIN_OPS and COMMANDS_SUPPORTING_TARGET definitions
cd /tmp && git -C /repo log --oneline -1 2>/dev/null || echo "Not a git repo"
# Get file info
wc -l /repo/src/utils/tmux.tsRepository: leeroybrun/happy-cli
Length of output: 150
🏁 Script executed:
# Check for WIN_OPS definition
rg "WIN_OPS" /repo/src/utils/tmux.ts -A 20 -B 2Repository: leeroybrun/happy-cli
Length of output: 127
🏁 Script executed:
# Check for COMMANDS_SUPPORTING_TARGET definition
rg "COMMANDS_SUPPORTING_TARGET" /repo/src/utils/tmux.ts -A 10 -B 2Repository: leeroybrun/happy-cli
Length of output: 127
🏁 Script executed:
# Search for commands with -t in their definition (from the reviewer's verification request)
rg "'[^']+': '[^']*-t" /repo/src/utils/tmux.ts | head -30Repository: leeroybrun/happy-cli
Length of output: 127
🏁 Script executed:
# Look at the exact lines in question (447-459) with more context
sed -n '440,465p' /repo/src/utils/tmux.tsRepository: leeroybrun/happy-cli
Length of output: 130
🏁 Script executed:
# List files to understand repo structure
git ls-files | head -20Repository: leeroybrun/happy-cli
Length of output: 411
🏁 Script executed:
# Find tmux.ts file
fd "tmux.ts" .Repository: leeroybrun/happy-cli
Length of output: 83
🏁 Script executed:
# If file doesn't exist, check all TypeScript files in utils
fd "\.ts$" . | grep -i tmuxRepository: leeroybrun/happy-cli
Length of output: 108
🏁 Script executed:
# Get file size
wc -l ./src/utils/tmux.tsRepository: leeroybrun/happy-cli
Length of output: 88
🏁 Script executed:
# Look at lines 440-470 to understand context
sed -n '440,470p' ./src/utils/tmux.tsRepository: leeroybrun/happy-cli
Length of output: 1101
🏁 Script executed:
# Search for WIN_OPS definition
rg "WIN_OPS" ./src/utils/tmux.ts -A 25 -B 2Repository: leeroybrun/happy-cli
Length of output: 2048
🏁 Script executed:
# Search for COMMANDS_SUPPORTING_TARGET definition
rg "COMMANDS_SUPPORTING_TARGET" ./src/utils/tmux.ts -A 15 -B 2Repository: leeroybrun/happy-cli
Length of output: 1459
Remove duplicate -t flag logic for commands with pre-included target specification.
Commands like select-window, select-pane, and others in WIN_OPS already include -t in their value (e.g., 'select-window': 'select-window -t'). The code at lines 451-455 then appends another -t flag unconditionally for any command in COMMANDS_SUPPORTING_TARGET, resulting in duplicate flags:
['tmux', 'select-window', '-t', '-t', targetValue]
The code should either:
- Strip
-tfrom WIN_OPS values and apply it consistently in executeTmuxCommand, or - Check if the command already ends with
-tbefore appending it again.
🤖 Prompt for AI Agents
In @src/utils/tmux.ts around lines 447 - 459, The code in executeTmuxCommand
builds fullCmd and unconditionally appends '-t' when COMMANDS_SUPPORTING_TARGET
matches, which causes duplicate '-t' for commands whose templates in WIN_OPS
already include '-t'. Fix it by detecting whether the assembled cmd already
contains a '-t' flag (e.g., check cmd.includes('-t') or inspect the last token
of cmd) before pushing '-t' and the target; reference
COMMANDS_SUPPORTING_TARGET, WIN_OPS, executeTmuxCommand, baseCmd and fullCmd and
ensure you only append '-t' and target when no '-t' is already present.
Summary of ChangesHello @leeroybrun, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the Happy CLI's stability, developer experience, and multi-agent capabilities. Key improvements include a robust system for managing development and stable environments, resilient handling of server connectivity issues with an offline mode and automatic reconnection, and a unified approach to configuring AI backend profiles and environment variables. The changes also refine Claude session management, abstract common agent logic, and introduce seamless tmux integration for daemon-spawned sessions, collectively making the CLI more reliable and user-friendly. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This is a large pull request that introduces several major features and bug fixes. The changes for development/stable variants, offline mode with reconnection, and profile-based configurations are significant improvements. The bug fix for the --continue flag conflict in Claude sessions is well-implemented. The codebase is also improved through refactoring, such as creating base classes for permission and reasoning handlers.
I have two main points of feedback:
- A critical bug in the new
tmuxintegration where theTMUXenvironment variable is parsed incorrectly, which will lead to incorrect session targeting. - A minor code duplication issue in the new
happy-devwrapper script.
Overall, the quality of the changes is high, with good documentation and testing for most of the new features.
| detectTmuxEnvironment(): TmuxEnvironment | null { | ||
| const tmuxEnv = process.env.TMUX; | ||
| if (!tmuxEnv) { | ||
| return null; | ||
| } | ||
|
|
||
| // Parse TMUX environment: /tmp/tmux-1000/default,4219,0 | ||
| try { | ||
| const parts = tmuxEnv.split(','); | ||
| if (parts.length >= 3) { | ||
| const socketPath = parts[0]; | ||
| // Extract last component from path (JavaScript doesn't support negative array indexing) | ||
| const pathParts = parts[1].split('/'); | ||
| const sessionAndWindow = pathParts[pathParts.length - 1] || parts[1]; | ||
| const pane = parts[2]; | ||
|
|
||
| // Extract session name from session.window format | ||
| let session: string; | ||
| let window: string; | ||
| if (sessionAndWindow.includes('.')) { | ||
| const parts = sessionAndWindow.split('.', 2); | ||
| session = parts[0]; | ||
| window = parts[1] || "0"; | ||
| } else { | ||
| session = sessionAndWindow; | ||
| window = "0"; | ||
| } | ||
|
|
||
| return { | ||
| session, | ||
| window, | ||
| pane, | ||
| socket_path: socketPath | ||
| }; | ||
| } | ||
| } catch (error) { | ||
| logger.debug('[TMUX] Failed to parse TMUX environment variable:', error); | ||
| } | ||
|
|
||
| return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation of detectTmuxEnvironment incorrectly parses the TMUX environment variable. According to the tmux man page and common usage, the format is socket-path,pid,session-id.
The current code treats the second component (pid) as the session name and the third component (session-id) as the pane index. This is incorrect and will lead to wrong session/pane targeting when running inside a tmux session.
To fix this, you should parse the components according to the correct format. The session ID is the third component. To get the session name, window index, and pane index, you would typically need to execute a command like tmux display-message -p '#S,#{window_index},#{pane_index}'.
| // Set development environment variables | ||
| process.env.HAPPY_HOME_DIR = join(homedir(), '.happy-dev'); | ||
| process.env.HAPPY_VARIANT = 'dev'; | ||
|
|
||
| try { | ||
| execFileSync( | ||
| process.execPath, | ||
| ['--no-warnings', '--no-deprecation', scriptPath, ...process.argv.slice(2)], | ||
| { | ||
| stdio: 'inherit', | ||
| env: process.env | ||
| } | ||
| ); | ||
| } catch (error) { | ||
| // Exit with the same code as the subprocess | ||
| process.exit(error.status || 1); | ||
| } | ||
| } else { | ||
| // Already have the flags, import normally | ||
| // Set development environment variables | ||
| process.env.HAPPY_HOME_DIR = join(homedir(), '.happy-dev'); | ||
| process.env.HAPPY_VARIANT = 'dev'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The environment variables HAPPY_HOME_DIR and HAPPY_VARIANT are set in both the if and else blocks. This duplication can be avoided by setting them once before the conditional block, which would make the code cleaner and easier to maintain.
process.env.HAPPY_HOME_DIR = join(homedir(), '.happy-dev');
process.env.HAPPY_VARIANT = 'dev';
try {
execFileSync(
process.execPath,
['--no-warnings', '--no-deprecation', scriptPath, ...process.argv.slice(2)],
{
stdio: 'inherit',
env: process.env
}
);
} catch (error) {
// Exit with the same code as the subprocess
process.exit(error.status || 1);
}
} else {
// Already have the flags, import normally
await import('../dist/index.mjs');
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
--continueto--resume.Documentation
✏️ Tip: You can customize this high-level summary in your review settings.