Files
2026-07-13 13:33:09 +08:00

161 lines
4.1 KiB
Go

package lib
import (
"context"
"encoding/base64"
"fmt"
"log"
"os"
"plandex-cli/api"
"plandex-cli/types"
shared "plandex-shared"
"sync"
"github.com/sashabaranov/go-openai"
)
func AutoLoadContextFiles(ctx context.Context, files []string) (string, error) {
contexts, err := api.Client.ListContext(CurrentPlanId, CurrentBranch)
if err != nil {
return "", fmt.Errorf("failed to get contexts: %v", err)
}
var totalSize int64
totalContexts := len(contexts)
for _, context := range contexts {
totalSize += context.BodySize
}
loadContextReqsByIndex := make(map[int]*shared.LoadContextParams)
filesSkippedTooLarge := []filePathWithSize{}
filesSkippedAfterSizeLimit := []string{}
var mu sync.Mutex
errCh := make(chan error, len(files))
for i, path := range files {
totalContexts++
if totalContexts > shared.MaxContextCount {
log.Println("Skipping file", path, "because it would exceed the max context count", totalContexts)
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, path)
errCh <- nil
continue
}
go func(index int, path string) {
fileInfo, err := os.Stat(path)
if err != nil {
errCh <- fmt.Errorf("failed to get file info for %s: %v", path, err)
return
}
if fileInfo.IsDir() {
log.Println("Skipping directory", path)
errCh <- nil // skip directories
return
}
size := fileInfo.Size()
mu.Lock()
if size > shared.MaxContextBodySize {
log.Println("Skipping file", path, "because it's too large", size)
filesSkippedTooLarge = append(filesSkippedTooLarge, filePathWithSize{Path: path, Size: size})
mu.Unlock()
errCh <- nil
return
}
if totalSize+size > shared.MaxTotalContextSize {
log.Println("Skipping file", path, "because it would exceed the max context body size", totalSize+size)
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, path)
mu.Unlock()
errCh <- nil
return
}
totalSize += size
mu.Unlock()
b, err := os.ReadFile(path)
if err != nil {
errCh <- fmt.Errorf("failed to read file %s: %v", path, err)
return
}
var contextType shared.ContextType
isImage := shared.IsImageFile(path)
if isImage {
contextType = shared.ContextImageType
} else {
contextType = shared.ContextFileType
}
var imageDetail openai.ImageURLDetail
if isImage {
imageDetail = openai.ImageURLDetailHigh
}
var body string
if isImage {
body = base64.StdEncoding.EncodeToString(b)
} else {
body = string(shared.NormalizeEOL(b))
}
mu.Lock()
loadContextReqsByIndex[index] = &shared.LoadContextParams{
ContextType: contextType,
FilePath: path,
Name: path,
Body: body,
AutoLoaded: true,
ImageDetail: imageDetail,
}
mu.Unlock()
errCh <- nil
}(i, path)
}
for range files {
if e := <-errCh; e != nil {
return "", fmt.Errorf("failed to load context: %v", e)
}
}
// Convert map back to ordered slice
loadContextReqs := make(shared.LoadContextRequest, 0, len(loadContextReqsByIndex))
for i := 0; i < len(files); i++ {
if req := loadContextReqsByIndex[i]; req != nil {
loadContextReqs = append(loadContextReqs, req)
}
}
// even if there are no files to load, we still need to hit the API endpoint because the stream is waiting on a channel for the autoload to finish
res, apiErr := api.Client.AutoLoadContext(ctx, CurrentPlanId, CurrentBranch, loadContextReqs)
if apiErr != nil {
return "", fmt.Errorf("failed to load context: %v", apiErr.Msg)
}
if res.MaxTokensExceeded {
overage := res.TotalTokens - res.MaxTokens
return "", fmt.Errorf("update would add %d 🪙 and exceed token limit (%d) by %d 🪙", res.TokensAdded, res.MaxTokens, overage)
}
msg := res.Msg
// Print skip info if any
if len(filesSkippedTooLarge) > 0 || len(filesSkippedAfterSizeLimit) > 0 {
msg += "\n\n" + getSkippedFilesMsg(filesSkippedTooLarge, filesSkippedAfterSizeLimit, nil, nil)
}
return msg, nil
}
func MustLoadAutoContextMap() {
MustLoadContext([]string{"."}, &types.LoadContextParams{
DefsOnly: true,
SkipIgnoreWarning: true,
AutoLoaded: true,
})
}