diff --git a/app/cli/changes_tui/replace.go b/app/cli/changes_tui/replace.go index 21e8de46..38cbab01 100644 --- a/app/cli/changes_tui/replace.go +++ b/app/cli/changes_tui/replace.go @@ -24,6 +24,8 @@ func (m changesUIModel) getReplacementOldDisplay() oldReplacementRes { oldContent := m.selectionInfo.currentRep.Old originalFile := m.selectionInfo.currentFilesBeforeReplacement.Files[m.selectionInfo.currentPath] + // log.Println(originalFile) + oldContent = strings.ReplaceAll(oldContent, "\\`\\`\\`", "```") originalFile = strings.ReplaceAll(originalFile, "\\`\\`\\`", "```") @@ -35,18 +37,23 @@ func (m changesUIModel) getReplacementOldDisplay() oldReplacementRes { panic("old content not found in full file") // should never happen } + // Convert originalFile to a slice of runes to properly handle multi-byte characters + originalFileRunes := []rune(originalFile) + toPrepend := "" numLinesPrepended := 0 for i := fileIdx - 1; i >= 0; i-- { - s := string(originalFile[i]) + s := string(originalFileRunes[i]) + // Prepend the string representation of the rune toPrepend = s + toPrepend - if originalFile[i] == '\n' { + if originalFileRunes[i] == '\n' { numLinesPrepended++ if numLinesPrepended == replacementPrependLines { break } } } + prependedToStart := strings.Index(originalFile, toPrepend) == 0 toPrepend = strings.TrimLeft(toPrepend, "\n") @@ -56,13 +63,15 @@ func (m changesUIModel) getReplacementOldDisplay() oldReplacementRes { toAppend := "" numLinesAppended := 0 - for i := fileIdx + len(oldContent); i < len(originalFile); i++ { - s := string(originalFile[i]) + // Convert originalFile to a slice of runes to properly handle multi-byte characters + for i := fileIdx + len([]rune(oldContent)); i < len(originalFileRunes); i++ { + s := string(originalFileRunes[i]) + if s == "\t" { s = " " } toAppend += s - if originalFile[i] == '\n' { + if originalFileRunes[i] == '\n' { numLinesAppended++ if numLinesAppended == replacementAppendLines { break @@ -80,8 +89,13 @@ func (m changesUIModel) getReplacementOldDisplay() oldReplacementRes { wrapWidth := m.changeOldViewport.Width - 6 toPrepend = wrap.String(toPrepend, wrapWidth) oldContent = wrap.String(oldContent, wrapWidth) + + // log.Println("toAppend", toAppend) + toAppend = wrap.String(toAppend, wrapWidth) + // log.Println("toAppend after wrap", toAppend) + toPrependLines := strings.Split(toPrepend, "\n") for i, line := range toPrependLines { toPrependLines[i] = color.New(color.FgWhite).Sprint(line) diff --git a/app/cli/changes_tui/selection.go b/app/cli/changes_tui/selection.go index bcb95ad3..274e245d 100644 --- a/app/cli/changes_tui/selection.go +++ b/app/cli/changes_tui/selection.go @@ -57,11 +57,16 @@ outer: var currentFilesBeforeReplacement *shared.CurrentPlanFiles var err error + // log.Println("currentRep: ", currentRep) + if currentRep == nil { currentFilesBeforeReplacement = m.currentPlan.CurrentPlanFiles } else { currentFilesBeforeReplacement, err = m.currentPlan.GetFilesBeforeReplacement(currentRep.Id) } + + // log.Println(spew.Sdump(currentFilesBeforeReplacement)) + if err != nil { err = fmt.Errorf("error getting current plan state: %v", err) log.Println(err) diff --git a/app/cli/cmd/branches.go b/app/cli/cmd/branches.go index 256bab33..2dee665d 100644 --- a/app/cli/cmd/branches.go +++ b/app/cli/cmd/branches.go @@ -53,12 +53,12 @@ func branches(cmd *cobra.Command, args []string) { for i, b := range branches { num := strconv.Itoa(i + 1) if b.Name == lib.CurrentBranch { - num = color.New(color.Bold, color.FgGreen).Sprint(num) + num = color.New(color.Bold, term.ColorHiGreen).Sprint(num) } var name string if b.Name == lib.CurrentBranch { - name = color.New(color.Bold, color.FgGreen).Sprint(b.Name) + " đ" + name = color.New(color.Bold, term.ColorHiGreen).Sprint(b.Name) + " đ" } else { name = b.Name } diff --git a/app/cli/cmd/build.go b/app/cli/cmd/build.go index 86de465d..1ed94d8d 100644 --- a/app/cli/cmd/build.go +++ b/app/cli/cmd/build.go @@ -44,8 +44,8 @@ func build(cmd *cobra.Command, args []string) { didBuild, err := plan_exec.Build(plan_exec.ExecParams{ CurrentPlanId: lib.CurrentPlanId, CurrentBranch: lib.CurrentBranch, - CheckOutdatedContext: func(cancelOpt bool, maybeContexts []*shared.Context) (bool, bool, bool) { - return lib.MustCheckOutdatedContext(cancelOpt, false, maybeContexts) + CheckOutdatedContext: func(maybeContexts []*shared.Context) (bool, bool) { + return lib.MustCheckOutdatedContext(false, maybeContexts) }, }, buildBg) diff --git a/app/cli/cmd/cd.go b/app/cli/cmd/cd.go index 85046efe..f0940a2a 100644 --- a/app/cli/cmd/cd.go +++ b/app/cli/cmd/cd.go @@ -111,7 +111,7 @@ func cd(cmd *cobra.Command, args []string) { // give the SetProjectPlan request some time to be sent before exiting time.Sleep(50 * time.Millisecond) - fmt.Println("â Changed current plan to " + color.New(color.FgGreen, color.Bold).Sprint(plan.Name)) + fmt.Println("â Changed current plan to " + color.New(term.ColorHiGreen, color.Bold).Sprint(plan.Name)) fmt.Println() term.PrintCmds("", "current") diff --git a/app/cli/cmd/changes.go b/app/cli/cmd/changes.go index c0491a81..07da2662 100644 --- a/app/cli/cmd/changes.go +++ b/app/cli/cmd/changes.go @@ -37,6 +37,8 @@ func changes(cmd *cobra.Command, args []string) { term.OutputErrorAndExit("Error getting current plan state: %s", apiErr.Msg) } + // log.Println(spew.Sdump(currentPlanState)) + for currentPlanState.HasPendingBuilds() { plansRunningRes, apiErr := api.Client.ListPlansRunning([]string{lib.CurrentProjectId}, false) @@ -76,8 +78,8 @@ func changes(cmd *cobra.Command, args []string) { didBuild, err := plan_exec.Build(plan_exec.ExecParams{ CurrentPlanId: lib.CurrentPlanId, CurrentBranch: lib.CurrentBranch, - CheckOutdatedContext: func(cancelOpt bool, maybeContexts []*shared.Context) (bool, bool, bool) { - return lib.MustCheckOutdatedContext(cancelOpt, true, maybeContexts) + CheckOutdatedContext: func(maybeContexts []*shared.Context) (bool, bool) { + return lib.MustCheckOutdatedContext(true, maybeContexts) }, }, false) diff --git a/app/cli/cmd/continue.go b/app/cli/cmd/continue.go index 0e4c406f..efd5b5d2 100644 --- a/app/cli/cmd/continue.go +++ b/app/cli/cmd/continue.go @@ -43,8 +43,8 @@ func doContinue(cmd *cobra.Command, args []string) { plan_exec.TellPlan(plan_exec.ExecParams{ CurrentPlanId: lib.CurrentPlanId, CurrentBranch: lib.CurrentBranch, - CheckOutdatedContext: func(cancelOpt bool, maybeContexts []*shared.Context) (bool, bool, bool) { - return lib.MustCheckOutdatedContext(cancelOpt, false, maybeContexts) + CheckOutdatedContext: func(maybeContexts []*shared.Context) (bool, bool) { + return lib.MustCheckOutdatedContext(false, maybeContexts) }, }, "", tellBg, tellStop, tellNoBuild, true) } diff --git a/app/cli/cmd/convo.go b/app/cli/cmd/convo.go index 836191a0..a9110a0f 100644 --- a/app/cli/cmd/convo.go +++ b/app/cli/cmd/convo.go @@ -91,7 +91,7 @@ func convo(cmd *cobra.Command, args []string) { output := fmt.Sprintf("\n%s", convo) + term.GetDivisionLine() + - color.New(color.Bold, color.FgCyan).Sprint(" Conversation size â") + fmt.Sprintf(" %d đĒ", totalTokens) + "\n\n" + color.New(color.Bold, term.ColorHiCyan).Sprint(" Conversation size â") + fmt.Sprintf(" %d đĒ", totalTokens) + "\n\n" term.PageOutput(output) } diff --git a/app/cli/cmd/current.go b/app/cli/cmd/current.go index a273f511..104bd935 100644 --- a/app/cli/cmd/current.go +++ b/app/cli/cmd/current.go @@ -59,7 +59,7 @@ func current(cmd *cobra.Command, args []string) { table.SetAutoWrapText(false) table.SetHeader([]string{"Current Plan", "Updated", "Created" /*"Branches",*/, "Branch", "Context", "Convo"}) - name := color.New(color.Bold, color.FgGreen).Sprint(plan.Name) + name := color.New(color.Bold, term.ColorHiGreen).Sprint(plan.Name) branch := currentBranchesByPlanId[lib.CurrentPlanId] row := []string{ diff --git a/app/cli/cmd/rm.go b/app/cli/cmd/rm.go index 6423e4be..ab379fa0 100644 --- a/app/cli/cmd/rm.go +++ b/app/cli/cmd/rm.go @@ -54,6 +54,17 @@ func contextRm(cmd *cobra.Command, args []string) { deleteIds[context.Id] = true break } + + // Check if id is a parent directory + parentDir := context.FilePath + for parentDir != "." && parentDir != "/" && parentDir != "" { + if parentDir == id { + deleteIds[context.Id] = true + break + } + parentDir = filepath.Dir(parentDir) // Move up one directory + } + } } } diff --git a/app/cli/cmd/tell.go b/app/cli/cmd/tell.go index 9262236e..a227257a 100644 --- a/app/cli/cmd/tell.go +++ b/app/cli/cmd/tell.go @@ -77,8 +77,8 @@ func doTell(cmd *cobra.Command, args []string) { plan_exec.TellPlan(plan_exec.ExecParams{ CurrentPlanId: lib.CurrentPlanId, CurrentBranch: lib.CurrentBranch, - CheckOutdatedContext: func(cancelOpt bool, maybeContexts []*shared.Context) (bool, bool, bool) { - return lib.MustCheckOutdatedContext(cancelOpt, false, maybeContexts) + CheckOutdatedContext: func(maybeContexts []*shared.Context) (bool, bool) { + return lib.MustCheckOutdatedContext(false, maybeContexts) }, }, prompt, tellBg, tellStop, tellNoBuild, false) } diff --git a/app/cli/fs/fs.go b/app/cli/fs/fs.go index 2b62bd25..b604df29 100644 --- a/app/cli/fs/fs.go +++ b/app/cli/fs/fs.go @@ -190,6 +190,12 @@ func GetPaths(baseDir, currentDir string) (*ProjectPaths, error) { } activePaths[relFile] = true + + parentDir := relFile + for parentDir != "." && parentDir != "/" && parentDir != "" { + parentDir = filepath.Dir(parentDir) + activeDirs[parentDir] = true + } } errCh <- nil @@ -225,6 +231,12 @@ func GetPaths(baseDir, currentDir string) (*ProjectPaths, error) { } activePaths[relFile] = true + + parentDir := relFile + for parentDir != "." && parentDir != "/" && parentDir != "" { + parentDir = filepath.Dir(parentDir) + activeDirs[parentDir] = true + } } errCh <- nil @@ -257,8 +269,6 @@ func GetPaths(baseDir, currentDir string) (*ProjectPaths, error) { if ignored != nil && ignored.MatchesPath(relPath) { return filepath.SkipDir } - - activeDirs[relPath] = true } else { relPath, err := filepath.Rel(currentDir, path) if err != nil { @@ -275,6 +285,12 @@ func GetPaths(baseDir, currentDir string) (*ProjectPaths, error) { mu.Lock() defer mu.Unlock() activePaths[relPath] = true + + parentDir := relPath + for parentDir != "." && parentDir != "/" && parentDir != "" { + parentDir = filepath.Dir(parentDir) + activeDirs[parentDir] = true + } } } diff --git a/app/cli/lib/apply.go b/app/cli/lib/apply.go index 0d1748eb..7803630e 100644 --- a/app/cli/lib/apply.go +++ b/app/cli/lib/apply.go @@ -2,7 +2,6 @@ package lib import ( "fmt" - "log" "os" "path/filepath" "plandex/api" @@ -61,7 +60,7 @@ func MustApplyPlan(planId, branch string, autoConfirm bool) { } } - anyOutdated, didUpdate, _ := MustCheckOutdatedContext(false, true, nil) + anyOutdated, didUpdate := MustCheckOutdatedContext(true, nil) if anyOutdated && !didUpdate { term.StopSpinner() @@ -69,196 +68,148 @@ func MustApplyPlan(planId, branch string, autoConfirm bool) { os.Exit(0) } - term.StopSpinner() - currentPlanFiles := currentPlanState.CurrentPlanFiles + isRepo := fs.ProjectRootIsGitRepo() + + toApply := currentPlanFiles.Files - if len(currentPlanFiles.Files) == 0 { + if len(toApply) == 0 { term.StopSpinner() fmt.Println("đ¤ˇââī¸ No changes to apply") return } - isRepo := fs.ProjectRootIsGitRepo() - - hasUncommittedChanges := false - if isRepo { - // Check if there are any uncommitted changes - var err error - hasUncommittedChanges, err = CheckUncommittedChanges() + if !autoConfirm { + term.StopSpinner() + numToApply := len(toApply) + suffix := "" + if numToApply > 1 { + suffix = "s" + } + shouldContinue, err := term.ConfirmYesNo("Apply changes to %d file%s?", numToApply, suffix) if err != nil { - term.OutputSimpleError("Error checking for uncommitted changes:") - term.OutputUnformattedErrorAndExit(err.Error()) + term.OutputErrorAndExit("failed to get confirmation user input: %s", err) } - } - - toApply := currentPlanFiles.Files - - var aborted bool - var stashed bool - var errMsg string - var errArgs []interface{} - var unformattedErrMsg string - - if len(toApply) == 0 { - fmt.Println("đ¤ˇââī¸ No changes to apply") - } else { - if !autoConfirm { - numToApply := len(toApply) - suffix := "" - if numToApply > 1 { - suffix = "s" - } - shouldContinue, err := term.ConfirmYesNo("Apply changes to %d file%s?", numToApply, suffix) - - if err != nil { - term.OutputErrorAndExit("failed to get confirmation user input: %s", err) - } - if !shouldContinue { - os.Exit(0) - } + if !shouldContinue { + os.Exit(0) } + term.ResumeSpinner() + } - defer func() { - if aborted { - // clear any partially applied changes before popping the stash - err := GitClearUncommittedChanges() - if err != nil { - log.Printf("Failed to clear uncommitted changes: %v", err) - } - } - - if stashed { - err := GitStashPop(true) - if err != nil { - log.Printf("Failed to pop git stash: %v", err) - } - } - - if errMsg != "" { - if unformattedErrMsg == "" { - term.OutputErrorAndExit(errMsg, errArgs...) - } else { - term.OutputSimpleError(errMsg, errArgs...) - term.OutputUnformattedErrorAndExit(unformattedErrMsg) - } - } - }() + onErr := func(errMsg string, errArgs ...interface{}) { + term.StopSpinner() + term.OutputErrorAndExit(errMsg, errArgs...) + } - if isRepo && hasUncommittedChanges { - // If there are uncommitted changes, first checkout any files that will be applied, then stash the changes + onGitErr := func(errMsg, unformattedErrMsg string) { + term.StopSpinner() + term.OutputSimpleError(errMsg, unformattedErrMsg) + } - // Checkout the files that will be applied - // It's safe to do this with the confidence that no work will be lost because we just ensured the plan is using the latest state of all these files - for path := range toApply { - exists := true - _, err := os.Stat(filepath.Join(fs.ProjectRoot, path)) - if err != nil { - if os.IsNotExist(err) { - exists = false - } else { - errMsg = "Error checking for file %s:" - errArgs = append(errArgs, path) - unformattedErrMsg = err.Error() - aborted = true - return - } - } + apiErr = api.Client.ApplyPlan(planId, branch) - if exists { - hasChanges, err := GitFileHasUncommittedChanges(path) - - if err != nil { - errMsg = "Error checking for uncommitted changes for file %s:" - errArgs = append(errArgs, path) - unformattedErrMsg = err.Error() - aborted = true - return - } - - // log.Printf("File %s has uncommitted changes: %v", path, hasChanges) - - if hasChanges { - err := os.Remove(filepath.Join(fs.ProjectRoot, path)) - if err != nil { - errMsg = "Failed to remove file prior to update %s:" - errArgs = append(errArgs, path) - unformattedErrMsg = err.Error() - aborted = true - return - } - - GitCheckoutFile(path) // ignore error to cover untracked files - } - } - } + if apiErr != nil { + onErr("failed to set pending results applied: %s", apiErr.Msg) + return + } - err := GitStashCreate("Plandex auto-stash") - if err != nil { - errMsg = "Failed to create git stash:" - unformattedErrMsg = err.Error() - aborted = true + var updatedFiles []string + for path, content := range toApply { + // Compute destination path + dstPath := filepath.Join(fs.ProjectRoot, path) + + content = strings.ReplaceAll(content, "\\`\\`\\`", "```") + + // Check if the file exists + var exists bool + _, err := os.Stat(dstPath) + if err == nil { + exists = true + } else { + if os.IsNotExist(err) { + exists = false + } else { + onErr("failed to check if %s exists:", dstPath) return } - stashed = true } - for path, content := range toApply { - // Compute destination path - dstPath := filepath.Join(fs.ProjectRoot, path) - // Create the directory if it doesn't exist - err := os.MkdirAll(filepath.Dir(dstPath), 0755) + if exists { + // read file content + bytes, err := os.ReadFile(dstPath) + if err != nil { - aborted = true - errMsg = "failed to create directory %s:" - errArgs = append(errArgs, filepath.Dir(dstPath)) + onErr("failed to read %s:", dstPath) return } - content = strings.ReplaceAll(content, "\\`\\`\\`", "```") + // Check if the file has changed + if string(bytes) == content { + // log.Println("File is unchanged, skipping") + continue + } else { + updatedFiles = append(updatedFiles, path) + } + } else { + updatedFiles = append(updatedFiles, path) - // Write the file - err = os.WriteFile(dstPath, []byte(content), 0644) + // Create the directory if it doesn't exist + err := os.MkdirAll(filepath.Dir(dstPath), 0755) if err != nil { - aborted = true - errMsg = "failed to write %s:" - errArgs = append(errArgs, dstPath) + onErr("failed to create directory %s:", filepath.Dir(dstPath)) return } } - term.StartSpinner("") - apiErr := api.Client.ApplyPlan(planId, branch) - term.StopSpinner() - - if apiErr != nil { - aborted = true - errMsg = "failed to set pending results applied: %s" - errArgs = append(errArgs, apiErr.Msg) + // Write the file + err = os.WriteFile(dstPath, []byte(content), 0644) + if err != nil { + onErr("failed to write %s:", dstPath) return } + } - if isRepo { - // Commit the changes - msg := currentPlanState.PendingChangesSummaryForApply() + term.StopSpinner() - // log.Println("Committing changes with message:") - // log.Println(msg) + if len(updatedFiles) == 0 { + fmt.Println("â Applied changes, but no files were updated") + return + } else { + if isRepo { + fmt.Println("âī¸ Plandex can commit these updates with an automatically generated message.") + fmt.Println() + fmt.Println("âšī¸ Only the files that Plandex is updating will be included the commit. Any other changes, staged or unstaged, will remain exactly as they are.") + fmt.Println() - // spew.Dump(currentPlanState) + confirmed, err := term.ConfirmYesNo("Commit Plandex updates now?") - err := GitAddAndCommit(fs.ProjectRoot, msg, true) if err != nil { - aborted = true - // return fmt.Errorf("failed to commit changes: %w", err) - term.OutputSimpleError("Failed to commit changes:") - term.OutputUnformattedErrorAndExit(err.Error()) + onErr("failed to get confirmation user input: %s", err) + } + + if confirmed { + // Commit the changes + msg := currentPlanState.PendingChangesSummaryForApply() + + // log.Println("Committing changes with message:") + // log.Println(msg) + + // spew.Dump(currentPlanState) + + err := GitAddAndCommitPaths(fs.ProjectRoot, msg, updatedFiles, true) + if err != nil { + onGitErr("Failed to commit changes:", err.Error()) + } } } - fmt.Println("â Applied changes") + suffix := "" + if len(updatedFiles) > 1 { + suffix = "s" + } + fmt.Printf("â Applied changes, %d file%s updated\n", len(updatedFiles), suffix) } } diff --git a/app/cli/lib/context_load.go b/app/cli/lib/context_load.go index 5cd30382..7e5de23d 100644 --- a/app/cli/lib/context_load.go +++ b/app/cli/lib/context_load.go @@ -78,6 +78,8 @@ func MustLoadContext(resources []string, params *types.LoadContextParams) { onErr(fmt.Errorf("failed to get project paths: %v", err)) } + // log.Println(spew.Sdump(paths)) + // fmt.Println("active paths", len(paths.ActivePaths)) // fmt.Println("all paths", len(paths.AllPaths)) // fmt.Println("ignored paths", len(paths.IgnoredPaths)) @@ -88,11 +90,19 @@ func MustLoadContext(resources []string, params *types.LoadContextParams) { if !params.ForceSkipIgnore { var filteredPaths []string for _, inputFilePath := range inputFilePaths { + // log.Println("inputFilePath", inputFilePath) + if _, ok := paths.ActivePaths[inputFilePath]; !ok { + // log.Println("not active", inputFilePath) + if _, ok := paths.IgnoredPaths[inputFilePath]; ok { + // log.Println("ignored", inputFilePath) + ignoredPaths[inputFilePath] = paths.IgnoredPaths[inputFilePath] } } else { + // log.Println("active", inputFilePath) + filteredPaths = append(filteredPaths, inputFilePath) } } diff --git a/app/cli/lib/context_paths.go b/app/cli/lib/context_paths.go index b060ef99..d443fa52 100644 --- a/app/cli/lib/context_paths.go +++ b/app/cli/lib/context_paths.go @@ -37,6 +37,8 @@ func ParseInputPaths(fileOrDirPaths []string, params *types.LoadContextParams) ( } if !(params.Recursive || params.NamesOnly) { + // log.Println("path", path, "info.Name()", info.Name()) + return fmt.Errorf("cannot process directory %s: --recursive or --tree flag not set", path) } diff --git a/app/cli/lib/context_update.go b/app/cli/lib/context_update.go index fe9029b2..373e8d88 100644 --- a/app/cli/lib/context_update.go +++ b/app/cli/lib/context_update.go @@ -18,7 +18,7 @@ import ( "github.com/plandex/plandex/shared" ) -func MustCheckOutdatedContext(cancelOpt, quiet bool, maybeContexts []*shared.Context) (contextOutdated, updated, canceled bool) { +func MustCheckOutdatedContext(quiet bool, maybeContexts []*shared.Context) (contextOutdated, updated bool) { if !quiet { term.StartSpinner("đŦ Checking context...") } @@ -29,13 +29,15 @@ func MustCheckOutdatedContext(cancelOpt, quiet bool, maybeContexts []*shared.Con term.OutputErrorAndExit("failed to check outdated context: %s", err) } - term.StopSpinner() + if !quiet { + term.StopSpinner() + } if len(outdatedRes.UpdatedContexts) == 0 { if !quiet { fmt.Println("â Context is up to date") } - return false, false, false + return false, false } types := []string{} if outdatedRes.NumFiles > 0 { @@ -89,11 +91,7 @@ func MustCheckOutdatedContext(cancelOpt, quiet bool, maybeContexts []*shared.Con var confirmed bool - if cancelOpt { - confirmed, canceled, err = term.ConfirmYesNoCancel("Update context now?") - } else { - confirmed, err = term.ConfirmYesNo("Update context now?") - } + confirmed, err = term.ConfirmYesNo("Update context now?") if err != nil { term.OutputErrorAndExit("failed to get user input: %s", err) @@ -101,9 +99,11 @@ func MustCheckOutdatedContext(cancelOpt, quiet bool, maybeContexts []*shared.Con if confirmed { MustUpdateContext(maybeContexts) + return true, true + } else { + return true, false } - return true, confirmed, canceled } func MustUpdateContext(maybeContexts []*shared.Context) { diff --git a/app/cli/lib/git.go b/app/cli/lib/git.go index b5ba472a..579e14de 100644 --- a/app/cli/lib/git.go +++ b/app/cli/lib/git.go @@ -21,7 +21,32 @@ func GitAddAndCommit(dir, message string, lockMutex bool) error { return fmt.Errorf("error adding files to git repository for dir: %s, err: %v", dir, err) } - err = GitCommit(dir, message, false) + err = GitCommit(dir, message, nil, false) + if err != nil { + return fmt.Errorf("error committing files to git repository for dir: %s, err: %v", dir, err) + } + + return nil +} + +func GitAddAndCommitPaths(dir, message string, paths []string, lockMutex bool) error { + if len(paths) == 0 { + return nil + } + + if lockMutex { + gitMutex.Lock() + defer gitMutex.Unlock() + } + + for _, path := range paths { + err := GitAdd(dir, path, false) + if err != nil { + return fmt.Errorf("error adding file %s to git repository for dir: %s, err: %v", path, dir, err) + } + } + + err := GitCommit(dir, message, paths, false) if err != nil { return fmt.Errorf("error committing files to git repository for dir: %s, err: %v", dir, err) } @@ -43,13 +68,19 @@ func GitAdd(repoDir, path string, lockMutex bool) error { return nil } -func GitCommit(repoDir, commitMsg string, lockMutex bool) error { +func GitCommit(repoDir, commitMsg string, paths []string, lockMutex bool) error { if lockMutex { gitMutex.Lock() defer gitMutex.Unlock() } - res, err := exec.Command("git", "-C", repoDir, "commit", "-m", commitMsg, "--allow-empty").CombinedOutput() + args := []string{"-C", repoDir, "commit", "-m", commitMsg, "--allow-empty"} + + if len(paths) > 0 { + args = append(args, paths...) + } + + res, err := exec.Command("git", args...).CombinedOutput() if err != nil { return fmt.Errorf("error committing files to git repository for dir: %s, err: %v, output: %s", repoDir, err, string(res)) } @@ -173,6 +204,8 @@ func GitCheckoutFile(path string) error { res, err := exec.Command("git", "checkout", path).CombinedOutput() if err != nil { + log.Println("Error checking out file:", string(res)) + return fmt.Errorf("error checking out file %s | err: %v, output: %s", path, err, string(res)) } diff --git a/app/cli/main.go b/app/cli/main.go index 71460cae..0a47aea3 100644 --- a/app/cli/main.go +++ b/app/cli/main.go @@ -22,8 +22,8 @@ func init() { return plan_exec.Build(plan_exec.ExecParams{ CurrentPlanId: lib.CurrentPlanId, CurrentBranch: lib.CurrentBranch, - CheckOutdatedContext: func(cancelOpt bool, maybeContexts []*shared.Context) (bool, bool, bool) { - return lib.MustCheckOutdatedContext(cancelOpt, true, maybeContexts) + CheckOutdatedContext: func(maybeContexts []*shared.Context) (bool, bool) { + return lib.MustCheckOutdatedContext(true, maybeContexts) }, }, false) }) diff --git a/app/cli/plan_exec/build.go b/app/cli/plan_exec/build.go index de4eb067..9d215eb8 100644 --- a/app/cli/plan_exec/build.go +++ b/app/cli/plan_exec/build.go @@ -22,7 +22,7 @@ func Build(params ExecParams, buildBg bool) (bool, error) { term.OutputErrorAndExit("Error getting context: %v", apiErr) } - anyOutdated, didUpdate, _ := params.CheckOutdatedContext(false, contexts) + anyOutdated, didUpdate := params.CheckOutdatedContext(contexts) if anyOutdated && !didUpdate { term.StopSpinner() diff --git a/app/cli/plan_exec/params.go b/app/cli/plan_exec/params.go index 309cbce5..b9236736 100644 --- a/app/cli/plan_exec/params.go +++ b/app/cli/plan_exec/params.go @@ -5,5 +5,5 @@ import "github.com/plandex/plandex/shared" type ExecParams struct { CurrentPlanId string CurrentBranch string - CheckOutdatedContext func(cancelOpt bool, maybeContexts []*shared.Context) (bool, bool, bool) + CheckOutdatedContext func(maybeContexts []*shared.Context) (bool, bool) } diff --git a/app/cli/plan_exec/tell.go b/app/cli/plan_exec/tell.go index d2d4d254..e8755170 100644 --- a/app/cli/plan_exec/tell.go +++ b/app/cli/plan_exec/tell.go @@ -29,9 +29,9 @@ func TellPlan( term.OutputErrorAndExit("Error getting context: %v", apiErr) } - anyOutdated, didUpdate, canceled := params.CheckOutdatedContext(true, contexts) + anyOutdated, didUpdate := params.CheckOutdatedContext(contexts) - if anyOutdated && !didUpdate && canceled { + if anyOutdated && !didUpdate { term.StopSpinner() if isUserContinue { log.Println("Plan won't continue") diff --git a/app/cli/stream_tui/update.go b/app/cli/stream_tui/update.go index 756ca809..aa4bd486 100644 --- a/app/cli/stream_tui/update.go +++ b/app/cli/stream_tui/update.go @@ -310,6 +310,10 @@ func (m *streamUIModel) streamUpdate(msg *shared.StreamMessage) (tea.Model, tea. m.updateViewportDimensions() + if m.processing && !m.finished { + return m, m.spinner.Tick + } + case shared.StreamMessageDescribing: m.processing = true return m, m.spinner.Tick @@ -319,6 +323,7 @@ func (m *streamUIModel) streamUpdate(msg *shared.StreamMessage) (tea.Model, tea. return m, tea.Quit case shared.StreamMessageFinished: + // log.Println("stream finished") m.finished = true return m, tea.Quit diff --git a/app/cli/stream_tui/view.go b/app/cli/stream_tui/view.go index 280868f6..d7a68f0a 100644 --- a/app/cli/stream_tui/view.go +++ b/app/cli/stream_tui/view.go @@ -83,6 +83,8 @@ func (m streamUIModel) doRenderBuild(outputStatic bool) string { lbl := "Building plan " bgColor := color.BgGreen if outputStatic { + // log.Printf("m.finished: %v, len(m.finishedByPath): %d, len(m.tokensByPath): %d", m.finished, len(m.finishedByPath), len(m.tokensByPath)) + if m.finished || len(m.finishedByPath) == len(m.tokensByPath) { lbl = "Built plan " } else if m.stopped || m.err != nil || m.apiErr != nil { diff --git a/app/cli/term/help.go b/app/cli/term/help.go index 8be8f248..cb232b89 100644 --- a/app/cli/term/help.go +++ b/app/cli/term/help.go @@ -51,6 +51,9 @@ func PrintCmdsWithColors(prefix string, colors []color.Attribute, cmds ...string } func printCmds(w io.Writer, prefix string, colors []color.Attribute, cmds ...string) { + if os.Getenv("PLANDEX_DISABLE_SUGGESTIONS") != "" { + return + } for _, cmd := range cmds { config, ok := CmdDesc[cmd] if !ok { diff --git a/app/server/email/email.go b/app/server/email/email.go index 93403543..f14f9b77 100644 --- a/app/server/email/email.go +++ b/app/server/email/email.go @@ -2,6 +2,7 @@ package email import ( "fmt" + "net/smtp" "os" "github.com/atotto/clipboard" @@ -15,10 +16,16 @@ func SendVerificationEmail(email string, pin string) error { // Check if the environment is production if os.Getenv("GOENV") == "production" { // Production environment - send email using AWS SES - subject := "Your Verification Pin" - htmlBody := fmt.Sprintf("
Your verification pin is: %s
", pin) - textBody := fmt.Sprintf("Your verification pin is: %s", pin) - return sendEmailViaSES(email, subject, htmlBody, textBody) + subject := "Your Plandex Pin" + htmlBody := fmt.Sprintf("Hi there,
Your pin is: %s
", pin) + textBody := fmt.Sprintf("Hi there,\n\nYour pin is: %s", pin) + + if os.Getenv("IS_CLOUD") == "" { + return sendEmailViaSES(email, subject, htmlBody, textBody) + } else { + return sendEmailViaSMTP(email, subject, htmlBody, textBody) + } + } if os.Getenv("GOENV") == "development" { @@ -80,3 +87,41 @@ func sendEmailViaSES(recipient, subject, htmlBody, textBody string) error { return err } + +func sendEmailViaSMTP(recipient, subject, htmlBody, textBody string) error { + smtpHost := os.Getenv("SMTP_HOST") + smtpPort := os.Getenv("SMTP_PORT") + smtpUser := os.Getenv("SMTP_USER") + smtpPassword := os.Getenv("SMTP_PASSWORD") + + if smtpHost == "" || smtpPort == "" || smtpUser == "" || smtpPassword == "" { + return fmt.Errorf("SMTP settings not found in environment variables") + } + + smtpAddress := fmt.Sprintf("%s:%s", smtpHost, smtpPort) + + auth := smtp.PlainAuth("", smtpUser, smtpPassword, smtpHost) + + // Generate a MIME boundary + boundary := "BOUNDARY1234567890" + header := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: multipart/alternative; boundary=\"%s\"\r\n\r\n", smtpUser, recipient, subject, boundary) + + // Prepare the text body part + textPart := fmt.Sprintf("--%s\r\nContent-Type: text/plain; charset=\"UTF-8\"\r\n\r\n%s\r\n", boundary, textBody) + + // Prepare the HTML body part + htmlPart := fmt.Sprintf("--%s\r\nContent-Type: text/html; charset=\"UTF-8\"\r\n\r\n%s\r\n", boundary, htmlBody) + + // End marker for the boundary + endBoundary := fmt.Sprintf("--%s--", boundary) + + // Combine the parts to form the full email message + message := []byte(header + textPart + htmlPart + endBoundary) + + err := smtp.SendMail(smtpAddress, auth, smtpUser, []string{recipient}, message) + if err != nil { + return fmt.Errorf("error sending email via SMTP: %v", err) + } + + return nil +} diff --git a/app/server/handlers/stream_helper.go b/app/server/handlers/stream_helper.go index 3ab03e8c..cd15a4e1 100644 --- a/app/server/handlers/stream_helper.go +++ b/app/server/handlers/stream_helper.go @@ -160,10 +160,8 @@ func initConnectActive(auth *types.ServerAuth, planId, branch string, w http.Res buildInfo.NumTokens = 0 buildInfo.Finished = true } else { - tokens, err := shared.GetNumTokens(build.Buffer) - if err != nil { - return fmt.Errorf("error getting tokens for build with ReplyId %s: %v", build.ReplyId, err) - } + tokens := build.BufferTokens + buildInfo.Finished = false buildInfo.NumTokens += tokens } diff --git a/app/server/model/plan/build_exec.go b/app/server/model/plan/build_exec.go index 4fd0f173..174decf5 100644 --- a/app/server/model/plan/build_exec.go +++ b/app/server/model/plan/build_exec.go @@ -182,7 +182,7 @@ func (fileState *activeBuildStreamFileState) buildFile() { log.Printf("File %s found in current plan.\n", filePath) currentState = currentPlanFile - log.Println("\n\nCurrent state:\n", currentState, "\n\n") + // log.Println("\n\nCurrent state:\n", currentState, "\n\n") } else if contextPart != nil { log.Printf("File %s found in model context. Using context state.\n", filePath) @@ -192,7 +192,7 @@ func (fileState *activeBuildStreamFileState) buildFile() { log.Println("Context state is empty. That's bad.") } - log.Println("\n\nCurrent state:\n", currentState, "\n\n") + // log.Println("\n\nCurrent state:\n", currentState, "\n\n") } fileState.currentState = currentState @@ -222,6 +222,18 @@ func (fileState *activeBuildStreamFileState) buildFile() { } fileState.onFinishBuildFile(planRes) return + } else { + currentNumTokens, err := shared.GetNumTokens(currentState) + + if err != nil { + log.Printf("Error getting num tokens for current state: %v\n", err) + fileState.onBuildFileError(fmt.Errorf("error getting num tokens for current state: %v", err)) + return + } + + log.Printf("Current state num tokens: %d\n", currentNumTokens) + + activeBuild.CurrentFileTokens = currentNumTokens } log.Println("Getting file from model: " + filePath) diff --git a/app/server/model/plan/build_result.go b/app/server/model/plan/build_result.go index 087bc49d..e82729de 100644 --- a/app/server/model/plan/build_result.go +++ b/app/server/model/plan/build_result.go @@ -51,10 +51,27 @@ func getPlanResult(params planResultParams) (*db.PlanFileResult, bool) { log.Printf("getPlanResult - streamedChange.Old.StartLine: %d\n", streamedChange.Old.StartLine) log.Printf("getPlanResult - streamedChange.Old.EndLine: %d\n", streamedChange.Old.EndLine) - if streamedChange.Old.StartLine == streamedChange.Old.EndLine { - old = currentStateLines[streamedChange.Old.StartLine-1] + startLine := streamedChange.Old.StartLine + endLine := streamedChange.Old.EndLine + + if startLine < 1 { + startLine = 1 + } + if startLine > len(currentStateLines) { + startLine = len(currentStateLines) + } + + if endLine < 1 { + endLine = 1 + } + if endLine > len(currentStateLines) { + endLine = len(currentStateLines) + } + + if startLine == endLine { + old = currentStateLines[startLine-1] } else { - old = strings.Join(currentStateLines[streamedChange.Old.StartLine-1:streamedChange.Old.EndLine], "\n") + old = strings.Join(currentStateLines[startLine-1:endLine], "\n") } log.Printf("getPlanResult - old: %s\n", old) diff --git a/app/server/model/plan/build_stream.go b/app/server/model/plan/build_stream.go index 7b2e3aaa..f0c0784d 100644 --- a/app/server/model/plan/build_stream.go +++ b/app/server/model/plan/build_stream.go @@ -8,6 +8,7 @@ import ( "plandex-server/db" "plandex-server/model" "plandex-server/types" + "strings" "time" "github.com/davecgh/go-spew/spew" @@ -15,6 +16,8 @@ import ( "github.com/sashabaranov/go-openai" ) +const MaxBuildStreamErrorRetries = 3 // uses naive exponential backoff so be careful about setting this too high + type activeBuildStreamState struct { client *openai.Client auth *types.ServerAuth @@ -33,8 +36,8 @@ type activeBuildStreamFileState struct { build *db.PlanBuild currentPlanState *shared.CurrentPlanState activeBuild *types.ActiveBuild - buffer string currentState string + numRetry int } func (fileState *activeBuildStreamFileState) listenStream(stream *openai.ChatCompletionStream) { @@ -61,7 +64,7 @@ func (fileState *activeBuildStreamFileState) listenStream(stream *openai.ChatCom return case <-timer.C: // Timer triggered because no new chunk was received in time - fileState.onBuildFileError(fmt.Errorf("stream timeout due to inactivity for file '%s'", filePath)) + fileState.retryOrError(fmt.Errorf("stream timeout due to inactivity for file '%s'", filePath)) return default: response, err := stream.Recv() @@ -77,15 +80,17 @@ func (fileState *activeBuildStreamFileState) listenStream(stream *openai.ChatCom if err == context.Canceled { log.Printf("File %s: Stream canceled\n", filePath) + log.Println("current buffer:") + log.Println(fileState.activeBuild.Buffer) return } - fileState.onBuildFileError(fmt.Errorf("stream error for file '%s': %v", filePath, err)) + fileState.retryOrError(fmt.Errorf("stream error for file '%s': %v", filePath, err)) return } if len(response.Choices) == 0 { - fileState.onBuildFileError(fmt.Errorf("stream error: no choices")) + fileState.retryOrError(fmt.Errorf("stream error: no choices")) return } @@ -96,6 +101,14 @@ func (fileState *activeBuildStreamFileState) listenStream(stream *openai.ChatCom if len(delta.ToolCalls) > 0 { content = delta.ToolCalls[0].Function.Arguments + trimmed := strings.TrimSpace(content) + if trimmed == "{%invalidjson%}" || trimmed == "``(no output)``````" { + log.Println("File", filePath+":", "%invalidjson%} token in streamed chunk") + fileState.retryOrError(fmt.Errorf("invalid JSON in streamed chunk for file '%s'", filePath)) + + return + } + buildInfo := &shared.BuildInfo{ Path: filePath, NumTokens: 1, @@ -108,11 +121,19 @@ func (fileState *activeBuildStreamFileState) listenStream(stream *openai.ChatCom BuildInfo: buildInfo, }) - fileState.buffer += content + fileState.activeBuild.Buffer += content + fileState.activeBuild.BufferTokens++ + + // After a reasonable threshhold, if buffer has significantly more tokens than original file + proposed changes, something is wrong + if fileState.activeBuild.BufferTokens > 500 && fileState.activeBuild.BufferTokens > int(float64(fileState.activeBuild.CurrentFileTokens+fileState.activeBuild.FileContentTokens)*1.5) { + fileState.retryOrError(fmt.Errorf("stream buffer tokens too high for file '%s'", filePath)) + return + } } var streamed types.StreamedChanges - err = json.Unmarshal([]byte(fileState.buffer), &streamed) + err = json.Unmarshal([]byte(fileState.activeBuild.Buffer), &streamed) + if err == nil { log.Printf("File %s: Parsed streamed replacements\n", filePath) spew.Dump(streamed) @@ -138,6 +159,7 @@ func (fileState *activeBuildStreamFileState) listenStream(stream *openai.ChatCom } } + // no retry here as this should never happen fileState.onBuildFileError(fmt.Errorf("replacements failed for file '%s'", filePath)) return @@ -160,10 +182,26 @@ func (fileState *activeBuildStreamFileState) listenStream(stream *openai.ChatCom spew.Dump(response) spew.Dump(fileState) - fileState.onBuildFileError(fmt.Errorf("stream chunk missing function call. Reason: %s, File: %s", choice.FinishReason, filePath)) + fileState.retryOrError(fmt.Errorf("stream chunk missing function call. Reason: %s, File: %s", choice.FinishReason, filePath)) return } } } } + +func (fileState *activeBuildStreamFileState) retryOrError(err error) { + if fileState.numRetry < MaxBuildStreamErrorRetries { + fileState.numRetry++ + fileState.activeBuild.Buffer = "" + fileState.activeBuild.BufferTokens = 0 + log.Printf("Retrying build file '%s' due to error: %v\n", fileState.filePath, err) + + // Exponential backoff + time.Sleep(time.Duration(fileState.numRetry*fileState.numRetry) * time.Second) + + fileState.buildFile() + } else { + fileState.onBuildFileError(err) + } +} diff --git a/app/server/model/plan/tell_load.go b/app/server/model/plan/tell_load.go index 5682ca41..bb7aec30 100644 --- a/app/server/model/plan/tell_load.go +++ b/app/server/model/plan/tell_load.go @@ -156,12 +156,15 @@ func (state *activeTellStreamState) loadTellPlan() error { } innerErrCh := make(chan error) + var userMsg *db.ConvoMessage go func() { if iteration == 0 && missingFileResponse == "" && !req.IsUserContinue { num := len(convo) + 1 - userMsg := db.ConvoMessage{ + log.Printf("storing user message | len(convo): %d | num: %d\n", len(convo), num) + + userMsg = &db.ConvoMessage{ OrgId: currentOrgId, PlanId: planId, UserId: currentUserId, @@ -171,7 +174,7 @@ func (state *activeTellStreamState) loadTellPlan() error { Message: req.Prompt, } - _, err = db.StoreConvoMessage(&userMsg, auth.User.Id, branch, true) + _, err = db.StoreConvoMessage(userMsg, auth.User.Id, branch, true) if err != nil { log.Printf("Error storing user message: %v\n", err) @@ -218,6 +221,10 @@ func (state *activeTellStreamState) loadTellPlan() error { } } + if userMsg != nil { + convo = append(convo, userMsg) + } + errCh <- nil }() diff --git a/app/server/model/plan/tell_stream.go b/app/server/model/plan/tell_stream.go index 3a8b5a31..0d0d7a19 100644 --- a/app/server/model/plan/tell_stream.go +++ b/app/server/model/plan/tell_stream.go @@ -15,6 +15,8 @@ import ( "github.com/sashabaranov/go-openai" ) +const MaxAutoContinueIterations = 30 + type activeTellStreamState struct { client *openai.Client req *shared.TellPlanRequest @@ -290,7 +292,7 @@ func (state *activeTellStreamState) listenStream(stream *openai.ChatCompletionSt ap.CurrentReplyDoneCh = nil }) - if req.AutoContinue && shouldContinue { + if req.AutoContinue && shouldContinue && iteration < MaxAutoContinueIterations { log.Println("Auto continue plan") // continue plan execTellPlan(client, plan, branch, auth, req, iteration+1, "", false) @@ -480,12 +482,21 @@ func (state *activeTellStreamState) listenStream(stream *openai.ChatCompletionSt modelContext: state.modelContext, } + fileContentTokens, err := shared.GetNumTokens(fileContents[i]) + + if err != nil { + log.Printf("Error getting num tokens for file %s: %v\n", file, err) + state.onError(fmt.Errorf("error getting num tokens for file %s: %v", file, err), true, "", "") + return + } + buildState.queueBuilds([]*types.ActiveBuild{{ - ReplyId: replyId, - Idx: i, - FileDescription: fileDescriptions[i], - FileContent: fileContents[i], - Path: file, + ReplyId: replyId, + Idx: i, + FileDescription: fileDescriptions[i], + FileContent: fileContents[i], + FileContentTokens: fileContentTokens, + Path: file, }}) } replyFiles = append(replyFiles, file) @@ -505,17 +516,12 @@ func (state *activeTellStreamState) storeAssistantReply() (*db.ConvoMessage, str branch := state.branch auth := state.auth replyNumTokens := state.replyNumTokens - iteration := state.iteration replyId := state.replyId convo := state.convo - missingFileResponse := state.missingFileResponse num := len(convo) + 1 - if iteration == 0 && missingFileResponse == "" { - num++ - } - log.Printf("Storing assistant reply num %d\n", num) + log.Printf("storing assistant reply | len(convo) %d | num %d\n", len(convo), num) assistantMsg := db.ConvoMessage{ Id: replyId, diff --git a/app/server/model/prompts/build.go b/app/server/model/prompts/build.go index 6b067afe..e750202d 100644 --- a/app/server/model/prompts/build.go +++ b/app/server/model/prompts/build.go @@ -75,7 +75,9 @@ var listChangesPrompt = ` The 'old' property is an object with two properties: 'startLine' and 'endLine'. - 'startLine' is the line number where the section to be replaced begins in the original file. 'endLine' is the line number where the section to be replaced ends in the original file. For a single line replacement, 'startLine' and 'endLine' will be the same. + 'startLine' is the line number where the section to be replaced begins in the original file. 'endLine' is the line number where the section to be replaced ends in the original file. For a single line replacement, 'startLine' and 'endLine' will be the same. + + Line numbers in 'startLine' and 'endLine' are 1-indexed. 1 is the minimum line number. The maximum line number is the number of lines in the original file. 'startLine' and 'endLine' must be valid line numbers in the original file, greater than or equal to 1 and less than or equal to the number of lines in the original file. You MUST refer to 1-indexed line numbers exactly as they are labeled in the original file. If the 'startLine' or 'endLine' is the first line of the file, 'startLine' or 'endLine' will be 1. If the 'startLine' or 'endLine' is the last line of the file, 'startLine' or 'endLine' will be the number of lines in the file. 'startLine' must NEVER be 0, -1, or any other number less than 1. The 'new' property is a string that represents the new code that will replace the old code. The new code must be valid and consistent with the intention of the plan. If the the proposed update is to remove code, the 'new' property should be an empty string. @@ -96,7 +98,11 @@ var listChangesPrompt = ` Apply changes intelligently in order to avoid syntax errors, breaking code, or removing code from the original file that should not be removed. Consider the reason behind the update and make sure the result is consistent with the intention of the plan. - Pay EXTREMELY close attention to opening and closing brackets, parentheses, and braces. Never leave them unbalanced when the changes are applied. + You ABSOLUTELY MUST NOT ovewrite or delete code from the original file unless the plan *clearly intends* for the code to be overwritten or removed. Do NOT replace a full section of code with only new code unless that is the clear intention of the plan. Instead, merge the original code and the proposed changes together intelligently according to the intention of the plan. + + Pay *EXTREMELY close attention* to opening and closing brackets, parentheses, and braces. Never leave them unbalanced when the changes are applied. + + The 'listChanges' function MUST be called *valid JSON*. Double quotes within json properties of the 'listChanges' function call parameters JSON object *must be properly escaped* with a backslash. [END YOUR INSTRUCTIONS] ` diff --git a/app/server/model/prompts/create.go b/app/server/model/prompts/create.go index 03e10639..cd0d4335 100644 --- a/app/server/model/prompts/create.go +++ b/app/server/model/prompts/create.go @@ -27,12 +27,13 @@ const SysCreate = Identity + ` A plan is a set of files with an attached context - src/main.rs: - lib/term.go: - main.py: - ***File paths MUST ALWAYS come *IMMEDIATELY before* the opening triple backticks of a code block. They should *not* be included in the code block itself. - ***File paths MUST ALWAYS appear *IMMEDIATELY* before the opening triple backticks of a code block. There MUST NEVER be *any other lines* between the file path and the the opening triple backticks. Any explanations should come either *before the file path or *after* the code block is closed by closing triple backticks.* - ***You *must not* include any other text in a code block label apart from the initial '- ' and the file path. DO NOT UNDER ANY CIRCUMSTANCES use a label like 'File path: src/main.rs' or 'src/main.rs: (Create this file)' or 'File to Create: src/main.rs' or 'File to Update: src/main.rs'. Instead use JUST 'src/main.rs:'. DO NOT include any explanatory text in the code block label like 'src/main.rs: (Add a new function)'. Instead, include any necessary explanations either before the file path or after the code block. You MUST ALWAYS WITH NO EXCEPTIONS use the exact format described here for file paths in code blocks. + ***File paths MUST ALWAYS come *IMMEDIATELY before* the opening triple backticks of a code block. They should *not* be included in the code block itself. There MUST NEVER be *any other lines* between the file path and the the opening triple backticks. Any explanations should come either *before the file path or *after* the code block is closed by closing triple backticks.* + ***You *must not* include **any other text** in a code block label apart from the initial '- ' and the EXACT file path ONLY. DO NOT UNDER ANY CIRCUMSTANCES use a label like 'File path: src/main.rs' or 'src/main.rs: (Create this file)' or 'File to Create: src/main.rs' or 'File to Update: src/main.rs'. Instead use EXACTLY 'src/main.rs:'. DO NOT include any explanatory text in the code block label like 'src/main.rs: (Add a new function)'. Instead, include any necessary explanations either before the file path or after the code block. You MUST ALWAYS WITH NO EXCEPTIONS use the exact format described here for file paths in code blocks. b. If not: - Explicitly say "Let's break up this task." - - Divide the task into smaller subtasks and list them in a numbered list. Stop there. + - Divide the task into smaller subtasks and list them in a numbered list. Stop there. + - If you are already working on a subtask and the subtask is still too large to be implemented in a single response, it should be further broken down into smaller subtasks. In that case, explicitly say "Let's further break up this subtask", further divide the subtask into even smaller steps, and list them in a numbered list. Stop there. + - Be thorough and exhaustive in your list of subtasks. Ensure you've accounted for *every subtask* that must be done to fully complete the user's task to a high standard. ## Code blocks and files @@ -44,6 +45,16 @@ const SysCreate = Identity + ` A plan is a set of files with an attached context **You must not include anything except valid code in labelled file blocks for code files.** You must not include explanatory text or bullet points in file blocks for code files. Only code. Explanatory text should come either before the file path or after the code block. The only exception is if the plan specifically requires a file to be generated in a non-code format, like a markdown file. In that case, you can include the non-code content in the file block. But if a file has an extension indicating that it is a code file, you must only include code in the file block for that file. + Files MUST NOT be labelled with a comment like "// File to create: src/main.rs" or "// File to update: src/main.rs". + + File block labels MUST ONLY include a *single* file path. You must NEVER include multiple files in a single file block. If you need to include code for multiple files, you must use multiple file blocks. + + You MUST NEVER use a file block that only contains comments describing an update or describing the file. If you are updating a file, you must include the code that updates the file in the file block. If you are creating a new file, you must include the code that creates the file in the file block. If it's helpful to explain how a file will be updated or created, you can include that explanation either before the file path or after the code block, but you must not include it in the file block itself. + + If code is being removed from a file, the removal must be shown in a labelled file block according to your instructions. Use a comment within the file block to denote the removal like '// Plandex: removed the fooBar function' or '// Plandex: removed the loop'. Do NOT use any other formatting apart from a labelled file block to denote the removal. + + If a change is related to code in an existing file in context, make the change as an update to the existing file. Do NOT create a new file for a change that applies to an existing file in context. For example, if there is an 'Page.tsx' file in the existing context and the user has asked you to update the structure of the page component, make the change in the existing 'Page.tsx' file. Do NOT create a new file like 'page.tsx' or 'NewPage.tsx' for the change. If the user has specifically asked you to apply a change to a new file, then you can create a new file. If there is no existing file that makes sense to apply a change to, then you can create a new file. + For code in markdown blocks, always include the language name after the opening triple backticks. If there are triple backticks within any file in context, they will be escaped with backslashes like this '` + "\\`\\`\\`" + `'. If you are outputting triple backticks in a code block, you MUST escape them in exactly the same way. @@ -56,7 +67,7 @@ const SysCreate = Identity + ` A plan is a set of files with an attached context As much as possible, do not include placeholders in code blocks like "// implement functionality here". Unless you absolutely cannot implement the full code block, do not include a placeholder denoted with comments. Do your best to implement the functionality rather than inserting a placeholder. You **MUST NOT** include placeholders just to shorten the code block. If the task is too large to implement in a single code block, you should break the task down into smaller steps and **FULLY** implement each step. - As much as possible, the code you suggest should be robust, complete, and ready for production. + As much as possible, the code you suggest should be robust, complete, and ready for production. ## Do the task yourself and don't give up @@ -139,7 +150,23 @@ const SysCreate = Identity + ` A plan is a set of files with an attached context var CreateSysMsgNumTokens, _ = shared.GetNumTokens(SysCreate) -const promptWrapperFormatStr = "# The user's latest prompt:\n```\n%s\n```\n\n Please respond according to the 'Your instructions' section above. If you're making a plan, remember to precede code blocks with the file path *exactly* as described in 2a, and do not use any other formatting for file paths. **Do not include explanations or any other text apart from the file path in code block labels.** Always use triple backticks to start and end code blocks. Only list out subtasks once for the plan--after that, do not list or describe a subtask that can be implemented in code without including a code block that implements the subtask. Do not ask the user to do anything that you can do yourself with a code block. Do not say a task is too large or complex for you to complete--do your best to break down the task and complete it even if it's very large or complex. Do not implement a task partially and then give up even if it's very large or complex--do your best to implement each task and subtask **fully**. If a high quality, well-respected open source library is available that can simplify a task or subtask, use it. If you're making a plan, also remember to end every response with either " + `"All tasks have been completed.", "Next, " (plus a brief descripton of the next step), or "The plan cannot be continued." according to your instructions for ending a response.` +const promptWrapperFormatStr = "# The user's latest prompt:\n```\n%s\n```\n\n" + `Please respond according to the 'Your instructions' section above. + +If you're making a plan, remember to label code blocks with the file path *exactly* as described in 2a, and do not use any other formatting for file paths. **Do not include explanations or any other text apart from the file path in code block labels.** + +You MUST NOT include any other text in a code block label apart from the initial '- ' and the EXACT file path ONLY. DO NOT UNDER ANY CIRCUMSTANCES use a label like 'File path: src/main.rs' or 'src/main.rs: (Create this file)' or 'File to Create: src/main.rs' or 'File to Update: src/main.rs'. Instead use EXACTLY 'src/main.rs:'. DO NOT include any explanatory text in the code block label like 'src/main.rs: (Add a new function)'. It is EXTREMELY IMPORTANT that the code block label includes *only* the initial '- ', the file path, and NO OTHER TEXT whatsoever. If additional text apart from the initial '- ' and the exact file path is included in the code block label, the plan will not be parsed properly and you will have failed at the task of generating a usable plan. + +Always use triple backticks to start and end code blocks. + +Only list out subtasks once for the plan--after that, do not list or describe a subtask that can be implemented in code without including a code block that implements the subtask. + +Do not ask the user to do anything that you can do yourself with a code block. Do not say a task is too large or complex for you to complete--do your best to break down the task and complete it even if it's very large or complex. + +Do not implement a task partially and then give up even if it's very large or complex--do your best to implement each task and subtask **fully**. + +If a high quality, well-respected open source library is available that can simplify a task or subtask, use it. + +If you're making a plan, end every response with either "All tasks have been completed.", "Next, " (plus a brief descripton of the next step), or "The plan cannot be continued." according to your instructions for ending a response.` func GetWrappedPrompt(prompt string) string { return fmt.Sprintf(promptWrapperFormatStr, prompt) diff --git a/app/server/types/active_plan.go b/app/server/types/active_plan.go index f88de3a9..4b8b0f97 100644 --- a/app/server/types/active_plan.go +++ b/app/server/types/active_plan.go @@ -14,14 +14,17 @@ import ( ) type ActiveBuild struct { - ReplyId string - FileDescription string - FileContent string - Path string - Idx int - Buffer string - Success bool - Error error + ReplyId string + FileDescription string + FileContent string + FileContentTokens int + CurrentFileTokens int + Path string + Idx int + Buffer string + BufferTokens int + Success bool + Error error } type subscription struct { diff --git a/app/server/types/active_plan_pending_builds.go b/app/server/types/active_plan_pending_builds.go index 02468c9a..3f5901da 100644 --- a/app/server/types/active_plan_pending_builds.go +++ b/app/server/types/active_plan_pending_builds.go @@ -63,12 +63,17 @@ func (ap *ActivePlan) PendingBuildsByPath(orgId, userId string, convoMessagesArg activeBuildsByPath[file] = []*ActiveBuild{} } + if err != nil { + return nil, fmt.Errorf("error getting tokens for file '%s': %v", file, err) + } + activeBuildsByPath[file] = append(activeBuildsByPath[file], &ActiveBuild{ - ReplyId: desc.ConvoMessageId, - Idx: i, - FileContent: parserRes.FileContents[i], - Path: file, - FileDescription: parserRes.FileDescriptions[i], + ReplyId: desc.ConvoMessageId, + Idx: i, + FileContent: parserRes.FileContents[i], + FileContentTokens: parserRes.NumTokensByFile[file], + Path: file, + FileDescription: parserRes.FileDescriptions[i], }) } } diff --git a/app/server/types/reply.go b/app/server/types/reply.go index e6123e17..4ae93868 100644 --- a/app/server/types/reply.go +++ b/app/server/types/reply.go @@ -135,7 +135,14 @@ func (r *ReplyParser) AddChunk(chunk string, addToTotal bool) { r.maybeFilePath = "" r.currentFileLines = []string{} - r.fileDescriptions = append(r.fileDescriptions, strings.Join(r.currentDescriptionLines[0:len(r.currentDescriptionLines)-4], "\n")) + var fileDescription string + if len(r.currentDescriptionLines) > 4 { + fileDescription = strings.Join(r.currentDescriptionLines[0:len(r.currentDescriptionLines)-4], "\n") + r.fileDescriptions = append(r.fileDescriptions, fileDescription) + } else { + r.fileDescriptions = append(r.fileDescriptions, "") + } + r.currentDescriptionLines = []string{""} r.currentDescriptionLineIdx = 0 @@ -223,13 +230,29 @@ func lineHasFilePath(line string) bool { func extractFilePath(line string) string { p := strings.ReplaceAll(line, "**", "") + p = strings.ReplaceAll(p, "`", "") + p = strings.ReplaceAll(p, "'", "") + p = strings.ReplaceAll(p, `"`, "") p = strings.TrimPrefix(p, "-") p = strings.TrimSpace(p) p = strings.TrimPrefix(p, "file:") + p = strings.TrimPrefix(p, "file path:") + p = strings.TrimPrefix(p, "File path:") + p = strings.TrimPrefix(p, "File Path:") p = strings.TrimSuffix(p, ":") p = strings.TrimSpace(p) - split := strings.Split(p, " ") + // split := strings.Split(p, " ") + // if len(split) > 1 { + // p = split[0] + // } + + split := strings.Split(p, ": ") + if len(split) > 1 { + p = split[len(split)-1] + } + + split = strings.Split(p, " (") if len(split) > 1 { p = split[0] } diff --git a/app/server/version.txt b/app/server/version.txt new file mode 100644 index 00000000..bcaffe19 --- /dev/null +++ b/app/server/version.txt @@ -0,0 +1 @@ +0.7.0 \ No newline at end of file diff --git a/app/shared/ai_models.go b/app/shared/ai_models.go index 0019b038..151e5538 100644 --- a/app/shared/ai_models.go +++ b/app/shared/ai_models.go @@ -111,15 +111,15 @@ func init() { Role: ModelRolePlanner, BaseModelConfig: AvailableModelsByName[openai.GPT4TurboPreview], Temperature: 0.4, - TopP: 0.4, + TopP: 0.3, }, PlannerModelConfig: PlannerModelConfigByName[openai.GPT4TurboPreview], }, PlanSummary: ModelRoleConfig{ Role: ModelRolePlanSummary, BaseModelConfig: AvailableModelsByName[openai.GPT4TurboPreview], - Temperature: 0.3, - TopP: 0.3, + Temperature: 0.2, + TopP: 0.2, }, Builder: TaskRoleConfig{ ModelRoleConfig: ModelRoleConfig{ diff --git a/app/shared/plan_result_replacements.go b/app/shared/plan_result_replacements.go index d9b2c4ad..bab63f1f 100644 --- a/app/shared/plan_result_replacements.go +++ b/app/shared/plan_result_replacements.go @@ -60,11 +60,15 @@ func (planState *CurrentPlanState) GetFilesBeforeReplacement( for path, planResults := range planRes.FileResultsByPath { updated := files[path] + // log.Println("path: ", path) PlanResLoop: for _, planRes := range planResults { + // log.Println("planRes: ", planRes.Id) + if !planRes.IsPending() { + // log.Println("Plan result is not pending -- continuing loop") continue } @@ -75,36 +79,62 @@ func (planState *CurrentPlanState) GetFilesBeforeReplacement( updated = planRes.Content files[path] = updated + updatedAtByPath[path] = planRes.CreatedAt + + // log.Println("No replacements for plan result -- creating file and continuing loop") + continue } else if updated == "" { context := planState.ContextsByPath[path] if context == nil { log.Printf("No context for path: %s\n", path) + return nil, fmt.Errorf("no context for path: %s", path) } + // log.Println("No updated content -- setting to context body") + updated = context.Body shas[path] = context.Sha } replacements := []*Replacement{} + foundTarget := false for _, replacement := range planRes.Replacements { if replacement.Id == replacementId { - break PlanResLoop + // log.Println("Found target replacement") + foundTarget = true + break } replacements = append(replacements, replacement) } - var allSucceeded bool - updated, allSucceeded = ApplyReplacements(updated, replacements, false) + if len(replacements) > 0 { + // log.Println("Applying replacements: ") + // for _, replacement := range replacements { + // log.Println(replacement.Id) + // } + + var allSucceeded bool + updated, allSucceeded = ApplyReplacements(updated, replacements, false) - if !allSucceeded { - return nil, fmt.Errorf("plan replacement failed - %s", path) + if !allSucceeded { + return nil, fmt.Errorf("plan replacement failed - %s", path) + } + + // log.Println("Updated content: ") + // log.Println(updated) + + updatedAtByPath[path] = planRes.CreatedAt } - updatedAtByPath[path] = planRes.CreatedAt + if foundTarget { + break PlanResLoop + } } + // log.Println("Setting updated content for path: ", path) + files[path] = updated } diff --git a/releases/cli/versions/0.7.3.md b/releases/cli/versions/0.7.3.md new file mode 100644 index 00000000..4c0b38ce --- /dev/null +++ b/releases/cli/versions/0.7.3.md @@ -0,0 +1,8 @@ +- Fixes for changes TUI replacement view +- Fixes for changes TUI text encoding issue +- Fixes context loading +- `plandex rm` can now remove a directory from context +- `plandex apply` fixes to avoid possible conflicts +- `plandex apply` ask user whether to commit changes +- Context update fixes +- Command suggestions can be disabled with PLANDEX_DISABLE_SUGGESTIONS environment variable \ No newline at end of file diff --git a/releases/server/versions/0.7.0.md b/releases/server/versions/0.7.0.md new file mode 100644 index 00000000..03081f0a --- /dev/null +++ b/releases/server/versions/0.7.0.md @@ -0,0 +1 @@ +Initial release \ No newline at end of file diff --git a/releases/server/versions/CHANGELOG.md b/releases/server/versions/CHANGELOG.md new file mode 100644 index 00000000..e69de29b