-
Notifications
You must be signed in to change notification settings - Fork 4
Add spotlight mode for testing worktree changes in main repo #375
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
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,3 +39,6 @@ bin/ | |
| # Tmux | ||
| tmux-*.log | ||
| .playwright-mcp/.mcp.json | ||
|
|
||
| # Spotlight mode | ||
| .spotlight-active | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,6 +10,7 @@ import ( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "io" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "os/exec" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "path/filepath" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "runtime" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "sync" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -280,6 +281,21 @@ func (s *Server) handleRequest(req *jsonRPCRequest) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "required": []string{"context"}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Name: "taskyou_spotlight", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Description: "Enable spotlight mode to sync worktree changes back to the main repository for testing. This bridges the gap between isolated task development and application runtime by syncing git-tracked files to where your app runs. Use 'start' to enable, 'stop' to restore original state, 'sync' for manual sync, or 'status' to check current state.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| InputSchema: map[string]interface{}{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "type": "object", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "properties": map[string]interface{}{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "action": map[string]interface{}{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "type": "string", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "enum": []string{"start", "stop", "sync", "status"}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "description": "Action to perform: 'start' enables spotlight mode and syncs files, 'stop' disables and restores original state, 'sync' manually syncs files (while active), 'status' shows current spotlight state", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "required": []string{"action"}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -683,6 +699,48 @@ This saves future tasks from re-exploring the codebase.`}, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "taskyou_spotlight": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| action, _ := params.Arguments["action"].(string) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if action == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.sendError(id, -32602, "action is required") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Get current task | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| task, err := s.db.GetTask(s.taskID) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil || task == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.sendError(id, -32603, "Failed to get current task") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if task.WorktreePath == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.sendError(id, -32602, "Task has no worktree (spotlight requires a worktree)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Get the project directory (main repo) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| project, err := s.db.GetProjectByName(task.Project) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil || project == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.sendError(id, -32603, "Failed to get project directory") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mainRepoDir := project.Path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Handle spotlight actions | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result, err := s.handleSpotlight(action, task.WorktreePath, mainRepoDir) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.sendError(id, -32603, err.Error()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.db.AppendTaskLog(s.taskID, "system", fmt.Sprintf("Spotlight %s: %s", action, result)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.sendResult(id, toolCallResult{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Content: []contentBlock{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {Type: "text", Text: result}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.sendError(id, -32602, fmt.Sprintf("Unknown tool: %s", params.Name)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -715,3 +773,256 @@ func (s *Server) send(resp jsonRPCResponse) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.writer.Write(data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.writer.Write([]byte("\n")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // spotlightStateFile returns the path to the spotlight state file in the worktree. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func spotlightStateFile(worktreePath string) string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return filepath.Join(worktreePath, ".spotlight-active") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // isSpotlightActive checks if spotlight mode is currently active for the worktree. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func isSpotlightActive(worktreePath string) bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _, err := os.Stat(spotlightStateFile(worktreePath)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return err == nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // handleSpotlight handles spotlight mode operations. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (s *Server) handleSpotlight(action, worktreePath, mainRepoDir string) (string, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| switch action { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "start": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return s.spotlightStart(worktreePath, mainRepoDir) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "stop": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return s.spotlightStop(worktreePath, mainRepoDir) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "sync": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return s.spotlightSync(worktreePath, mainRepoDir) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "status": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return s.spotlightStatus(worktreePath, mainRepoDir) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "", fmt.Errorf("unknown spotlight action: %s", action) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+789
to
+806
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // spotlightStart enables spotlight mode and performs initial sync. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (s *Server) spotlightStart(worktreePath, mainRepoDir string) (string, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if isSpotlightActive(worktreePath) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "Spotlight mode is already active. Use 'sync' to sync changes or 'stop' to disable.", nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // First, stash any uncommitted changes in the main repo to preserve them | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stashCmd := exec.Command("git", "stash", "push", "-m", "spotlight-backup-"+time.Now().Format("20060102-150405")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stashCmd.Dir = mainRepoDir | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stashOutput, _ := stashCmd.CombinedOutput() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stashCreated := !strings.Contains(string(stashOutput), "No local changes") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // First, stash any uncommitted changes in the main repo to preserve them | |
| stashCmd := exec.Command("git", "stash", "push", "-m", "spotlight-backup-"+time.Now().Format("20060102-150405")) | |
| stashCmd.Dir = mainRepoDir | |
| stashOutput, _ := stashCmd.CombinedOutput() | |
| stashCreated := !strings.Contains(string(stashOutput), "No local changes") | |
| // First, stash any uncommitted changes in the main repo to preserve them. | |
| // Use `git diff --quiet` to robustly detect changes instead of parsing git's output. | |
| hasChanges := false | |
| // Check for unstaged changes | |
| diffCmd := exec.Command("git", "diff", "--quiet") | |
| diffCmd.Dir = mainRepoDir | |
| if err := diffCmd.Run(); err != nil { | |
| // Non-zero exit status indicates there are changes; treat other errors the same | |
| hasChanges = true | |
| } | |
| // Check for staged changes | |
| diffCachedCmd := exec.Command("git", "diff", "--cached", "--quiet") | |
| diffCachedCmd.Dir = mainRepoDir | |
| if err := diffCachedCmd.Run(); err != nil { | |
| hasChanges = true | |
| } | |
| stashCreated := false | |
| if hasChanges { | |
| stashCmd := exec.Command("git", "stash", "push", "-m", "spotlight-backup-"+time.Now().Format("20060102-150405")) | |
| stashCmd.Dir = mainRepoDir | |
| // Consider the stash "created" only if the command succeeds. | |
| if err := stashCmd.Run(); err == nil { | |
| stashCreated = true | |
| } | |
| } |
Copilot
AI
Feb 5, 2026
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 git commands 'git checkout .' and 'git clean -fd' are executed without checking their errors. If these commands fail, the main repository might not be properly restored, but the user would still receive a success message. The error should be captured and handled appropriately, potentially warning the user if restoration fails.
Copilot
AI
Feb 5, 2026
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.
If the stash pop fails, the spotlight state file is still removed. This means spotlight mode will appear to be inactive, but the main repo wasn't properly restored. Consider not removing the state file if stash pop fails, or at least providing a more prominent warning that manual intervention may be needed.
| if stashCreated { | |
| stashPopCmd := exec.Command("git", "stash", "pop") | |
| stashPopCmd.Dir = mainRepoDir | |
| if output, err := stashPopCmd.CombinedOutput(); err != nil { | |
| stashMsg = fmt.Sprintf("⚠️ Failed to restore stash: %s", string(output)) | |
| } else { | |
| stashMsg = "✓ Original main repo changes restored from stash" | |
| } | |
| } | |
| // Remove the state file | |
| os.Remove(spotlightStateFile(worktreePath)) | |
| stashPopFailed := false | |
| if stashCreated { | |
| stashPopCmd := exec.Command("git", "stash", "pop") | |
| stashPopCmd.Dir = mainRepoDir | |
| if output, err := stashPopCmd.CombinedOutput(); err != nil { | |
| stashPopFailed = true | |
| stashMsg = fmt.Sprintf("⚠️ Failed to restore stash: %s\n The main repo may not be fully restored. Please inspect the git state in %s and resolve manually.", strings.TrimSpace(string(output)), mainRepoDir) | |
| } else { | |
| stashMsg = "✓ Original main repo changes restored from stash" | |
| } | |
| } | |
| // Remove the state file only if restoration (including stash pop) succeeded | |
| if !stashPopFailed { | |
| os.Remove(spotlightStateFile(worktreePath)) | |
| } |
Copilot
AI
Feb 5, 2026
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 sync operation only copies files from the worktree to the main repo but doesn't handle file deletions. If a file is deleted in the worktree, it won't be deleted in the main repo during sync. This could lead to discrepancies where the main repo has files that were removed from the worktree. Consider using 'git diff --name-status' to detect deletions and remove those files from the main repo as well.
Copilot
AI
Feb 5, 2026
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 file sync operation doesn't validate or sanitize file paths before copying. If a git repository contains files with path traversal sequences (e.g., '../../../etc/passwd'), the filepath.Join could potentially write files outside the intended main repo directory. While git typically prevents such files from being tracked, consider adding path validation to ensure all destination paths are within the mainRepoDir using filepath.Clean and checking that the result starts with mainRepoDir.
| srcPath := filepath.Join(worktreePath, file) | |
| dstPath := filepath.Join(mainRepoDir, file) | |
| // Clean and validate relative path to prevent path traversal | |
| cleanFile := filepath.Clean(file) | |
| if cleanFile == ".." || strings.HasPrefix(cleanFile, ".."+string(os.PathSeparator)) || filepath.IsAbs(cleanFile) { | |
| failed++ | |
| continue | |
| } | |
| srcPath := filepath.Join(worktreePath, cleanFile) | |
| dstPath := filepath.Join(mainRepoDir, cleanFile) | |
| dstPath = filepath.Clean(dstPath) | |
| // Ensure destination path is within mainRepoDir | |
| prefix := mainRepoDir | |
| if prefix != "" { | |
| prefix = filepath.Clean(prefix) | |
| } | |
| if dstPath != prefix && !strings.HasPrefix(dstPath, prefix+string(os.PathSeparator)) { | |
| failed++ | |
| continue | |
| } |
Copilot
AI
Feb 5, 2026
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.
Comparing file contents using byte-to-byte string comparison (string(srcData) == string(dstData)) may not be the most efficient approach for large files. While functionally correct, consider using bytes.Equal(srcData, dstData) which is more idiomatic and can be more efficient as it doesn't require converting to strings.
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 'sync' action doesn't check if spotlight mode is active before syncing. This could lead to unexpected behavior where files are synced to the main repo without a stash being created first, potentially overwriting uncommitted changes. Consider adding a check like the one in 'stop' action to ensure spotlight is active before syncing.