Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions cmd/entire/cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,18 +279,19 @@ func GetLogLevel() string {
return settings.LogLevel
}

// IsMultiSessionWarningDisabled checks if multi-session warnings are disabled.
// Returns false (show warnings) by default if settings cannot be loaded or the key is missing.
func IsMultiSessionWarningDisabled() bool {
// IsMultiSessionWarningEnabled checks if multi-session warnings are enabled.
// Returns false (hide warnings) by default if settings cannot be loaded or the key is missing.
// This is an opt-in feature - users must explicitly enable it.
func IsMultiSessionWarningEnabled() bool {
settings, err := LoadEntireSettings()
if err != nil {
return false // Default: show warnings
return false // Default: warnings disabled
}
if settings.StrategyOptions == nil {
return false
}
if disabled, ok := settings.StrategyOptions["disable_multisession_warning"].(bool); ok {
return disabled
if enabled, ok := settings.StrategyOptions["enable_multisession_warning"].(bool); ok {
return enabled
}
return false
}
Expand Down
35 changes: 26 additions & 9 deletions cmd/entire/cli/hooks_claudecode_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ func parseAndLogHookInput() (*hookInputData, error) {
// checkConcurrentSessions checks for concurrent session conflicts and shows warnings if needed.
// Returns true if the hook should be skipped due to an unresolved conflict.
func checkConcurrentSessions(ag agent.Agent, entireSessionID string) (bool, error) {
// Check if warnings are disabled via settings
if IsMultiSessionWarningDisabled() {
// Check if warnings are enabled via settings (opt-in feature, disabled by default)
if !IsMultiSessionWarningEnabled() {
return false, nil
}

Expand Down Expand Up @@ -191,7 +191,7 @@ func checkConcurrentSessions(ag agent.Agent, entireSessionID string) (bool, erro

// Build message with other session's prompt if available
var message string
suppressHint := "\n\nTo suppress this warning in future sessions, run:\n entire enable --disable-multisession-warning"
suppressHint := "\n\nTo disable this warning, remove enable_multisession_warning from .entire/settings.json"
if otherPrompt != "" {
message = fmt.Sprintf("Another session is active: \"%s\"\n\nYou can continue here, but checkpoints from both sessions will be interleaved.\n\nTo resume the other session instead, exit Claude and run: %s%s\n\nPress the up arrow key to get your prompt back.", otherPrompt, resumeCmd, suppressHint)
} else {
Expand Down Expand Up @@ -285,9 +285,9 @@ func handleSessionInitErrors(ag agent.Agent, initErr error) error {
// Check for session ID conflict error (shadow branch has different session)
var sessionConflictErr *strategy.SessionIDConflictError
if errors.As(initErr, &sessionConflictErr) {
// If multi-session warnings are disabled, skip this error silently
// The user has explicitly opted to work with multiple concurrent sessions
if IsMultiSessionWarningDisabled() {
// If multi-session warnings are not enabled, skip this error silently
// This allows multiple sessions to proceed when warnings are disabled (opt-in)
if !IsMultiSessionWarningEnabled() {
return nil
}

Expand Down Expand Up @@ -322,9 +322,7 @@ func handleSessionInitErrors(ag agent.Agent, initErr error) error {
"Options:\n"+
"1. Commit your changes (git commit) to create a new base commit\n"+
"2. Run 'entire rewind reset' to discard the shadow branch and start fresh\n"+
"3. Resume the existing session: %s\n\n"+
"To suppress this warning in future sessions, run:\n"+
" entire enable --disable-multisession-warning",
"3. Resume the existing session: %s",
sessionConflictErr.ShadowBranch,
sessionConflictErr.ExistingSession,
sessionConflictErr.NewSession,
Expand Down Expand Up @@ -373,6 +371,17 @@ func captureInitialState() error {
if err := handleSessionInitErrors(hookData.agent, initErr); err != nil {
return err
}
// Error was handled (returned nil), exit early without calling OnPromptStart
// since session initialization failed
return nil
}
}

// If strategy implements PromptHooks, call OnPromptStart to check overlap
// This happens AFTER session initialization to ensure session state exists
if promptHooks, ok := strat.(strategy.PromptHooks); ok {
if err := promptHooks.OnPromptStart(hookData.entireSessionID); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to run prompt start hook: %v\n", err)
}
}

Expand Down Expand Up @@ -630,6 +639,14 @@ func commitWithMetadata() error {
return fmt.Errorf("failed to save changes: %w", err)
}

// If strategy implements PromptHooks, call OnPromptEnd to clean up state
// This happens AFTER SaveChanges to ensure the flag is only cleared after successful checkpoint
if promptHooks, ok := strat.(strategy.PromptHooks); ok {
if err := promptHooks.OnPromptEnd(entireSessionID); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to run prompt end hook: %v\n", err)
}
}

// Update session state with new transcript position for strategies that create
// commits on the active branch (auto-commit strategy). This prevents parsing old transcript
// lines on subsequent checkpoints.
Expand Down
6 changes: 3 additions & 3 deletions cmd/entire/cli/hooks_geminicli_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ func outputGeminiBlockingResponse(reason string) {
// If the warning was already shown, subsequent calls proceed normally (both sessions create interleaved checkpoints).
// Note: This function may call os.Exit(0) and not return if a blocking response is needed on first conflict.
func checkConcurrentSessionsGemini(entireSessionID string) {
// Check if warnings are disabled via settings
if IsMultiSessionWarningDisabled() {
// Check if warnings are enabled via settings (opt-in feature, disabled by default)
if !IsMultiSessionWarningEnabled() {
return
}

Expand Down Expand Up @@ -149,7 +149,7 @@ func checkConcurrentSessionsGemini(entireSessionID string) {

// Build message - matches Claude Code format but with Gemini-specific instructions
var message string
suppressHint := "\n\nTo suppress this warning in future sessions, run:\n entire enable --disable-multisession-warning"
suppressHint := "\n\nTo disable this warning, remove enable_multisession_warning from .entire/settings.json"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gemini CLI handlers missing overlap detection hook calls

Medium Severity

The PR adds PromptHooks interface with OnPromptStart() and OnPromptEnd() methods for shadow branch overlap detection and integrates them into Claude Code handlers, but the Gemini CLI handlers were not updated. The handleGeminiBeforeAgent() function is missing the OnPromptStart() call after InitializeSession, and commitGeminiSession() is missing the OnPromptEnd() call after SaveChanges. This means the shadow branch overlap detection feature (which prevents "ghost files") will not work for Gemini CLI users.

Additional Locations (1)

Fix in Cursor Fix in Web

if otherPrompt != "" {
message = fmt.Sprintf("Another session is active: \"%s\"\n\nYou can continue here, but checkpoints from both sessions will be interleaved.\n\nTo resume the other session instead, exit Gemini CLI and run: %s%s\n\nPress the up arrow key to get your prompt back.", otherPrompt, resumeCmd, suppressHint)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ func TestConcurrentSessionWarning_BlocksFirstPrompt(t *testing.T) {
env.GitAdd("README.md")
env.GitCommit("Initial commit")
env.GitCheckoutNewBranch("feature/test")
env.InitEntire(strategy.StrategyNameManualCommit)
// Enable multi-session warnings (opt-in feature)
env.InitEntireWithOptions(strategy.StrategyNameManualCommit, map[string]any{
"enable_multisession_warning": true,
})

// Start session A and create a checkpoint
sessionA := env.NewSession()
Expand Down Expand Up @@ -92,7 +95,10 @@ func TestConcurrentSessionWarning_SetsWarningFlag(t *testing.T) {
env.GitAdd("README.md")
env.GitCommit("Initial commit")
env.GitCheckoutNewBranch("feature/test")
env.InitEntire(strategy.StrategyNameManualCommit)
// Enable multi-session warnings (opt-in feature)
env.InitEntireWithOptions(strategy.StrategyNameManualCommit, map[string]any{
"enable_multisession_warning": true,
})

// Start session A and create a checkpoint
sessionA := env.NewSession()
Expand Down Expand Up @@ -136,7 +142,10 @@ func TestConcurrentSessionWarning_SubsequentPromptsSucceed(t *testing.T) {
env.GitAdd("README.md")
env.GitCommit("Initial commit")
env.GitCheckoutNewBranch("feature/test")
env.InitEntire(strategy.StrategyNameManualCommit)
// Enable multi-session warnings (opt-in feature)
env.InitEntireWithOptions(strategy.StrategyNameManualCommit, map[string]any{
"enable_multisession_warning": true,
})

// Start session A and create a checkpoint
sessionA := env.NewSession()
Expand Down Expand Up @@ -268,9 +277,9 @@ func TestConcurrentSessionWarning_DisabledViaSetting(t *testing.T) {
env.GitCommit("Initial commit")
env.GitCheckoutNewBranch("feature/test")

// Initialize Entire with multi-session warning disabled
// Initialize Entire with multi-session warning disabled (default behavior, explicit for clarity)
env.InitEntireWithOptions(strategy.StrategyNameManualCommit, map[string]any{
"disable_multisession_warning": true,
"enable_multisession_warning": false,
})

// Start session A and create a checkpoint
Expand Down Expand Up @@ -335,7 +344,10 @@ func TestConcurrentSessionWarning_ContainsSuppressHint(t *testing.T) {
env.GitAdd("README.md")
env.GitCommit("Initial commit")
env.GitCheckoutNewBranch("feature/test")
env.InitEntire(strategy.StrategyNameManualCommit)
// Enable multi-session warnings (opt-in feature)
env.InitEntireWithOptions(strategy.StrategyNameManualCommit, map[string]any{
"enable_multisession_warning": true,
})

// Start session A and create a checkpoint
sessionA := env.NewSession()
Expand Down Expand Up @@ -363,7 +375,7 @@ func TestConcurrentSessionWarning_ContainsSuppressHint(t *testing.T) {
}

// Verify the warning message contains the suppression hint
expectedHint := "entire enable --disable-multisession-warning"
expectedHint := "remove enable_multisession_warning"
if !strings.Contains(response.StopReason, expectedHint) {
t.Errorf("Warning message should contain suppression hint %q, got: %s", expectedHint, response.StopReason)
}
Expand All @@ -382,7 +394,10 @@ func TestConcurrentSessions_BothCondensedOnCommit(t *testing.T) {
env.GitAdd("README.md")
env.GitCommit("Initial commit")
env.GitCheckoutNewBranch("feature/test")
env.InitEntire(strategy.StrategyNameManualCommit)
// Enable multi-session warnings (opt-in feature)
env.InitEntireWithOptions(strategy.StrategyNameManualCommit, map[string]any{
"enable_multisession_warning": true,
})

// Session A: create checkpoint
sessionA := env.NewSession()
Expand Down
44 changes: 33 additions & 11 deletions cmd/entire/cli/integration_test/gemini_concurrent_session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ func TestGeminiConcurrentSessionWarning_BlocksFirstPrompt(t *testing.T) {
env.GitAdd("README.md")
env.GitCommit("Initial commit")
env.GitCheckoutNewBranch("feature/test")
env.InitEntireWithAgent(strategy.StrategyNameManualCommit, "gemini")
// Enable multi-session warnings (opt-in feature)
env.InitEntireWithAgentAndOptions(strategy.StrategyNameManualCommit, "gemini", map[string]any{
"enable_multisession_warning": true,
})

// Start session A and create a checkpoint
sessionA := env.NewGeminiSession()
Expand Down Expand Up @@ -99,7 +102,10 @@ func TestGeminiConcurrentSessionWarning_SetsWarningFlag(t *testing.T) {
env.GitAdd("README.md")
env.GitCommit("Initial commit")
env.GitCheckoutNewBranch("feature/test")
env.InitEntireWithAgent(strategy.StrategyNameManualCommit, "gemini")
// Enable multi-session warnings (opt-in feature)
env.InitEntireWithAgentAndOptions(strategy.StrategyNameManualCommit, "gemini", map[string]any{
"enable_multisession_warning": true,
})

// Start session A and create a checkpoint
sessionA := env.NewGeminiSession()
Expand Down Expand Up @@ -143,7 +149,10 @@ func TestGeminiConcurrentSessionWarning_SubsequentPromptsSucceed(t *testing.T) {
env.GitAdd("README.md")
env.GitCommit("Initial commit")
env.GitCheckoutNewBranch("feature/test")
env.InitEntireWithAgent(strategy.StrategyNameManualCommit, "gemini")
// Enable multi-session warnings (opt-in feature)
env.InitEntireWithAgentAndOptions(strategy.StrategyNameManualCommit, "gemini", map[string]any{
"enable_multisession_warning": true,
})

// Start session A and create a checkpoint
sessionA := env.NewGeminiSession()
Expand Down Expand Up @@ -278,7 +287,10 @@ func TestGeminiConcurrentSessionWarning_ResumeCommandFormat(t *testing.T) {
env.GitAdd("README.md")
env.GitCommit("Initial commit")
env.GitCheckoutNewBranch("feature/test")
env.InitEntireWithAgent(strategy.StrategyNameManualCommit, "gemini")
// Enable multi-session warnings (opt-in feature)
env.InitEntireWithAgentAndOptions(strategy.StrategyNameManualCommit, "gemini", map[string]any{
"enable_multisession_warning": true,
})

// Start session A and create a checkpoint
sessionA := env.NewGeminiSession()
Expand Down Expand Up @@ -331,8 +343,10 @@ func TestCrossAgentConcurrentSession_ClaudeSessionShowsClaudeResumeInGemini(t *t
env.GitAdd("README.md")
env.GitCommit("Initial commit")
env.GitCheckoutNewBranch("feature/test")
// Initialize with Claude Code agent first
env.InitEntireWithAgent(strategy.StrategyNameManualCommit, agent.AgentNameClaudeCode)
// Initialize with Claude Code agent first and enable multi-session warnings
env.InitEntireWithAgentAndOptions(strategy.StrategyNameManualCommit, agent.AgentNameClaudeCode, map[string]any{
"enable_multisession_warning": true,
})

// Start Claude session A and create a checkpoint
sessionA := env.NewSession()
Expand Down Expand Up @@ -410,8 +424,10 @@ func TestCrossAgentConcurrentSession_GeminiSessionShowsGeminiResumeInClaude(t *t
env.GitAdd("README.md")
env.GitCommit("Initial commit")
env.GitCheckoutNewBranch("feature/test")
// Initialize with Gemini agent first
env.InitEntireWithAgent(strategy.StrategyNameManualCommit, agent.AgentNameGemini)
// Initialize with Gemini agent first and enable multi-session warnings
env.InitEntireWithAgentAndOptions(strategy.StrategyNameManualCommit, agent.AgentNameGemini, map[string]any{
"enable_multisession_warning": true,
})

// Start Gemini session A and create a checkpoint
sessionA := env.NewGeminiSession()
Expand Down Expand Up @@ -556,7 +572,10 @@ func TestGeminiConcurrentSessionWarning_ContainsSuppressHint(t *testing.T) {
env.GitAdd("README.md")
env.GitCommit("Initial commit")
env.GitCheckoutNewBranch("feature/test")
env.InitEntireWithAgent(strategy.StrategyNameManualCommit, "gemini")
// Enable multi-session warnings (opt-in feature)
env.InitEntireWithAgentAndOptions(strategy.StrategyNameManualCommit, "gemini", map[string]any{
"enable_multisession_warning": true,
})

// Start session A and create a checkpoint
sessionA := env.NewGeminiSession()
Expand Down Expand Up @@ -584,7 +603,7 @@ func TestGeminiConcurrentSessionWarning_ContainsSuppressHint(t *testing.T) {
}

// Verify the warning message contains the suppression hint
expectedHint := "entire enable --disable-multisession-warning"
expectedHint := "remove enable_multisession_warning"
if !strings.Contains(response.Reason, expectedHint) {
t.Errorf("Warning message should contain suppression hint %q, got: %s", expectedHint, response.Reason)
}
Expand All @@ -603,7 +622,10 @@ func TestGeminiConcurrentSessions_BothCondensedOnCommit(t *testing.T) {
env.GitAdd("README.md")
env.GitCommit("Initial commit")
env.GitCheckoutNewBranch("feature/test")
env.InitEntireWithAgent(strategy.StrategyNameManualCommit, "gemini")
// Enable multi-session warnings (opt-in feature)
env.InitEntireWithAgentAndOptions(strategy.StrategyNameManualCommit, "gemini", map[string]any{
"enable_multisession_warning": true,
})

// Session A: create checkpoint
sessionA := env.NewGeminiSession()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,8 +414,10 @@ func TestShadow_MultipleConcurrentSessions(t *testing.T) {

env.GitCheckoutNewBranch("feature/test")

// Initialize AFTER branch switch
env.InitEntire(strategy.StrategyNameManualCommit)
// Initialize AFTER branch switch with multi-session warnings enabled (opt-in feature)
env.InitEntireWithOptions(strategy.StrategyNameManualCommit, map[string]any{
"enable_multisession_warning": true,
})

// Start first session
session1 := env.NewSession()
Expand Down
Loading