777 lines
21 KiB
Go
777 lines
21 KiB
Go
package lib
|
||
|
||
import (
|
||
"bufio"
|
||
"encoding/base64"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"path/filepath"
|
||
"plandex-cli/api"
|
||
"plandex-cli/auth"
|
||
"plandex-cli/fs"
|
||
"plandex-cli/term"
|
||
"plandex-cli/types"
|
||
"plandex-cli/url"
|
||
"strings"
|
||
"sync"
|
||
|
||
shared "plandex-shared"
|
||
|
||
"github.com/fatih/color"
|
||
)
|
||
|
||
const maxSkippedFileList = 20
|
||
|
||
func MustLoadContext(resources []string, params *types.LoadContextParams) {
|
||
if params.DefsOnly {
|
||
// while caching is set up to work with multiple map paths, it can end up in a partially loaded state if token limits are exceeded, so better to just load one at a time
|
||
if len(resources) > 1 {
|
||
term.OutputErrorAndExit("Please load a single map directory at a time")
|
||
}
|
||
|
||
term.LongSpinnerWithWarning("🗺️ Building project map...", "🗺️ This can take a while in larger projects...")
|
||
} else if params.NamesOnly {
|
||
term.LongSpinnerWithWarning("🌳 Loading directory tree...", "🌳 This can take a while in larger projects...")
|
||
} else {
|
||
term.StartSpinner("📥 Loading context...")
|
||
}
|
||
|
||
onErr := func(err error) {
|
||
term.StopSpinner()
|
||
term.OutputErrorAndExit("Failed to load context: %v", err)
|
||
}
|
||
|
||
var loadContextReq shared.LoadContextRequest
|
||
|
||
fileInfo, err := os.Stdin.Stat()
|
||
if err != nil {
|
||
onErr(fmt.Errorf("failed to stat stdin: %v", err))
|
||
}
|
||
|
||
var authVars map[string]string
|
||
var openAIBase string
|
||
|
||
if params.Note != "" || fileInfo.Mode()&os.ModeNamedPipe != 0 {
|
||
authVars = MustVerifyAuthVarsSilent(auth.Current.IntegratedModelsMode)
|
||
}
|
||
|
||
if params.Note != "" {
|
||
loadContextReq = append(loadContextReq, &shared.LoadContextParams{
|
||
ContextType: shared.ContextNoteType,
|
||
Body: params.Note,
|
||
ApiKeys: authVars,
|
||
OpenAIBase: openAIBase,
|
||
OpenAIOrgId: os.Getenv("OPENAI_ORG_ID"),
|
||
SessionId: params.SessionId,
|
||
AutoLoaded: params.AutoLoaded,
|
||
})
|
||
}
|
||
|
||
if fileInfo.Mode()&os.ModeNamedPipe != 0 {
|
||
reader := bufio.NewReader(os.Stdin)
|
||
pipedData, err := io.ReadAll(reader)
|
||
if err != nil {
|
||
onErr(fmt.Errorf("failed to read piped data: %v", err))
|
||
}
|
||
|
||
if len(pipedData) > 0 {
|
||
loadContextReq = append(loadContextReq, &shared.LoadContextParams{
|
||
ContextType: shared.ContextPipedDataType,
|
||
Body: string(pipedData),
|
||
ApiKeys: authVars,
|
||
OpenAIBase: openAIBase,
|
||
OpenAIOrgId: os.Getenv("OPENAI_ORG_ID"),
|
||
SessionId: params.SessionId,
|
||
AutoLoaded: params.AutoLoaded,
|
||
})
|
||
}
|
||
}
|
||
|
||
var inputUrls []string
|
||
var inputFilePaths []string
|
||
|
||
if len(resources) > 0 {
|
||
for _, resource := range resources {
|
||
// so far resources are either files or urls
|
||
if url.IsValidURL(resource) {
|
||
inputUrls = append(inputUrls, resource)
|
||
} else {
|
||
if strings.HasPrefix(resource, "."+string(os.PathSeparator)) {
|
||
resource = resource[2:]
|
||
}
|
||
|
||
inputFilePaths = append(inputFilePaths, resource)
|
||
}
|
||
}
|
||
}
|
||
|
||
var contextMu sync.Mutex
|
||
|
||
errCh := make(chan error)
|
||
ignoredPaths := make(map[string]string)
|
||
|
||
mapFilesTruncatedTooLarge := []filePathWithSize{}
|
||
mapFilesSkippedAfterSizeLimit := []string{}
|
||
|
||
// We'll reuse these for all skipping, including directory-tree partial skipping and URLs
|
||
filesSkippedTooLarge := []filePathWithSize{}
|
||
filesSkippedAfterSizeLimit := []string{}
|
||
|
||
var totalSize int64
|
||
|
||
numRoutines := 0
|
||
|
||
// filter out already loaded contexts
|
||
alreadyLoadedByComposite := make(map[string]*shared.Context)
|
||
existingContexts, apiErr := api.Client.ListContext(CurrentPlanId, CurrentBranch)
|
||
if apiErr != nil {
|
||
onErr(fmt.Errorf("failed to list contexts: %v", apiErr.Msg))
|
||
}
|
||
|
||
existsByComposite := make(map[string]*shared.Context)
|
||
for _, context := range existingContexts {
|
||
switch context.ContextType {
|
||
case shared.ContextFileType, shared.ContextDirectoryTreeType, shared.ContextMapType, shared.ContextImageType:
|
||
existsByComposite[strings.Join([]string{string(context.ContextType), context.FilePath}, "|")] = context
|
||
case shared.ContextURLType:
|
||
existsByComposite[strings.Join([]string{string(context.ContextType), context.Url}, "|")] = context
|
||
}
|
||
}
|
||
|
||
var cachedMapPaths map[string]bool
|
||
var cachedMapLoadRes *shared.LoadContextResponse
|
||
|
||
mapInputShas := map[string]string{}
|
||
mapInputTokens := map[string]int{}
|
||
mapInputSizes := map[string]int64{}
|
||
|
||
toLoadMapPaths := []string{}
|
||
mapInputPathsForPaths := map[string]string{}
|
||
|
||
currentMapInputBatch := shared.FileMapInputs{}
|
||
mapInputBatches := []shared.FileMapInputs{currentMapInputBatch}
|
||
|
||
sem := make(chan struct{}, ContextMapMaxClientConcurrency)
|
||
|
||
if len(inputFilePaths) > 0 {
|
||
|
||
var mapSize int64
|
||
|
||
if params.DefsOnly {
|
||
for _, inputFilePath := range inputFilePaths {
|
||
composite := strings.Join([]string{string(shared.ContextMapType), inputFilePath}, "|")
|
||
if existsByComposite[composite] != nil {
|
||
alreadyLoadedByComposite[composite] = existsByComposite[composite]
|
||
continue
|
||
}
|
||
|
||
toLoadMapPaths = append(toLoadMapPaths, inputFilePath)
|
||
}
|
||
|
||
var uncachedMapPaths []string
|
||
|
||
res, err := api.Client.LoadCachedFileMap(CurrentPlanId, CurrentBranch, shared.LoadCachedFileMapRequest{
|
||
FilePaths: toLoadMapPaths,
|
||
})
|
||
|
||
if err != nil {
|
||
onErr(fmt.Errorf("error checking cached file map: %v", err))
|
||
}
|
||
|
||
if res.LoadRes != nil {
|
||
if res.LoadRes.MaxTokensExceeded {
|
||
term.StopSpinner()
|
||
overage := res.LoadRes.TotalTokens - res.LoadRes.MaxTokens
|
||
|
||
term.OutputErrorAndExit("Update would add %d 🪙 and exceed token limit (%d) by %d 🪙\n", res.LoadRes.TokensAdded, res.LoadRes.MaxTokens, overage)
|
||
}
|
||
|
||
cachedMapLoadRes = res.LoadRes
|
||
cachedMapPaths = res.CachedByPath
|
||
|
||
for _, path := range toLoadMapPaths {
|
||
if !cachedMapPaths[path] {
|
||
uncachedMapPaths = append(uncachedMapPaths, path)
|
||
}
|
||
}
|
||
} else {
|
||
uncachedMapPaths = toLoadMapPaths
|
||
}
|
||
|
||
toLoadMapPaths = uncachedMapPaths
|
||
inputFilePaths = toLoadMapPaths
|
||
}
|
||
|
||
if len(inputFilePaths) > 0 {
|
||
baseDir := fs.GetBaseDirForFilePaths(inputFilePaths)
|
||
|
||
paths, err := fs.GetProjectPaths(baseDir)
|
||
if err != nil {
|
||
onErr(fmt.Errorf("failed to get project paths: %v", err))
|
||
}
|
||
|
||
if !params.ForceSkipIgnore {
|
||
var filteredPaths []string
|
||
for _, inputFilePath := range inputFilePaths {
|
||
if _, ok := paths.ActivePaths[inputFilePath]; !ok {
|
||
ignored, reason, err := fs.IsIgnored(paths, inputFilePath, baseDir)
|
||
if err != nil {
|
||
onErr(fmt.Errorf("failed to check if %s is ignored: %v", inputFilePath, err))
|
||
}
|
||
if ignored {
|
||
ignoredPaths[inputFilePath] = reason
|
||
}
|
||
} else {
|
||
filteredPaths = append(filteredPaths, inputFilePath)
|
||
}
|
||
}
|
||
inputFilePaths = filteredPaths
|
||
|
||
}
|
||
|
||
if params.NamesOnly {
|
||
// "params.NamesOnly" => we create directory-tree contexts (ContextDirectoryTreeType)
|
||
// Partial skipping of subpaths
|
||
for _, inputFilePath := range inputFilePaths {
|
||
composite := strings.Join([]string{string(shared.ContextDirectoryTreeType), inputFilePath}, "|")
|
||
if existsByComposite[composite] != nil {
|
||
alreadyLoadedByComposite[composite] = existsByComposite[composite]
|
||
continue
|
||
}
|
||
|
||
numRoutines++
|
||
go func(inputFilePath string) {
|
||
sem <- struct{}{}
|
||
defer func() { <-sem }()
|
||
|
||
flattenedPaths, err := ParseInputPaths(ParseInputPathsParams{
|
||
FileOrDirPaths: []string{inputFilePath},
|
||
BaseDir: baseDir,
|
||
ProjectPaths: paths,
|
||
LoadParams: params,
|
||
})
|
||
if err != nil {
|
||
errCh <- fmt.Errorf("failed to parse input paths: %v", err)
|
||
return
|
||
}
|
||
|
||
if !params.ForceSkipIgnore {
|
||
var filteredPaths []string
|
||
for _, path := range flattenedPaths {
|
||
if _, ok := paths.ActivePaths[path]; ok {
|
||
filteredPaths = append(filteredPaths, path)
|
||
} else {
|
||
ignored, reason, err := fs.IsIgnored(paths, path, baseDir)
|
||
if err != nil {
|
||
errCh <- fmt.Errorf("failed to check if %s is ignored: %v", path, err)
|
||
return
|
||
}
|
||
if ignored {
|
||
ignoredPaths[path] = reason
|
||
}
|
||
}
|
||
}
|
||
flattenedPaths = filteredPaths
|
||
}
|
||
|
||
// PARTIAL skipping of subpaths
|
||
var keptPaths []string
|
||
for _, p := range flattenedPaths {
|
||
lineSize := int64(len(p))
|
||
|
||
contextMu.Lock()
|
||
if lineSize > shared.MaxContextBodySize {
|
||
filesSkippedTooLarge = append(filesSkippedTooLarge, filePathWithSize{Path: p, Size: lineSize})
|
||
contextMu.Unlock()
|
||
continue
|
||
}
|
||
|
||
if totalSize+lineSize > shared.MaxContextBodySize {
|
||
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, p)
|
||
contextMu.Unlock()
|
||
continue
|
||
}
|
||
|
||
totalSize += lineSize
|
||
contextMu.Unlock()
|
||
|
||
keptPaths = append(keptPaths, p)
|
||
}
|
||
|
||
body := strings.Join(keptPaths, "\n")
|
||
|
||
name := inputFilePath
|
||
if name == "." {
|
||
name = "cwd"
|
||
}
|
||
if name == ".." {
|
||
name = "parent"
|
||
}
|
||
|
||
contextMu.Lock()
|
||
loadContextReq = append(loadContextReq, &shared.LoadContextParams{
|
||
ContextType: shared.ContextDirectoryTreeType,
|
||
Name: name,
|
||
Body: body,
|
||
FilePath: inputFilePath,
|
||
ForceSkipIgnore: params.ForceSkipIgnore,
|
||
AutoLoaded: params.AutoLoaded,
|
||
})
|
||
contextMu.Unlock()
|
||
|
||
errCh <- nil
|
||
}(inputFilePath)
|
||
}
|
||
|
||
} else {
|
||
flattenedPaths, err := ParseInputPaths(ParseInputPathsParams{
|
||
FileOrDirPaths: inputFilePaths,
|
||
BaseDir: baseDir,
|
||
ProjectPaths: paths,
|
||
LoadParams: params,
|
||
})
|
||
if err != nil {
|
||
onErr(fmt.Errorf("failed to parse input paths: %v", err))
|
||
}
|
||
|
||
if !params.ForceSkipIgnore {
|
||
var filteredPaths []string
|
||
for _, path := range flattenedPaths {
|
||
if _, ok := paths.ActivePaths[path]; ok {
|
||
filteredPaths = append(filteredPaths, path)
|
||
} else {
|
||
ignored, reason, err := fs.IsIgnored(paths, path, baseDir)
|
||
if err != nil {
|
||
onErr(fmt.Errorf("failed to check if %s is ignored: %v", path, err))
|
||
}
|
||
if ignored {
|
||
ignoredPaths[path] = reason
|
||
}
|
||
}
|
||
}
|
||
flattenedPaths = filteredPaths
|
||
|
||
}
|
||
|
||
var numPaths int
|
||
if params.DefsOnly {
|
||
filtered := []string{}
|
||
for _, path := range flattenedPaths {
|
||
if shared.HasFileMapSupport(path) {
|
||
numPaths++
|
||
|
||
if numPaths > shared.MaxContextMapPaths {
|
||
mapFilesSkippedAfterSizeLimit = append(mapFilesSkippedAfterSizeLimit, path)
|
||
continue
|
||
}
|
||
|
||
filtered = append(filtered, path)
|
||
}
|
||
}
|
||
flattenedPaths = filtered
|
||
} else if params.NamesOnly {
|
||
filtered := []string{}
|
||
for _, path := range flattenedPaths {
|
||
numPaths++
|
||
|
||
if numPaths > shared.MaxContextMapPaths {
|
||
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, path)
|
||
continue
|
||
}
|
||
|
||
filtered = append(filtered, path)
|
||
}
|
||
flattenedPaths = filtered
|
||
} else {
|
||
filtered := []string{}
|
||
for _, path := range flattenedPaths {
|
||
numPaths++
|
||
|
||
if numPaths > shared.MaxContextCount {
|
||
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, path)
|
||
continue
|
||
}
|
||
|
||
filtered = append(filtered, path)
|
||
}
|
||
flattenedPaths = filtered
|
||
}
|
||
|
||
inputFilePaths = flattenedPaths
|
||
|
||
for _, path := range flattenedPaths {
|
||
var mapInputPath string
|
||
if params.DefsOnly {
|
||
for _, inputPath := range toLoadMapPaths {
|
||
absPath, err := filepath.Abs(path)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
absInputPath, err := filepath.Abs(inputPath)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
if absPath == absInputPath ||
|
||
strings.HasPrefix(absPath+string(os.PathSeparator), absInputPath+string(os.PathSeparator)) {
|
||
mapInputPath = inputPath
|
||
break
|
||
}
|
||
}
|
||
|
||
if mapInputPath == "" {
|
||
continue
|
||
}
|
||
|
||
mapInputPathsForPaths[path] = mapInputPath
|
||
}
|
||
|
||
var contextType shared.ContextType
|
||
isImage := shared.IsImageFile(path)
|
||
if isImage {
|
||
contextType = shared.ContextImageType
|
||
} else if params.DefsOnly {
|
||
contextType = shared.ContextMapType
|
||
} else {
|
||
contextType = shared.ContextFileType
|
||
}
|
||
|
||
if !params.DefsOnly {
|
||
composite := strings.Join([]string{string(contextType), path}, "|")
|
||
if existsByComposite[composite] != nil {
|
||
alreadyLoadedByComposite[composite] = existsByComposite[composite]
|
||
continue
|
||
}
|
||
}
|
||
|
||
numRoutines++
|
||
|
||
go func(path string) {
|
||
sem <- struct{}{}
|
||
defer func() { <-sem }()
|
||
|
||
var size int64
|
||
|
||
fileInfo, err := os.Stat(path)
|
||
if err != nil {
|
||
errCh <- fmt.Errorf("failed to get file info for %s: %v", path, err)
|
||
return
|
||
}
|
||
size = fileInfo.Size()
|
||
|
||
if !params.DefsOnly && size > shared.MaxContextBodySize {
|
||
contextMu.Lock()
|
||
filesSkippedTooLarge = append(filesSkippedTooLarge, filePathWithSize{Path: path, Size: size})
|
||
contextMu.Unlock()
|
||
errCh <- nil
|
||
return
|
||
}
|
||
|
||
if !params.DefsOnly {
|
||
contextMu.Lock()
|
||
if totalSize+size > shared.MaxContextBodySize {
|
||
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, path)
|
||
contextMu.Unlock()
|
||
errCh <- nil
|
||
return
|
||
}
|
||
totalSize += size
|
||
contextMu.Unlock()
|
||
}
|
||
|
||
if params.DefsOnly {
|
||
res, err := getMapFileDetails(path, size, totalSize)
|
||
if err != nil {
|
||
errCh <- fmt.Errorf("failed to get map file details for %s: %v", path, err)
|
||
return
|
||
}
|
||
|
||
contextMu.Lock()
|
||
defer contextMu.Unlock()
|
||
|
||
if currentMapInputBatch.NumFiles()+1 > shared.ContextMapMaxBatchSize || currentMapInputBatch.TotalSize()+size > shared.ContextMapMaxBatchBytes {
|
||
currentMapInputBatch = shared.FileMapInputs{}
|
||
mapInputBatches = append(mapInputBatches, currentMapInputBatch)
|
||
}
|
||
|
||
currentMapInputBatch[path] = res.mapContent
|
||
mapSize += res.size
|
||
mapInputShas[path] = res.shaVal
|
||
mapInputTokens[path] = res.tokens
|
||
mapInputSizes[path] = res.size
|
||
|
||
if len(res.mapFilesTruncatedTooLarge) > 0 {
|
||
mapFilesTruncatedTooLarge = append(mapFilesTruncatedTooLarge, res.mapFilesTruncatedTooLarge...)
|
||
}
|
||
|
||
if len(res.mapFilesSkippedAfterSizeLimit) > 0 {
|
||
mapFilesSkippedAfterSizeLimit = append(mapFilesSkippedAfterSizeLimit, res.mapFilesSkippedAfterSizeLimit...)
|
||
}
|
||
|
||
} else if isImage {
|
||
fileContent, err := os.ReadFile(path)
|
||
if err != nil {
|
||
errCh <- fmt.Errorf("failed to read the file %s: %v", path, err)
|
||
return
|
||
}
|
||
|
||
contextMu.Lock()
|
||
defer contextMu.Unlock()
|
||
|
||
loadContextReq = append(loadContextReq, &shared.LoadContextParams{
|
||
ContextType: shared.ContextImageType,
|
||
Name: path,
|
||
Body: base64.StdEncoding.EncodeToString(fileContent),
|
||
FilePath: path,
|
||
ImageDetail: params.ImageDetail,
|
||
AutoLoaded: params.AutoLoaded,
|
||
})
|
||
} else {
|
||
fileContent, err := os.ReadFile(path)
|
||
if err != nil {
|
||
errCh <- fmt.Errorf("failed to read the file %s: %v", path, err)
|
||
return
|
||
}
|
||
fileContent = shared.NormalizeEOL(fileContent)
|
||
|
||
contextMu.Lock()
|
||
defer contextMu.Unlock()
|
||
|
||
loadContextReq = append(loadContextReq, &shared.LoadContextParams{
|
||
ContextType: shared.ContextFileType,
|
||
Name: path,
|
||
Body: string(fileContent),
|
||
FilePath: path,
|
||
AutoLoaded: params.AutoLoaded,
|
||
})
|
||
}
|
||
|
||
errCh <- nil
|
||
}(path)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if len(inputUrls) > 0 {
|
||
for _, u := range inputUrls {
|
||
composite := strings.Join([]string{string(shared.ContextURLType), u}, "|")
|
||
if existsByComposite[composite] != nil {
|
||
alreadyLoadedByComposite[composite] = existsByComposite[composite]
|
||
continue
|
||
}
|
||
|
||
numRoutines++
|
||
go func(u string) {
|
||
sem <- struct{}{}
|
||
defer func() { <-sem }()
|
||
|
||
body, err := url.FetchURLContent(u)
|
||
if err != nil {
|
||
errCh <- fmt.Errorf("failed to fetch content from URL %s: %v", u, err)
|
||
return
|
||
}
|
||
|
||
name := url.SanitizeURL(u)
|
||
// show the first 20 characters, then ellipsis then the last 20 characters of 'name'
|
||
if len(name) > 40 {
|
||
name = name[:20] + "⋯" + name[len(name)-20:]
|
||
}
|
||
|
||
// Check the size of the URL body, just like a file:
|
||
size := int64(len(body))
|
||
|
||
contextMu.Lock()
|
||
defer contextMu.Unlock()
|
||
|
||
if size > shared.MaxContextBodySize {
|
||
filesSkippedTooLarge = append(filesSkippedTooLarge, filePathWithSize{Path: u, Size: size})
|
||
errCh <- nil
|
||
return
|
||
}
|
||
if totalSize+size > shared.MaxContextBodySize {
|
||
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, u)
|
||
errCh <- nil
|
||
return
|
||
}
|
||
totalSize += size
|
||
|
||
loadContextReq = append(loadContextReq, &shared.LoadContextParams{
|
||
ContextType: shared.ContextURLType,
|
||
Name: name,
|
||
Body: body,
|
||
Url: u,
|
||
AutoLoaded: params.AutoLoaded,
|
||
})
|
||
|
||
errCh <- nil
|
||
}(u)
|
||
}
|
||
}
|
||
|
||
for i := 0; i < numRoutines; i++ {
|
||
err := <-errCh
|
||
if err != nil {
|
||
onErr(err)
|
||
}
|
||
}
|
||
|
||
if params.DefsOnly {
|
||
allMapBodies, err := processMapBatches(mapInputBatches)
|
||
if err != nil {
|
||
onErr(fmt.Errorf("failed to process map batches: %v", err))
|
||
}
|
||
|
||
for _, inputPath := range toLoadMapPaths {
|
||
var name string
|
||
if inputPath == "." {
|
||
name = "cwd"
|
||
} else if inputPath == ".." {
|
||
name = "parent"
|
||
} else {
|
||
name = inputPath
|
||
}
|
||
|
||
pathBodies := shared.FileMapBodies{}
|
||
pathShas := map[string]string{}
|
||
pathTokens := map[string]int{}
|
||
pathSizes := map[string]int64{}
|
||
for path, body := range allMapBodies {
|
||
mapInputPath := mapInputPathsForPaths[path]
|
||
if mapInputPath == inputPath {
|
||
pathBodies[path] = body
|
||
pathShas[path] = mapInputShas[path]
|
||
pathTokens[path] = mapInputTokens[path]
|
||
pathSizes[path] = mapInputSizes[path]
|
||
}
|
||
}
|
||
|
||
// load the map even if it's empty (no paths)
|
||
// it needs to exist so it can be updated later
|
||
loadContextReq = append(loadContextReq, &shared.LoadContextParams{
|
||
ContextType: shared.ContextMapType,
|
||
Name: name,
|
||
MapBodies: pathBodies,
|
||
InputShas: pathShas,
|
||
InputTokens: pathTokens,
|
||
InputSizes: pathSizes,
|
||
FilePath: inputPath,
|
||
AutoLoaded: params.AutoLoaded,
|
||
})
|
||
|
||
}
|
||
}
|
||
|
||
filesToLoad := map[string]string{}
|
||
for _, context := range loadContextReq {
|
||
if context.ContextType == shared.ContextFileType {
|
||
filesToLoad[context.FilePath] = context.Body
|
||
}
|
||
}
|
||
|
||
hasConflicts, err := checkContextConflicts(filesToLoad)
|
||
|
||
if err != nil {
|
||
onErr(fmt.Errorf("failed to check context conflicts: %v", err))
|
||
}
|
||
|
||
if len(loadContextReq)+len(cachedMapPaths) == 0 {
|
||
term.StopSpinner()
|
||
fmt.Println("🤷♂️ No context loaded")
|
||
|
||
didOutputReason := false
|
||
if len(alreadyLoadedByComposite) > 0 {
|
||
printAlreadyLoadedMsg(alreadyLoadedByComposite)
|
||
didOutputReason = true
|
||
}
|
||
if len(ignoredPaths) > 0 && !params.SkipIgnoreWarning {
|
||
printIgnoredMsg()
|
||
didOutputReason = true
|
||
}
|
||
|
||
if !didOutputReason {
|
||
fmt.Println()
|
||
fmt.Printf("Use %s to load a file or URL:", color.New(color.BgCyan, color.FgHiWhite).Sprint(" plandex load [file-path|url] "))
|
||
fmt.Println()
|
||
fmt.Println("plandex load file.c file.h")
|
||
fmt.Println("plandex load https://github.com/some-org/some-repo/README.md")
|
||
|
||
fmt.Println()
|
||
fmt.Printf("%s with the --recursive/-r flag:\n", color.New(color.Bold, term.ColorHiCyan).Sprint("Load a whole directory"))
|
||
fmt.Println("plandex load app/src -r")
|
||
|
||
fmt.Println()
|
||
fmt.Printf("%s with the --tree flag:\n", color.New(color.Bold, term.ColorHiCyan).Sprint("Load a directory layout (file names only)"))
|
||
|
||
fmt.Println()
|
||
fmt.Printf("%s file paths are relative to the current directory\n", color.New(color.Bold, term.ColorHiYellow).Sprint("Note:"))
|
||
|
||
fmt.Println()
|
||
fmt.Printf("%s with the -n flag:\n", color.New(color.Bold, term.ColorHiCyan).Sprint("Load a note"))
|
||
fmt.Println("plandex load -n 'Some note here'")
|
||
|
||
fmt.Println()
|
||
fmt.Printf("%s from any command:\n", color.New(color.Bold, term.ColorHiCyan).Sprint("Pipe data in"))
|
||
fmt.Println("npm test | plandex load")
|
||
}
|
||
|
||
os.Exit(0)
|
||
}
|
||
|
||
var res *shared.LoadContextResponse
|
||
if cachedMapLoadRes != nil {
|
||
res = cachedMapLoadRes
|
||
} else {
|
||
res, apiErr = api.Client.LoadContext(CurrentPlanId, CurrentBranch, loadContextReq)
|
||
if apiErr != nil {
|
||
onErr(fmt.Errorf("failed to load context: %v", apiErr.Msg))
|
||
}
|
||
}
|
||
|
||
term.StopSpinner()
|
||
|
||
if hasConflicts {
|
||
term.StartSpinner("🏗️ Starting build...")
|
||
_, err := buildPlanInlineFn(false, nil)
|
||
if err != nil {
|
||
onErr(fmt.Errorf("failed to build plan: %v", err))
|
||
}
|
||
fmt.Println()
|
||
}
|
||
|
||
fmt.Println("✅ " + res.Msg)
|
||
|
||
if len(alreadyLoadedByComposite) > 0 {
|
||
printAlreadyLoadedMsg(alreadyLoadedByComposite)
|
||
}
|
||
|
||
if len(ignoredPaths) > 0 && !params.SkipIgnoreWarning {
|
||
printIgnoredMsg()
|
||
}
|
||
|
||
if len(filesSkippedTooLarge) > 0 || len(filesSkippedAfterSizeLimit) > 0 ||
|
||
len(mapFilesTruncatedTooLarge) > 0 || len(mapFilesSkippedAfterSizeLimit) > 0 {
|
||
printSkippedFilesMsg(filesSkippedTooLarge, filesSkippedAfterSizeLimit,
|
||
mapFilesTruncatedTooLarge, mapFilesSkippedAfterSizeLimit)
|
||
}
|
||
}
|
||
|
||
func printAlreadyLoadedMsg(alreadyLoadedByComposite map[string]*shared.Context) {
|
||
fmt.Println()
|
||
pronoun := "they're"
|
||
if len(alreadyLoadedByComposite) == 1 {
|
||
pronoun = "it's"
|
||
}
|
||
fmt.Printf("🙅♂️ Skipped because %s already in context:\n", pronoun)
|
||
for _, context := range alreadyLoadedByComposite {
|
||
_, icon := context.TypeAndIcon()
|
||
|
||
fmt.Printf(" • %s %s\n", icon, context.Name)
|
||
}
|
||
}
|
||
|
||
func printIgnoredMsg() {
|
||
fmt.Println()
|
||
fmt.Println("ℹ️ " + color.New(color.FgWhite).Sprint("Due to .gitignore or .plandexignore, some paths weren't loaded.\nUse --force / -f to load ignored paths."))
|
||
}
|