Plandex_backup / app /cli /cmd /repl.go
google-labs-jules[bot]
Final deployment for HF with landing page
93d826e
package cmd
import (
"fmt"
"os"
"path/filepath"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/fs"
"plandex-cli/lib"
"plandex-cli/term"
"plandex-cli/types"
"plandex-cli/version"
shared "plandex-shared"
"regexp"
"sort"
"strings"
"unicode"
"github.com/fatih/color"
"github.com/google/uuid"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
"github.com/plandex-ai/go-prompt"
pstrings "github.com/plandex-ai/go-prompt/strings"
"github.com/lithammer/fuzzysearch/fuzzy"
)
var replCmd = &cobra.Command{
Use: "repl",
Short: "Start interactive Plandex REPL",
Run: runRepl,
}
var cliSuggestions []prompt.Suggest
var projectPaths *types.ProjectPaths
var currentPrompt *prompt.Prompt
var replConfig *shared.PlanConfig
var sessionId string
func init() {
RootCmd.AddCommand(replCmd)
replCmd.Flags().BoolP("chat", "c", false, "Start in chat mode")
replCmd.Flags().BoolP("tell", "t", false, "Start in tell mode")
AddNewPlanFlags(replCmd)
for _, config := range term.CliCommands {
if config.Repl {
desc := config.Desc
if config.Alias != "" {
desc = fmt.Sprintf("(\\%s) %s", config.Alias, desc)
}
cliSuggestions = append(cliSuggestions, prompt.Suggest{Text: "\\" + config.Cmd, Description: desc})
}
}
}
func setReplConfig() {
replConfig = lib.MustGetCurrentPlanConfig()
}
func runRepl(cmd *cobra.Command, args []string) {
sessionId = uuid.New().String()
term.SetIsRepl(true)
auth.MustResolveAuthWithOrg()
lib.MustResolveOrCreateProject()
term.StartSpinner("")
lib.LoadState()
chatFlag, err := cmd.Flags().GetBool("chat")
if err != nil {
term.OutputErrorAndExit("Error getting chat flag: %v", err)
}
tellFlag, err := cmd.Flags().GetBool("tell")
if err != nil {
term.OutputErrorAndExit("Error getting tell flag: %v", err)
}
if chatFlag && tellFlag {
term.OutputErrorAndExit("Cannot specify both --chat and --tell flags")
}
if chatFlag {
lib.CurrentReplState.Mode = lib.ReplModeChat
lib.WriteState()
} else if tellFlag {
lib.CurrentReplState.Mode = lib.ReplModeTell
lib.WriteState()
}
afterNew := false
if lib.CurrentPlanId == "" {
os.Setenv("PLANDEX_DISABLE_SUGGESTIONS", "1")
args := []string{}
if noAuto {
args = append(args, "--no-auto")
} else if basicAuto {
args = append(args, "--basic")
} else if plusAuto {
args = append(args, "--plus")
} else if semiAuto {
args = append(args, "--semi")
} else if fullAuto {
args = append(args, "--full")
}
if ossModels {
args = append(args, "--oss")
} else if strongModels {
args = append(args, "--strong")
} else if cheapModels {
args = append(args, "--cheap")
} else if dailyModels {
args = append(args, "--daily")
} else if reasoningModels {
args = append(args, "--reasoning")
} else if geminiPlannerModels {
args = append(args, "--gemini-planner")
} else if o3PlannerModels {
args = append(args, "--o3-planner")
} else if r1PlannerModels {
args = append(args, "--r1-planner")
} else if perplexityPlannerModels {
args = append(args, "--perplexity-planner")
} else if opusPlannerModels {
args = append(args, "--opus-planner")
}
newCmd.Run(newCmd, args)
os.Setenv("PLANDEX_DISABLE_SUGGESTIONS", "")
afterNew = true
}
setReplConfig()
lib.MustVerifyAuthVars(auth.Current.IntegratedModelsMode)
projectPaths, err = fs.GetProjectPaths(fs.Cwd)
if err != nil {
color.New(term.ColorHiRed).Printf("Error getting project paths: %v\n", err)
}
settings, apiErr := api.Client.GetSettings(lib.CurrentPlanId, lib.CurrentBranch)
if apiErr != nil {
term.OutputErrorAndExit("Error getting settings: %v", apiErr.Msg)
}
var printAutoFn func()
var printModelFn func()
if !afterNew {
var didUpdateConfig bool
var updatedConfig *shared.PlanConfig
var updatedSettings *shared.PlanSettings
didUpdateConfig, updatedConfig, printAutoFn = resolveAutoModeSilent(replConfig)
updatedSettings, printModelFn = resolveModelPackSilent(settings)
if didUpdateConfig {
loadMapIfNeeded(replConfig, updatedConfig)
removeMapIfNeeded(replConfig, updatedConfig)
if updatedConfig != nil {
replConfig = updatedConfig
}
}
if updatedSettings != nil {
settings = updatedSettings
}
}
replWelcome(replWelcomeParams{
afterNew: afterNew,
isHelp: false,
printAutoFn: printAutoFn,
printModelFn: printModelFn,
config: replConfig,
packName: settings.GetModelPack().Name,
})
var p *prompt.Prompt
p = prompt.New(
func(in string) { executor(in, p) },
prompt.WithPrefixCallback(func() string {
// Get last part of current working directory
// cwd := fs.Cwd
// dirName := filepath.Base(cwd)
// Build prefix with directory and mode indicator
var modeIcon string
if lib.CurrentReplState.Mode == lib.ReplModeTell {
modeIcon = "⚡️"
if replConfig.AutoApply && replConfig.AutoExec {
modeIcon += "❗️" // warning reminder for auto apply and auto exec
}
} else if lib.CurrentReplState.Mode == lib.ReplModeChat {
modeIcon = "💬"
}
return fmt.Sprintf("%s ", modeIcon)
}),
prompt.WithTitle("Plandex "+version.Version),
prompt.WithSelectedSuggestionBGColor(prompt.LightGray),
prompt.WithSuggestionBGColor(prompt.DarkGray),
prompt.WithCompletionOnDown(),
prompt.WithCompleter(completer),
prompt.WithExecuteOnEnterCallback(executeOnEnter),
prompt.WithHistory(lib.GetHistory()),
)
currentPrompt = p
p.Run()
}
func getSuggestions() []prompt.Suggest {
suggestions := []prompt.Suggest{}
if lib.CurrentReplState.IsMulti {
suggestions = append(suggestions, []prompt.Suggest{
{Text: "\\send", Description: "(\\s) Send the current prompt"},
{Text: "\\multi", Description: "(\\m) Turn multi-line mode off"},
{Text: "\\run", Description: "(\\r) Run a file through tell/chat based on current mode"},
{Text: "\\quit", Description: "(\\q) Exit the REPL"},
}...)
}
if lib.CurrentReplState.Mode == lib.ReplModeTell {
suggestions = append(suggestions, []prompt.Suggest{
{Text: "\\chat", Description: "(\\ch) Switch to 'chat' mode to have a conversation without making changes"},
}...)
} else if lib.CurrentReplState.Mode == lib.ReplModeChat {
suggestions = append(suggestions, []prompt.Suggest{
{Text: "\\tell", Description: "(\\t) Switch to 'tell' mode for implementation"},
}...)
}
if !lib.CurrentReplState.IsMulti {
suggestions = append(suggestions, []prompt.Suggest{
{Text: "\\multi", Description: "(\\m) Turn multi-line mode on"},
{Text: "\\run", Description: "(\\r) Run a file through tell/chat based on current mode"},
{Text: "\\quit", Description: "(\\q) Exit the REPL"},
}...)
}
// Add help command suggestion
suggestions = append(suggestions, prompt.Suggest{Text: "\\help", Description: "(\\h) REPL info and list of commands"})
suggestions = append(suggestions, cliSuggestions...)
for path := range projectPaths.ActivePaths {
if path == "." {
continue
}
isDir := projectPaths.ActiveDirs[path]
if isDir {
path += "/"
}
suggestions = append(suggestions, prompt.Suggest{Text: "@" + path})
loadArgs := path
if isDir {
loadArgs += " -r"
}
suggestions = append(suggestions, prompt.Suggest{Text: "\\load " + loadArgs})
if isDir {
loadArgs = path
loadArgs += " --map"
suggestions = append(suggestions, prompt.Suggest{Text: "\\load " + loadArgs})
loadArgs = path
loadArgs += " --tree"
suggestions = append(suggestions, prompt.Suggest{Text: "\\load " + loadArgs})
}
if filepath.Ext(path) == ".md" || filepath.Ext(path) == ".txt" {
suggestions = append(suggestions, prompt.Suggest{Text: "\\run " + path})
}
}
return suggestions
}
func executeOnEnter(p *prompt.Prompt, indentSize int) (int, bool) {
input := p.Buffer().Text()
cmd, _ := parseCommand(input)
if cmd != "" {
return 0, true
}
if lib.CurrentReplState.IsMulti {
return 0, false
}
return 0, true
}
const cancelOpt = "Cancel"
func executor(in string, p *prompt.Prompt) {
defer lib.WriteHistory(in)
in = strings.TrimSpace(in)
lines := strings.Split(in, "\n")
lastLine := lines[len(lines)-1]
lastLine = strings.TrimSpace(lastLine)
trimmedInput := strings.TrimSpace(in)
if trimmedInput == "" {
return
}
// condense whitespace
condensedInput := strings.Join(strings.Fields(trimmedInput), " ")
// Handle plandex/pdx command prefix
if strings.HasPrefix(lastLine, "plandex ") || strings.HasPrefix(lastLine, "pdx ") {
fmt.Println()
parts := strings.Fields(lastLine)
if len(parts) > 1 {
args := parts[1:] // Skip the "plandex" or "pdx" command
_, err := lib.ExecPlandexCommandWithParams(args, lib.ExecPlandexCommandParams{
SessionId: sessionId,
})
if err != nil {
color.New(term.ColorHiRed).Printf("Error executing command: %v\n", err)
}
}
fmt.Println()
return
}
// Find the last \ or @ in the last line
lastBackslashIndex := strings.LastIndex(lastLine, "\\")
lastAtIndex := strings.LastIndex(lastLine, "@")
var preservedBuffer string
if len(lines) > 1 {
preservedBuffer = strings.Join(lines[:len(lines)-1], "\n") + "\n"
}
suggestions, _, _ := completer(prompt.Document{Text: in})
// Handle file references
if lastAtIndex != -1 && lastAtIndex > lastBackslashIndex {
paths := strings.Split(lastLine, "@")
numPaths := len(paths)
filteredPaths := []string{}
for i, path := range paths {
p := strings.TrimSpace(path)
if i == 0 {
// text before the @
preservedBuffer += p + " "
continue
}
if (p == "" || !projectPaths.ActivePaths[p]) && len(suggestions) > 0 && i == numPaths-1 {
p = strings.Replace(suggestions[0].Text, "@", "", 1)
filteredPaths = append(filteredPaths, p)
} else if projectPaths.ActivePaths[p] {
filteredPaths = append(filteredPaths, p)
}
}
if len(filteredPaths) > 0 {
args := []string{"load"}
args = append(args, filteredPaths...)
args = append(args, "-r")
fmt.Println()
_, err := lib.ExecPlandexCommandWithParams(args, lib.ExecPlandexCommandParams{
SessionId: sessionId,
})
if err != nil {
color.New(term.ColorHiRed).Printf("Error executing command: %v\n", err)
}
fmt.Println()
if preservedBuffer != "" {
p.InsertTextMoveCursor(preservedBuffer, true)
}
return
}
}
// Handle commands
if lastBackslashIndex != -1 {
cmdString := strings.TrimSpace(lastLine[lastBackslashIndex+1:])
if cmdString == "" {
return
}
res := execWithInput(execWithInputParams{
cmdString: cmdString,
in: condensedInput,
lastBackslashIndex: lastBackslashIndex,
preservedBuffer: preservedBuffer,
p: p,
lastLine: lastLine,
condensedInput: condensedInput,
trimmedInput: trimmedInput,
lines: lines,
suggestions: suggestions,
})
if res.shouldReturn {
return
}
condensedInput = res.condensedInput
trimmedInput = res.trimmedInput
} else if len(lines) == 1 {
// Check for likely accidental command inputs (with no backslash) and confirm with user
var allCommands []string
for replCmd := range lib.ReplCmdAliases {
allCommands = append(allCommands, replCmd)
}
for _, config := range term.CliCommands {
if config.Repl {
allCommands = append(allCommands, config.Cmd)
}
}
// Only suggest commands if they're close enough matches
maybeCmds := findSimilarCommands(lastLine, allCommands)
if len(maybeCmds) > 0 {
res := suggestCmds(maybeCmds, getPromptOpt(lastLine))
if res.shouldReturn {
return
}
matchedCmd := res.matchedCmd
if matchedCmd != "" {
res := execWithInput(execWithInputParams{
cmdString: matchedCmd,
in: condensedInput,
lastBackslashIndex: lastBackslashIndex,
preservedBuffer: preservedBuffer,
p: p,
lastLine: lastLine,
condensedInput: condensedInput,
trimmedInput: trimmedInput,
lines: lines,
suggestions: suggestions,
})
if res.shouldReturn {
return
}
condensedInput = res.condensedInput
trimmedInput = res.trimmedInput
}
}
}
// Handle non-command input based on mode
if lib.CurrentReplState.Mode == lib.ReplModeTell {
fmt.Println()
args := []string{"tell", trimmedInput}
var err error
_, err = lib.ExecPlandexCommandWithParams(args, lib.ExecPlandexCommandParams{
SessionId: sessionId,
})
if err != nil {
color.New(term.ColorHiRed).Printf("Error executing tell: %v\n", err)
}
} else if lib.CurrentReplState.Mode == lib.ReplModeChat {
fmt.Println()
args := []string{"chat", trimmedInput}
output, err := lib.ExecPlandexCommandWithParams(args, lib.ExecPlandexCommandParams{
SessionId: sessionId,
})
if err != nil {
color.New(term.ColorHiRed).Printf("Error executing chat: %v\n", err)
}
replacer := strings.NewReplacer("'", "", "\"", "", "*", "", "`", "", "_", "")
output = replacer.Replace(output)
rx := regexp.MustCompile(`(?i)(switch|start|continue|begin|change|chang|move|proceed|go|transition)(ing)?( to | with | into )?(tell|implementation|coding|development)( mode)?`)
if rx.MatchString(output) {
fmt.Println()
res, err := term.ConfirmYesNo("Switch to tell mode for implementation?")
if err != nil {
color.New(term.ColorHiRed).Printf("Error confirming yes/no: %v\n", err)
}
if res {
lib.CurrentReplState.Mode = lib.ReplModeTell
lib.WriteState()
fmt.Println()
color.New(color.BgMagenta, color.FgHiWhite, color.Bold).Println(" ⚡️ Tell mode is enabled ")
fmt.Println()
fmt.Println("Now that you're in tell mode, you can either begin the implementation based on the conversation so far, or you can send another prompt to begin the implementation with additional information or instructions.")
fmt.Println()
beginImplOpt := "Begin implementation"
anotherPromptOpt := "Send another prompt"
sel, err := term.SelectFromList("What would you like to do?", []string{beginImplOpt, anotherPromptOpt})
if err != nil {
color.New(term.ColorHiRed).Printf("Error selecting from list: %v\n", err)
}
if sel == beginImplOpt {
fmt.Println()
args := []string{"tell", "--from-chat"}
_, err := lib.ExecPlandexCommandWithParams(args, lib.ExecPlandexCommandParams{
SessionId: sessionId,
})
if err != nil {
color.New(term.ColorHiRed).Printf("Error executing tell: %v\n", err)
}
}
}
}
}
fmt.Println()
}
func completer(in prompt.Document) ([]prompt.Suggest, pstrings.RuneNumber, pstrings.RuneNumber) {
// Don't show suggestions if we're navigating history
if currentPrompt.IsNavigatingHistory() {
return []prompt.Suggest{}, 0, 0
}
endIndex := in.CurrentRuneIndex()
lines := strings.Split(in.Text, "\n")
currentLineNum := strings.Count(in.TextBeforeCursor(), "\n")
// Don't show suggestions if we're not on the last line
if currentLineNum < len(lines)-1 {
return []prompt.Suggest{}, 0, 0
}
lastLine := lines[len(lines)-1]
if strings.TrimSpace(lastLine) == "" && len(lines) > 1 {
lastLine = lines[len(lines)-2]
}
// Handle plandex/pdx command prefix
if strings.HasPrefix(lastLine, "plandex ") || strings.HasPrefix(lastLine, "pdx ") {
parts := strings.Fields(lastLine)
var prefix string
if len(parts) > 1 {
prefix = parts[len(parts)-1]
}
startIndex := endIndex - pstrings.RuneNumber(len(prefix))
suggestions := []prompt.Suggest{}
for _, config := range term.CliCommands {
suggestions = append(suggestions, prompt.Suggest{
Text: config.Cmd,
Description: config.Desc,
})
}
filtered := prompt.FilterFuzzy(suggestions, prefix, true)
return filtered, startIndex, endIndex
}
// Find the last valid \ or @ in the current line
lastBackslashIndex := -1
lastAtIndex := -1
// Helper function to check if character at index is valid (start of line or after space)
isValidPosition := func(str string, index int) bool {
if index <= 0 {
return true // Start of line
}
return unicode.IsSpace(rune(str[index-1])) // After whitespace
}
// Find last valid backslash
for i := len(lastLine) - 1; i >= 0; i-- {
if lastLine[i] == '\\' && isValidPosition(lastLine, i) {
lastBackslashIndex = i
break
}
}
// Find last valid @
for i := len(lastLine) - 1; i >= 0; i-- {
if lastLine[i] == '@' && isValidPosition(lastLine, i) {
lastAtIndex = i
break
}
}
var w string
var startIndex pstrings.RuneNumber
if lastBackslashIndex == -1 && lastAtIndex == -1 {
return []prompt.Suggest{}, 0, 0
}
// Use the rightmost special character
if lastBackslashIndex > lastAtIndex {
// Get everything after the last backslash
w = lastLine[lastBackslashIndex:]
startIndex = endIndex - pstrings.RuneNumber(len(w))
} else if lastAtIndex != -1 {
// Get everything after the last @
w = lastLine[lastAtIndex:]
startIndex = endIndex - pstrings.RuneNumber(len(w))
}
// Verify this is at the end of the line (allowing for trailing spaces)
if !strings.HasSuffix(strings.TrimSpace(lastLine), strings.TrimSpace(w)) {
return []prompt.Suggest{}, 0, 0
}
wTrimmed := strings.TrimSpace(strings.TrimPrefix(w, "\\"))
parts := strings.Split(wTrimmed, " ")
wCmd := parts[0]
// For commands, verify it starts with an actual command
if strings.HasPrefix(w, "\\") {
isValidCommand := false
for _, config := range term.CliCommands {
if !config.Repl {
continue
}
if strings.HasPrefix(config.Cmd, wCmd) ||
(config.Alias != "" && strings.HasPrefix(config.Alias, wCmd)) {
isValidCommand = true
break
}
}
// Also check built-in REPL commands
if strings.HasPrefix("quit", wCmd) ||
strings.HasPrefix("multi", wCmd) ||
strings.HasPrefix("tell", wCmd) ||
strings.HasPrefix("chat", wCmd) ||
strings.HasPrefix("send", wCmd) ||
strings.HasPrefix("run", wCmd) {
isValidCommand = true
}
if !isValidCommand && wCmd != "" {
return []prompt.Suggest{}, 0, 0
}
}
fuzzySuggestions := prompt.FilterFuzzy(getSuggestions(), w, true)
prefixMatches := prompt.FilterHasPrefix(getSuggestions(), w, true)
runFilteredFuzzy := []prompt.Suggest{}
runFilteredPrefixMatches := []prompt.Suggest{}
for _, s := range fuzzySuggestions {
if strings.HasPrefix(s.Text, "\\run ") {
if wCmd == "run" {
runFilteredFuzzy = append(runFilteredFuzzy, s)
}
} else {
runFilteredFuzzy = append(runFilteredFuzzy, s)
}
}
for _, s := range prefixMatches {
if strings.HasPrefix(s.Text, "\\run ") {
if wCmd == "run" {
runFilteredPrefixMatches = append(runFilteredPrefixMatches, s)
}
} else {
runFilteredPrefixMatches = append(runFilteredPrefixMatches, s)
}
}
fuzzySuggestions = runFilteredFuzzy
prefixMatches = runFilteredPrefixMatches
loadFilteredFuzzy := []prompt.Suggest{}
loadFilteredPrefixMatches := []prompt.Suggest{}
for _, s := range fuzzySuggestions {
if strings.HasPrefix(s.Text, "\\load ") {
if wCmd == "load" {
loadFilteredFuzzy = append(loadFilteredFuzzy, s)
}
} else {
loadFilteredFuzzy = append(loadFilteredFuzzy, s)
}
}
for _, s := range prefixMatches {
if strings.HasPrefix(s.Text, "\\load ") {
if wCmd == "load" {
loadFilteredPrefixMatches = append(loadFilteredPrefixMatches, s)
}
} else {
loadFilteredPrefixMatches = append(loadFilteredPrefixMatches, s)
}
}
fuzzySuggestions = loadFilteredFuzzy
prefixMatches = loadFilteredPrefixMatches
if strings.TrimSpace(w) != "\\" {
sort.Slice(prefixMatches, func(i, j int) bool {
iTxt := prefixMatches[i].Text
jTxt := prefixMatches[j].Text
if iTxt == "\\chat" || iTxt == "\\tell" || iTxt == "\\multi" || iTxt == "\\quit" || iTxt == "\\send" || iTxt == "\\run" {
return true
}
if jTxt == "\\chat" || jTxt == "\\tell" || jTxt == "\\multi" || jTxt == "\\quit" || jTxt == "\\send" || jTxt == "\\run" {
return false
}
return prefixMatches[i].Text < prefixMatches[j].Text
})
}
if len(prefixMatches) > 0 {
// Remove prefix matches from fuzzy results to avoid duplicates
prefixMatchSet := make(map[string]bool)
for _, s := range prefixMatches {
prefixMatchSet[s.Text] = true
}
nonPrefixFuzzy := make([]prompt.Suggest, 0)
for _, s := range fuzzySuggestions {
if !prefixMatchSet[s.Text] {
nonPrefixFuzzy = append(nonPrefixFuzzy, s)
}
}
fuzzySuggestions = append(prefixMatches, nonPrefixFuzzy...)
}
var aliasMatch string
if lib.ReplCmdAliases[wTrimmed] != "" {
aliasMatch = "\\" + lib.ReplCmdAliases[wTrimmed]
} else {
for _, s := range term.CliCommands {
if s.Alias == wTrimmed {
aliasMatch = "\\" + s.Cmd
break
}
}
}
if aliasMatch != "" {
// put the suggestion with the alias match at the beginning
var matched prompt.Suggest
found := false
for _, s := range fuzzySuggestions {
if s.Text == aliasMatch {
matched = s
found = true
break
}
}
if found {
newSuggestions := []prompt.Suggest{}
newSuggestions = append(newSuggestions, matched)
for _, s := range fuzzySuggestions {
if s.Text != aliasMatch {
newSuggestions = append(newSuggestions, s)
}
}
fuzzySuggestions = newSuggestions
}
}
return fuzzySuggestions, startIndex, endIndex
}
type replWelcomeParams struct {
afterNew bool
isHelp bool
printAutoFn func()
printModelFn func()
packName string
config *shared.PlanConfig
}
func replWelcome(params replWelcomeParams) {
// print REPL welcome message and basic info
// have to make these requests serially in case re-authentication is needed
afterNew := params.afterNew
isHelp := params.isHelp
printAutoFn := params.printAutoFn
printModelFn := params.printModelFn
packName := params.packName
plan, apiErr := api.Client.GetPlan(lib.CurrentPlanId)
if apiErr != nil {
term.OutputErrorAndExit("Error getting plan: %v", apiErr.Msg)
}
config := params.config
if config == nil {
config, apiErr = api.Client.GetPlanConfig(lib.CurrentPlanId)
if apiErr != nil {
term.OutputErrorAndExit("Error getting plan config: %v", apiErr.Msg)
}
}
currentBranchesByPlanId, err := api.Client.GetCurrentBranchByPlanId(lib.CurrentProjectId, shared.GetCurrentBranchByPlanIdRequest{
CurrentBranchByPlanId: map[string]string{
lib.CurrentPlanId: lib.CurrentBranch,
},
})
if err != nil {
term.OutputErrorAndExit("Error getting current branches: %v", err)
}
term.StopSpinner()
if !afterNew {
fmt.Println()
}
color.New(color.FgHiWhite, color.BgBlue, color.Bold).Print(" 👋 Welcome to Plandex ")
versionStr := version.Version
if versionStr != "development" {
color.New(color.FgHiWhite, color.BgHiBlack).Printf(" v%s ", versionStr)
}
fmt.Println()
fmt.Println()
fmt.Println(lib.GetCurrentPlanTable(plan, currentBranchesByPlanId, nil))
table := tablewriter.NewWriter(os.Stdout)
table.SetAutoWrapText(false)
var contextMode string
if config.AutoLoadContext {
contextMode = "auto"
} else {
contextMode = "manual"
}
filesStr := "%s for loading files into context"
if contextMode == "auto" {
filesStr += " manually (optional)"
}
filesStr += "\n"
color.New(color.FgHiWhite).Printf("%s for commands\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\"))
color.New(color.FgHiWhite).Printf(filesStr, color.New(term.ColorHiCyan, color.Bold).Sprint("@"))
color.New(color.FgHiWhite).Printf("%s (\\h) for help\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\help"))
color.New(color.FgHiWhite).Printf("%s (\\q) to exit\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\quit"))
fmt.Println()
if printAutoFn != nil {
printAutoFn()
} else {
printAutoModeTable(config)
}
color.New(color.FgHiWhite).Printf("%s to change auto mode\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\set-auto"))
color.New(color.FgHiWhite).Printf("%s to see config\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\config"))
color.New(color.FgHiWhite).Printf("%s to customize config\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\set-config"))
fmt.Println()
if printModelFn != nil {
printModelFn()
} else {
printModelPackTable(packName)
}
color.New(color.FgHiWhite).Printf("%s to see model details\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\models"))
color.New(color.FgHiWhite).Printf("%s to change models\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\set-model"))
showReplMode()
showMultiLineMode()
fmt.Println()
if !isHelp {
if lib.CurrentReplState.Mode == lib.ReplModeTell {
color.New(color.FgHiWhite, color.BgBlue, color.Bold).Println(" Describe a coding task 👇 ")
} else {
color.New(color.FgHiWhite, color.BgBlue, color.Bold).Println(" Ask a question or chat 👇 ")
}
fmt.Println()
}
}
func replHelp() {
replWelcome(replWelcomeParams{
afterNew: false,
isHelp: true,
})
term.PrintHelpAllCommands()
}
func showReplMode() {
fmt.Println()
if lib.CurrentReplState.Mode == lib.ReplModeTell {
color.New(color.BgMagenta, color.FgHiWhite, color.Bold).Println(" ⚡️ Tell mode is enabled ")
color.New(color.FgHiWhite).Printf("%s (\\ch) switch to chat mode to chat without writing code or making changes\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\chat"))
} else if lib.CurrentReplState.Mode == lib.ReplModeChat {
color.New(color.BgMagenta, color.FgHiWhite, color.Bold).Println(" 💬 Chat mode is enabled ")
color.New(color.FgHiWhite).Printf("%s (\\t) switch to tell mode to start writing code and implementing tasks\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\tell"))
}
fmt.Println()
}
func showMultiLineMode() {
if lib.CurrentReplState.IsMulti {
color.New(color.BgMagenta, color.FgHiWhite, color.Bold).Println(" 🔢 Multi-line mode is enabled ")
fmt.Printf("%s to exit multi-line mode\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\multi"))
fmt.Printf("%s for line breaks\n", color.New(term.ColorHiCyan, color.Bold).Sprint("enter"))
fmt.Printf("%s to send prompt\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\send"))
} else {
color.New(color.BgMagenta, color.FgHiWhite, color.Bold).Println(" 1️⃣ Multi-line mode is disabled ")
fmt.Printf("%s for multi-line editing mode\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\multi"))
fmt.Printf("%s to send prompt\n", color.New(term.ColorHiCyan, color.Bold).Sprint("enter"))
}
}
func parseCommand(in string) (string, string) {
in = strings.TrimSpace(in)
lines := strings.Split(in, "\n")
lastLine := lines[len(lines)-1]
lastLine = strings.TrimSpace(lastLine)
input := strings.TrimSpace(in)
if input == "" {
return "", ""
}
// Handle plandex/pdx command prefix
if strings.HasPrefix(lastLine, "plandex ") || strings.HasPrefix(lastLine, "pdx ") {
return lastLine, lastLine
}
// Find the last \ or @ in the last line
lastBackslashIndex := strings.LastIndex(lastLine, "\\")
lastAtIndex := strings.LastIndex(lastLine, "@")
suggestions, _, _ := completer(prompt.Document{Text: in})
// Handle file references
if lastAtIndex != -1 && lastAtIndex > lastBackslashIndex {
paths := strings.Split(lastLine, "@")
split2 := strings.SplitN(lastLine, "@", 2)
numPaths := len(paths)
filteredPaths := []string{}
for i, path := range paths {
p := strings.TrimSpace(path)
if i == 0 {
// text before the @
continue
}
if (p == "" || !projectPaths.ActivePaths[p]) && len(suggestions) > 0 && i == numPaths-1 {
p = strings.Replace(suggestions[0].Text, "@", "", 1)
filteredPaths = append(filteredPaths, p)
} else if projectPaths.ActivePaths[p] {
filteredPaths = append(filteredPaths, p)
}
}
if len(filteredPaths) > 0 {
res := ""
for _, p := range filteredPaths {
res += "@" + p + " "
}
return res, split2[1]
}
}
// Handle commands
if lastBackslashIndex != -1 {
cmdString := strings.TrimSpace(lastLine[lastBackslashIndex+1:])
if cmdString == "" {
return "", ""
}
// Split into command and args
parts := strings.Fields(cmdString)
cmd := parts[0]
args := parts[1:]
// Handle built-in REPL commands
switch cmd {
case "quit", lib.ReplCmdAliases["quit"]:
return "\\quit", "\\" + cmdString
case "help", lib.ReplCmdAliases["help"]:
return "\\help", "\\" + cmdString
case "multi", lib.ReplCmdAliases["multi"]:
return "\\multi", "\\" + cmdString
case "send", lib.ReplCmdAliases["send"]:
return "\\send", "\\" + cmdString
case "tell", lib.ReplCmdAliases["tell"]:
return "\\tell", "\\" + cmdString
case "chat", lib.ReplCmdAliases["chat"]:
return "\\chat", "\\" + cmdString
case "run", lib.ReplCmdAliases["run"]:
return "\\run", "\\" + cmdString
default:
// Check CLI commands
var matchedCmd string
for _, config := range term.CliCommands {
if (cmd == config.Cmd || (config.Alias != "" && cmd == config.Alias)) && config.Repl {
matchedCmd = config.Cmd
break
}
}
if matchedCmd == "" {
for _, config := range term.CliCommands {
if strings.HasPrefix(config.Cmd, cmd) && config.Repl {
matchedCmd = config.Cmd
break
}
}
}
if matchedCmd != "" {
res := matchedCmd
if len(args) > 0 {
res += " " + strings.Join(args, " ")
}
return res, "\\" + cmdString
}
}
}
return "", ""
}
func isFileInProjectPaths(filePath string) bool {
// Convert to absolute path
absPath, err := filepath.Abs(filePath)
if err != nil {
return false
}
// Check if file is within any project path
for path := range projectPaths.ActivePaths {
projectAbs, err := filepath.Abs(path)
if err != nil {
continue
}
if strings.HasPrefix(absPath, projectAbs) {
return true
}
}
return false
}
func handleRunCommand(args []string) error {
if len(args) != 1 {
return fmt.Errorf("run command requires exactly one file path argument")
}
filePath := args[0]
// Check if file exists
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return fmt.Errorf("file does not exist: %s", filePath)
}
// Build command based on current mode
var cmdArgs []string
if lib.CurrentReplState.Mode == lib.ReplModeTell {
cmdArgs = []string{"tell", "-f", filePath}
} else {
cmdArgs = []string{"chat", "-f", filePath}
}
// Execute the command
_, err := lib.ExecPlandexCommand(cmdArgs)
if err != nil {
return fmt.Errorf("error executing command: %v", err)
}
return nil
}
func getPromptOpt(cmd string) string {
asPrompt := cmd
if len(asPrompt) > 20 {
asPrompt = asPrompt[:20] + "..."
}
return fmt.Sprintf("Send '%s' as a prompt to the AI model", asPrompt)
}
type suggestCmdsResult struct {
shouldReturn bool
matchedCmd string
}
func suggestCmds(cmds []string, promptOpt string) suggestCmdsResult {
var matchedCmd string
fmt.Println()
opts := []string{}
for _, match := range cmds {
opts = append(opts, "\\"+match)
}
opts = append(opts, cancelOpt, promptOpt)
sel, err := term.SelectFromList("🤔 Did you mean to type one of these commands?", opts)
if err != nil {
color.New(term.ColorHiRed).Printf("Error selecting from list: %v\n", err)
}
if sel == cancelOpt {
return suggestCmdsResult{shouldReturn: true}
} else if sel != promptOpt {
matchedCmd = strings.Replace(sel, "\\", "", 1)
}
return suggestCmdsResult{matchedCmd: matchedCmd}
}
type execWithInputParams struct {
cmdString string
in string
lastBackslashIndex int
preservedBuffer string
p *prompt.Prompt
lastLine string
condensedInput string
trimmedInput string
lines []string
suggestions []prompt.Suggest
}
type execWithInputResult struct {
shouldReturn bool
condensedInput string
trimmedInput string
}
func execWithInput(params execWithInputParams) execWithInputResult {
cmdString := params.cmdString
in := params.in
lastBackslashIndex := params.lastBackslashIndex
preservedBuffer := params.preservedBuffer
lastLine := params.lastLine
p := params.p
condensedInput := params.condensedInput
trimmedInput := params.trimmedInput
lines := params.lines
suggestions := params.suggestions
// Split into command and args
parts := strings.Fields(cmdString)
cmd := parts[0]
args := parts[1:]
var fuzzyNEQCheckCmds []string
for replCmd := range lib.ReplCmdAliases {
if replCmd != cmd {
fuzzyNEQCheckCmds = append(fuzzyNEQCheckCmds, replCmd)
}
}
for _, config := range term.CliCommands {
if !config.Repl {
continue
}
if config.Cmd != cmd {
fuzzyNEQCheckCmds = append(fuzzyNEQCheckCmds, config.Cmd)
}
}
fuzzyNEQMatches := findSimilarCommands(cmd, fuzzyNEQCheckCmds)
// Handle built-in REPL commands
switch {
case cmd == "quit" || cmd == lib.ReplCmdAliases["quit"]:
lib.WriteHistory(in)
os.Exit(0)
case cmd == "help" || cmd == lib.ReplCmdAliases["help"]:
if lastBackslashIndex > 0 {
preservedBuffer += lastLine[:lastBackslashIndex]
}
replHelp()
fmt.Println()
if preservedBuffer != "" {
p.InsertTextMoveCursor(preservedBuffer, true)
}
return execWithInputResult{shouldReturn: true}
case cmd == "multi" || cmd == lib.ReplCmdAliases["multi"]:
if lastBackslashIndex > 0 {
preservedBuffer += lastLine[:lastBackslashIndex]
}
fmt.Println()
lib.CurrentReplState.IsMulti = !lib.CurrentReplState.IsMulti
showMultiLineMode()
lib.WriteState()
fmt.Println()
if preservedBuffer != "" {
p.InsertTextMoveCursor(preservedBuffer, true)
}
return execWithInputResult{shouldReturn: true}
case cmd == "send" || cmd == lib.ReplCmdAliases["send"]:
condensedSplit := strings.Split(condensedInput, "\\s")
condensedInput = strings.TrimSpace(condensedSplit[0])
condensedInput = strings.TrimSpace(condensedInput)
trimmedSplit := strings.Split(trimmedInput, "\\s")
trimmedInput = strings.TrimSpace(trimmedSplit[0])
trimmedInput = strings.TrimSpace(trimmedInput)
if condensedInput == "" {
fmt.Println()
fmt.Println("🤷‍♂️ No prompt to send")
fmt.Println()
return execWithInputResult{shouldReturn: true}
}
case cmd == "tell" || cmd == lib.ReplCmdAliases["tell"]:
if lastBackslashIndex > 0 {
preservedBuffer += lastLine[:lastBackslashIndex]
}
lib.CurrentReplState.Mode = lib.ReplModeTell
lib.WriteState()
showReplMode()
if preservedBuffer != "" {
p.InsertTextMoveCursor(preservedBuffer, true)
}
return execWithInputResult{shouldReturn: true}
case cmd == "chat" || cmd == lib.ReplCmdAliases["chat"]:
if lastBackslashIndex > 0 {
preservedBuffer += lastLine[:lastBackslashIndex]
}
lib.CurrentReplState.Mode = lib.ReplModeChat
lib.WriteState()
showReplMode()
if preservedBuffer != "" {
p.InsertTextMoveCursor(preservedBuffer, true)
}
return execWithInputResult{shouldReturn: true}
case cmd == "run" || cmd == lib.ReplCmdAliases["run"]:
if lastBackslashIndex > 0 {
preservedBuffer += lastLine[:lastBackslashIndex]
}
fmt.Println()
if err := handleRunCommand(args); err != nil {
color.New(term.ColorHiRed).Printf("Run command failed: %v\n", err)
}
fmt.Println()
if preservedBuffer != "" {
p.InsertTextMoveCursor(preservedBuffer, true)
}
return execWithInputResult{shouldReturn: true}
default:
// Check CLI commands
var matchedCmd string
for _, config := range term.CliCommands {
if (cmd == config.Cmd || (config.Alias != "" && cmd == config.Alias)) && config.Repl {
matchedCmd = config.Cmd
break
}
}
if matchedCmd == "" && len(suggestions) > 0 {
matchedCmd = strings.Replace(suggestions[0].Text, "\\", "", 1)
return execWithInput(execWithInputParams{
cmdString: matchedCmd,
in: condensedInput,
lastBackslashIndex: lastBackslashIndex,
preservedBuffer: preservedBuffer,
p: p,
lastLine: lastLine,
condensedInput: condensedInput,
trimmedInput: trimmedInput,
lines: lines,
suggestions: suggestions,
})
}
if matchedCmd == "" {
promptOpt := getPromptOpt(cmd)
if len(fuzzyNEQMatches) > 0 {
res := suggestCmds(fuzzyNEQMatches, promptOpt)
if res.shouldReturn {
return execWithInputResult{shouldReturn: true}
}
matchedCmd = res.matchedCmd
return execWithInput(execWithInputParams{
cmdString: matchedCmd,
in: condensedInput,
lastBackslashIndex: lastBackslashIndex,
preservedBuffer: preservedBuffer,
p: p,
lastLine: lastLine,
condensedInput: condensedInput,
trimmedInput: trimmedInput,
lines: lines,
suggestions: suggestions,
})
} else if len(lines) == 1 && strings.HasPrefix(trimmedInput, "\\") {
showCmdsOpt := "Show available commands"
opts := []string{cancelOpt, showCmdsOpt, promptOpt}
sel, err := term.SelectFromList("🤔 Couldn't find a matching command. What do you want to do?", opts)
if err != nil {
color.New(term.ColorHiRed).Printf("Error selecting from list: %v\n", err)
}
if sel == cancelOpt {
return execWithInputResult{shouldReturn: true}
} else if sel == showCmdsOpt {
replHelp()
fmt.Println()
return execWithInputResult{shouldReturn: true}
}
}
}
if matchedCmd != "" {
// fmt.Println("> plandex " + config.Cmd)
if lastBackslashIndex > 0 {
preservedBuffer += lastLine[:lastBackslashIndex]
}
fmt.Println()
execArgs := []string{matchedCmd}
if matchedCmd == "continue" && chatOnly {
execArgs = append(execArgs, "--chat")
}
execArgs = append(execArgs, args...)
_, err := lib.ExecPlandexCommandWithParams(execArgs, lib.ExecPlandexCommandParams{
SessionId: sessionId,
})
if err != nil {
color.New(term.ColorHiRed).Printf("Error executing command: %v\n", err)
}
fmt.Println()
if preservedBuffer != "" {
p.InsertTextMoveCursor(preservedBuffer, true)
}
if strings.HasPrefix(matchedCmd, "set-auto") || strings.HasPrefix(matchedCmd, "set-config") {
term.StartSpinner("")
setReplConfig()
term.StopSpinner()
}
return execWithInputResult{shouldReturn: true}
}
}
return execWithInputResult{
condensedInput: condensedInput,
trimmedInput: trimmedInput,
}
}
func findSimilarCommands(input string, commands []string) []string {
input = strings.TrimSpace(input)
input = strings.ToLower(input)
input = strings.Trim(input, "/")
// Get ranked matches
ranks := fuzzy.RankFind(input, commands)
// Filter strictly by distance
var filtered []string
for _, rank := range ranks {
// include if either is a substring of the other
if strings.Contains(rank.Target, input) || strings.Contains(input, rank.Target) {
filtered = append(filtered, rank.Target)
continue
}
// Normalize threshold based on command length
maxLen := len(input)
if len(rank.Target) > maxLen {
maxLen = len(rank.Target)
}
threshold := 4 // Base threshold
if maxLen < 5 {
threshold = 1 // Stricter for very short commands
}
if rank.Distance <= threshold {
filtered = append(filtered, rank.Target)
}
}
return filtered
}