package lib import ( "crypto/sha256" "encoding/base64" "encoding/hex" "fmt" "os" "plandex-cli/api" "plandex-cli/fs" "plandex-cli/term" "plandex-cli/types" "plandex-cli/url" "strconv" "strings" "sync" shared "plandex-shared" "github.com/fatih/color" "github.com/olekukonko/tablewriter" ) func CheckOutdatedContextWithOutput(quiet, autoConfirm bool, maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (contextOutdated, updated bool, err error) { if !quiet { term.StartSpinner("🔬 Checking context...") } var contexts []*shared.Context if maybeContexts != nil { contexts = maybeContexts } else { res, err := api.Client.ListContext(CurrentPlanId, CurrentBranch) if err != nil { term.StopSpinner() return false, false, fmt.Errorf("failed to list context: %s", err) } contexts = res } outdatedRes, err := CheckOutdatedContext(contexts, projectPaths) if err != nil { term.StopSpinner() return false, false, fmt.Errorf("failed to check outdated context: %s", err) } if !quiet { term.StopSpinner() } if len(outdatedRes.UpdatedContexts) == 0 && len(outdatedRes.RemovedContexts) == 0 { if !quiet { fmt.Println("✅ Context is up to date") } return false, false, nil } if len(outdatedRes.UpdatedContexts) > 0 { types := []string{} if outdatedRes.NumFiles > 0 { lbl := "file" if outdatedRes.NumFiles > 1 { lbl = "files" } lbl = strconv.Itoa(outdatedRes.NumFiles) + " " + lbl types = append(types, lbl) } if outdatedRes.NumUrls > 0 { lbl := "url" if outdatedRes.NumUrls > 1 { lbl = "urls" } lbl = strconv.Itoa(outdatedRes.NumUrls) + " " + lbl types = append(types, lbl) } if outdatedRes.NumTrees > 0 { lbl := "directory tree" if outdatedRes.NumTrees > 1 { lbl = "directory trees" } lbl = strconv.Itoa(outdatedRes.NumTrees) + " " + lbl types = append(types, lbl) } if outdatedRes.NumMaps > 0 { lbl := "map" if outdatedRes.NumMaps > 1 { lbl = "maps" } lbl = strconv.Itoa(outdatedRes.NumMaps) + " " + lbl types = append(types, lbl) } var msg string if len(types) <= 2 { msg += strings.Join(types, " and ") } else { for i, add := range types { if i == len(types)-1 { msg += ", and " + add } else { msg += ", " + add } } } phrase := "have been" if len(outdatedRes.UpdatedContexts) == 1 { phrase = "has been" } if !quiet { term.StopSpinner() color.New(term.ColorHiCyan, color.Bold).Printf("%s in context %s modified 👇\n\n", msg, phrase) tableString := tableForContextOutdated(outdatedRes.UpdatedContexts, outdatedRes.TokenDiffsById) fmt.Println(tableString) } } if len(outdatedRes.RemovedContexts) > 0 { types := []string{} if outdatedRes.NumFilesRemoved > 0 { lbl := "file" if outdatedRes.NumFilesRemoved > 1 { lbl = "files" } lbl = strconv.Itoa(outdatedRes.NumFilesRemoved) + " " + lbl types = append(types, lbl) } if outdatedRes.NumTreesRemoved > 0 { lbl := "directory tree" if outdatedRes.NumTreesRemoved > 1 { lbl = "directory trees" } lbl = strconv.Itoa(outdatedRes.NumTreesRemoved) + " " + lbl types = append(types, lbl) } var msg string if len(types) <= 2 { msg += strings.Join(types, " and ") } else { for i, add := range types { if i == len(types)-1 { msg += ", and " + add } else { msg += ", " + add } } } phrase := "have been" if len(outdatedRes.RemovedContexts) == 1 { phrase = "has been" } if !quiet { term.StopSpinner() color.New(term.ColorHiCyan, color.Bold).Printf("%s in context %s removed 👇\n\n", msg, phrase) tableString := tableForContextOutdated(outdatedRes.RemovedContexts, outdatedRes.TokenDiffsById) fmt.Println(tableString) } } confirmed := autoConfirm if !autoConfirm { confirmed, err = term.ConfirmYesNo("Update context now?") if err != nil { term.OutputErrorAndExit("failed to get user input: %s", err) } } if confirmed { reqFn := outdatedRes.ReqFn if reqFn == nil { return false, false, fmt.Errorf("no update request function provided") } _, err = UpdateContextWithOutput(UpdateContextParams{ Contexts: contexts, OutdatedRes: *outdatedRes, ReqFn: reqFn, }) if err != nil { return false, false, fmt.Errorf("error updating context: %v", err) } return true, true, nil } else { return true, false, nil } } type UpdateContextParams struct { Contexts []*shared.Context OutdatedRes types.ContextOutdatedResult ReqFn func() (map[string]*shared.UpdateContextParams, error) } type UpdateContextResult struct { HasConflicts bool Msg string } func UpdateContextWithOutput(params UpdateContextParams) (UpdateContextResult, error) { term.StartSpinner("🔄 Updating context...") updateRes, err := UpdateContext(params) if err != nil { return UpdateContextResult{}, err } term.StopSpinner() fmt.Println("✅ " + updateRes.Msg) return updateRes, nil } func UpdateContext(params UpdateContextParams) (UpdateContextResult, error) { var err error reqFn := params.ReqFn if reqFn == nil { return UpdateContextResult{}, fmt.Errorf("no update request function provided") } req, err := reqFn() if err != nil { return UpdateContextResult{}, fmt.Errorf("error getting update request: %v", err) } var hasConflicts bool var msg string contextsById := map[string]*shared.Context{} for _, context := range params.Contexts { contextsById[context.Id] = context } deleteIds := map[string]bool{} for _, context := range params.OutdatedRes.RemovedContexts { deleteIds[context.Id] = true } filesToLoad := map[string]string{} for id := range req { context := contextsById[id] if context.ContextType == shared.ContextFileType { filesToLoad[context.FilePath] = context.Body } } for id := range deleteIds { context := contextsById[id] if context.ContextType == shared.ContextFileType { filesToLoad[context.FilePath] = "" } } hasConflicts, err = checkContextConflicts(filesToLoad) if err != nil { return UpdateContextResult{}, fmt.Errorf("failed to check context conflicts: %v", err) } if len(req) > 0 { res, apiErr := api.Client.UpdateContext(CurrentPlanId, CurrentBranch, req) if apiErr != nil { return UpdateContextResult{}, fmt.Errorf("failed to update context: %v", apiErr) } msg = res.Msg } if len(deleteIds) > 0 { res, apiErr := api.Client.DeleteContext(CurrentPlanId, CurrentBranch, shared.DeleteContextRequest{ Ids: deleteIds, }) if apiErr != nil { return UpdateContextResult{}, fmt.Errorf("failed to delete contexts: %v", apiErr) } msg += " " + res.Msg } return UpdateContextResult{ HasConflicts: hasConflicts, Msg: strings.TrimSpace(msg), }, nil } // CheckOutdatedContext is where we replicate your partial-read logic for map files // so that large map files or newly added map files do not read more than MaxContextMapSingleInputSize func CheckOutdatedContext(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (*types.ContextOutdatedResult, error) { return checkOutdatedAndMaybeUpdateContext(false, maybeContexts, projectPaths) } type mapState struct { removedMapPaths []string mapInputShas map[string]string mapInputTokens map[string]int mapInputSizes map[string]int64 totalMapSize int64 currentMapInputBatch shared.FileMapInputs mapInputBatches []shared.FileMapInputs } func checkOutdatedAndMaybeUpdateContext(doUpdate bool, maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (*types.ContextOutdatedResult, error) { var contexts []*shared.Context if maybeContexts == nil { contextsRes, apiErr := api.Client.ListContext(CurrentPlanId, CurrentBranch) if apiErr != nil { return nil, fmt.Errorf("error retrieving context: %v", apiErr) } contexts = contextsRes } else { contexts = maybeContexts } totalTokens := 0 for _, c := range contexts { totalTokens += c.NumTokens } var errs []error reqFns := map[string]func() (*shared.UpdateContextParams, error){} var updatedContexts []*shared.Context var tokenDiffsById = map[string]int{} var numFiles int var numUrls int var numTrees int var numMaps int var numFilesRemoved int var numTreesRemoved int var mu sync.Mutex var wg sync.WaitGroup contextsById := make(map[string]*shared.Context) deleteIds := make(map[string]bool) paths := projectPaths // We track skipped items for final warnings var filesSkippedTooLarge []filePathWithSize var filesSkippedAfterSizeLimit []string var mapFilesTruncatedTooLarge []filePathWithSize var mapFilesSkippedAfterSizeLimit []string mapFilesTruncatedSet := map[string]bool{} mapFilesSkippedAfterSizeLimitSet := map[string]bool{} mapFileInfoByPath := map[string]os.FileInfo{} mapFileRemovedByPath := map[string]bool{} var totalSize int64 var totalBodySize int64 var totalContextCount int sem := make(chan struct{}, ContextMapMaxClientConcurrency) for _, c := range contexts { contextsById[c.Id] = c } for _, context := range contexts { switch context.ContextType { case shared.ContextFileType: wg.Add(1) go func(ctx *shared.Context) { defer wg.Done() sem <- struct{}{} defer func() { <-sem }() if _, err := os.Stat(ctx.FilePath); os.IsNotExist(err) { mu.Lock() defer mu.Unlock() deleteIds[ctx.Id] = true numFilesRemoved++ tokenDiffsById[ctx.Id] = -ctx.NumTokens return } fileContent, err := os.ReadFile(ctx.FilePath) if err != nil { mu.Lock() defer mu.Unlock() errs = append(errs, fmt.Errorf("failed to read the file %s: %v", ctx.FilePath, err)) return } fileContent = shared.NormalizeEOL(fileContent) fileInfo, err := os.Stat(ctx.FilePath) if err != nil { mu.Lock() defer mu.Unlock() errs = append(errs, fmt.Errorf("failed to get file info for %s: %v", ctx.FilePath, err)) return } size := fileInfo.Size() // Individual skip checks if size > shared.MaxContextBodySize { mu.Lock() defer mu.Unlock() filesSkippedTooLarge = append(filesSkippedTooLarge, filePathWithSize{Path: ctx.FilePath, Size: size}) return } if totalSize+size > shared.MaxContextBodySize { mu.Lock() defer mu.Unlock() filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.FilePath) return } // Compare new sha hash := sha256.Sum256(fileContent) sha := hex.EncodeToString(hash[:]) if sha != ctx.Sha { if totalContextCount >= shared.MaxContextCount { mu.Lock() defer mu.Unlock() filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.FilePath) return } oldBodySize := int64(len(ctx.Body)) newBodySize := int64(len(fileContent)) if totalBodySize+(newBodySize-oldBodySize) > shared.MaxContextBodySize { mu.Lock() defer mu.Unlock() filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.FilePath) return } // Accept mu.Lock() totalSize += size totalContextCount++ totalBodySize += (newBodySize - oldBodySize) mu.Unlock() var numTokens int if shared.IsImageFile(ctx.FilePath) { tokens, err := shared.GetImageTokens(base64.StdEncoding.EncodeToString(fileContent), ctx.ImageDetail) if err != nil { mu.Lock() defer mu.Unlock() errs = append(errs, fmt.Errorf("failed to get image tokens for %s: %v", ctx.FilePath, err)) return } numTokens = tokens } else { numTokens = shared.GetNumTokensEstimate(string(fileContent)) } mu.Lock() defer mu.Unlock() tokenDiffsById[ctx.Id] = numTokens - ctx.NumTokens numFiles++ updatedContexts = append(updatedContexts, ctx) reqFns[ctx.Id] = func() (*shared.UpdateContextParams, error) { return &shared.UpdateContextParams{ Body: string(fileContent), }, nil } } }(context) case shared.ContextDirectoryTreeType: wg.Add(1) go func(ctx *shared.Context) { defer wg.Done() sem <- struct{}{} defer func() { <-sem }() if _, err := os.Stat(ctx.FilePath); os.IsNotExist(err) { mu.Lock() deleteIds[ctx.Id] = true numTreesRemoved++ tokenDiffsById[ctx.Id] = -ctx.NumTokens mu.Unlock() return } baseDir := fs.GetBaseDirForFilePaths([]string{ctx.FilePath}) flattenedPaths, err := ParseInputPaths(ParseInputPathsParams{ FileOrDirPaths: []string{ctx.FilePath}, BaseDir: baseDir, ProjectPaths: paths, LoadParams: &types.LoadContextParams{ NamesOnly: true, ForceSkipIgnore: ctx.ForceSkipIgnore, }, }) if err != nil { mu.Lock() defer mu.Unlock() errs = append(errs, fmt.Errorf("failed to get directory tree %s: %v", ctx.FilePath, err)) return } if !ctx.ForceSkipIgnore && paths != nil { var filtered []string for _, p := range flattenedPaths { if _, ok := paths.ActivePaths[p]; ok { filtered = append(filtered, p) } } flattenedPaths = filtered } // Partial skipping for sub-paths var kept []string mu.Lock() for _, p := range flattenedPaths { lineSize := int64(len(p)) // If line is individually too large, skip if lineSize > shared.MaxContextBodySize { filesSkippedTooLarge = append(filesSkippedTooLarge, filePathWithSize{Path: p, Size: lineSize}) continue } if totalSize+lineSize > shared.MaxContextBodySize { filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, p) continue } // Accept totalSize += lineSize kept = append(kept, p) } mu.Unlock() body := strings.Join(kept, "\n") newHash := sha256.Sum256([]byte(body)) newSha := hex.EncodeToString(newHash[:]) if newSha != ctx.Sha { if totalContextCount >= shared.MaxContextCount { mu.Lock() defer mu.Unlock() filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.FilePath) return } oldBodySize := int64(len(ctx.Body)) newBodySize := int64(len(body)) if totalBodySize+(newBodySize-oldBodySize) > shared.MaxContextBodySize { mu.Lock() defer mu.Unlock() filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.FilePath) return } numTokens := shared.GetNumTokensEstimate(body) mu.Lock() defer mu.Unlock() totalContextCount++ totalBodySize += (newBodySize - oldBodySize) tokenDiffsById[ctx.Id] = numTokens - ctx.NumTokens numTrees++ updatedContexts = append(updatedContexts, ctx) reqFns[ctx.Id] = func() (*shared.UpdateContextParams, error) { return &shared.UpdateContextParams{ Body: body, }, nil } } }(context) case shared.ContextMapType: // Instead of reading all files in the same goroutine, // we now spawn one goroutine per map-file to mirror the loading logic concurrency. wg.Add(1) go func(ctx *shared.Context) { defer wg.Done() // We collect paths from the existing map var mapPaths []string for path := range ctx.MapShas { mapPaths = append(mapPaths, path) } // Next, see if there are newly added files baseDir := fs.GetBaseDirForFilePaths([]string{ctx.FilePath}) flattenedPaths, err := ParseInputPaths(ParseInputPathsParams{ FileOrDirPaths: []string{ctx.FilePath}, BaseDir: baseDir, ProjectPaths: projectPaths, LoadParams: &types.LoadContextParams{Recursive: true}, }) if err != nil { mu.Lock() defer mu.Unlock() errs = append(errs, fmt.Errorf("failed to get the directory tree %s: %v", ctx.FilePath, err)) return } var filtered []string if projectPaths != nil { for _, p := range flattenedPaths { if _, ok := projectPaths.ActivePaths[p]; ok { filtered = append(filtered, p) } } flattenedPaths = filtered } // If a path was not already in the map, it's newly added for _, p := range flattenedPaths { if _, ok := ctx.MapShas[p]; !ok { mapPaths = append(mapPaths, p) } } totalMapPaths := len(mapPaths) currentMapInputBatch := shared.FileMapInputs{} state := mapState{ removedMapPaths: []string{}, mapInputShas: map[string]string{}, mapInputTokens: map[string]int{}, mapInputSizes: map[string]int64{}, totalMapSize: 0, currentMapInputBatch: currentMapInputBatch, mapInputBatches: []shared.FileMapInputs{currentMapInputBatch}, } innerExistenceErrCh := make(chan error, len(mapPaths)) // Existence: check each path in its own goroutine: for _, path := range mapPaths { go func(path string) { sem <- struct{}{} defer func() { <-sem }() var removed bool var hasFileInfo bool mu.Lock() if _, ok := mapFileInfoByPath[path]; ok { hasFileInfo = true } else if _, ok := mapFileRemovedByPath[path]; ok { removed = true } mu.Unlock() if !(hasFileInfo || removed) { fileInfo, err := os.Stat(path) if err != nil { if os.IsNotExist(err) { removed = true } else { innerExistenceErrCh <- fmt.Errorf("failed to stat map file %s: %v", path, err) return } } mu.Lock() prevTokens := ctx.MapTokens[path] prevSize := ctx.MapSizes[path] if removed { mapFileRemovedByPath[path] = true totalMapPaths-- if _, existed := ctx.MapShas[path]; existed { state.removedMapPaths = append(state.removedMapPaths, path) tokenDiffsById[ctx.Id] -= prevTokens state.totalMapSize -= prevSize } } else { mapFileInfoByPath[path] = fileInfo } mu.Unlock() } innerExistenceErrCh <- nil }(path) } for range mapPaths { err := <-innerExistenceErrCh if err != nil { mu.Lock() defer mu.Unlock() errs = append(errs, err) return } } // Updates: check each path in its own goroutine: innerUpdatesErrCh := make(chan error, len(mapPaths)) for _, path := range mapPaths { go func(path string) { sem <- struct{}{} defer func() { <-sem }() var removed bool var fileInfo os.FileInfo var hasFileInfo bool mu.Lock() if _, ok := mapFileInfoByPath[path]; ok { fileInfo = mapFileInfoByPath[path] hasFileInfo = true } else if _, ok := mapFileRemovedByPath[path]; ok { removed = true } mu.Unlock() if removed { innerUpdatesErrCh <- nil return } if !hasFileInfo { innerUpdatesErrCh <- fmt.Errorf("failed to get map file info for %s - should already be set", path) return } size := fileInfo.Size() var totalMapSize int64 mu.Lock() prevTokens := ctx.MapTokens[path] prevSize := ctx.MapSizes[path] prevSha := ctx.MapShas[path] totalMapSize = state.totalMapSize mu.Unlock() res, err := getMapFileDetails(path, size, totalMapSize) if err != nil { innerUpdatesErrCh <- fmt.Errorf("failed to get map file details for %s: %v", path, err) return } if res.shaVal == prevSha { // no change innerUpdatesErrCh <- nil return } mu.Lock() totalMapPaths++ if totalMapPaths > shared.MaxContextMapPaths { if _, ok := mapFilesSkippedAfterSizeLimitSet[path]; !ok { mapFilesSkippedAfterSizeLimitSet[path] = true mapFilesSkippedAfterSizeLimit = append(mapFilesSkippedAfterSizeLimit, path) } mu.Unlock() innerUpdatesErrCh <- nil return } defer mu.Unlock() if state.currentMapInputBatch.NumFiles()+1 > shared.ContextMapMaxBatchSize || state.totalMapSize+res.size > shared.ContextMapMaxBatchBytes { state.currentMapInputBatch = shared.FileMapInputs{} state.mapInputBatches = append(state.mapInputBatches, state.currentMapInputBatch) } sizeChange := int64(res.size) - prevSize state.totalMapSize += sizeChange tokenDiffsById[ctx.Id] += (res.tokens - prevTokens) state.mapInputShas[path] = res.shaVal state.mapInputTokens[path] = res.tokens state.currentMapInputBatch[path] = res.mapContent state.mapInputSizes[path] = res.size if len(res.mapFilesTruncatedTooLarge) > 0 { for _, file := range res.mapFilesTruncatedTooLarge { if _, ok := mapFilesTruncatedSet[file.Path]; !ok { mapFilesTruncatedSet[file.Path] = true mapFilesTruncatedTooLarge = append(mapFilesTruncatedTooLarge, file) } } } if len(res.mapFilesSkippedAfterSizeLimit) > 0 { for _, file := range res.mapFilesSkippedAfterSizeLimit { if _, ok := mapFilesSkippedAfterSizeLimitSet[file]; !ok { mapFilesSkippedAfterSizeLimitSet[file] = true mapFilesSkippedAfterSizeLimit = append(mapFilesSkippedAfterSizeLimit, file) } } } innerUpdatesErrCh <- nil }(path) } for range mapPaths { err := <-innerUpdatesErrCh if err != nil { mu.Lock() defer mu.Unlock() errs = append(errs, err) return } } hasAnyUpdate := len(state.removedMapPaths) > 0 || len(state.mapInputShas) > 0 if hasAnyUpdate { mu.Lock() defer mu.Unlock() updatedContexts = append(updatedContexts, ctx) numMaps++ reqFns[ctx.Id] = func() (*shared.UpdateContextParams, error) { updatedMapBodies, err := processMapBatches(state.mapInputBatches) if err != nil { return nil, fmt.Errorf("failed to process map batches: %v", err) } return &shared.UpdateContextParams{ MapBodies: updatedMapBodies, InputShas: state.mapInputShas, InputTokens: state.mapInputTokens, InputSizes: state.mapInputSizes, RemovedMapPaths: state.removedMapPaths, }, nil } } }(context) case shared.ContextURLType: wg.Add(1) go func(ctx *shared.Context) { defer wg.Done() sem <- struct{}{} defer func() { <-sem }() body, err := url.FetchURLContent(ctx.Url) if err != nil { mu.Lock() defer mu.Unlock() errs = append(errs, fmt.Errorf("failed to fetch the URL %s: %v", ctx.Url, err)) return } size := int64(len(body)) if size > shared.MaxContextBodySize { mu.Lock() defer mu.Unlock() filesSkippedTooLarge = append(filesSkippedTooLarge, filePathWithSize{Path: ctx.Url, Size: size}) return } if totalSize+size > shared.MaxContextBodySize { mu.Lock() defer mu.Unlock() filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.Url) return } hash := sha256.Sum256([]byte(body)) newSha := hex.EncodeToString(hash[:]) if newSha != ctx.Sha { if totalContextCount >= shared.MaxContextCount { mu.Lock() defer mu.Unlock() filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.Url) return } oldBodySize := int64(len(ctx.Body)) newBodySize := size if totalBodySize+(newBodySize-oldBodySize) > shared.MaxContextBodySize { mu.Lock() defer mu.Unlock() filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.Url) return } numTokens := shared.GetNumTokensEstimate(string(body)) mu.Lock() defer mu.Unlock() totalSize += size totalContextCount++ totalBodySize += (newBodySize - oldBodySize) tokenDiffsById[ctx.Id] = numTokens - ctx.NumTokens numUrls++ updatedContexts = append(updatedContexts, ctx) reqFns[ctx.Id] = func() (*shared.UpdateContextParams, error) { return &shared.UpdateContextParams{ Body: string(body), }, nil } } }(context) } } wg.Wait() if len(errs) > 0 { return nil, fmt.Errorf("failed to check context outdated: %v", errs) } // Identify contexts to remove var removedContexts []*shared.Context for id := range deleteIds { removedContexts = append(removedContexts, contextsById[id]) } // If nothing changed if len(reqFns) == 0 && len(removedContexts) == 0 { return &types.ContextOutdatedResult{ Msg: "Context is up to date", }, nil } reqFn := func() (map[string]*shared.UpdateContextParams, error) { req := map[string]*shared.UpdateContextParams{} var mu sync.Mutex errCh := make(chan error, len(reqFns)) for id, fn := range reqFns { go func(id string, fn func() (*shared.UpdateContextParams, error)) { res, err := fn() if err != nil { errCh <- err return } mu.Lock() req[id] = res mu.Unlock() errCh <- nil }(id, fn) } for i := 0; i < len(reqFns); i++ { err := <-errCh if err != nil { return nil, err } } return req, nil } // Build final result outdatedRes := types.ContextOutdatedResult{ UpdatedContexts: updatedContexts, RemovedContexts: removedContexts, TokenDiffsById: tokenDiffsById, NumFiles: numFiles, NumUrls: numUrls, NumTrees: numTrees, NumMaps: numMaps, NumFilesRemoved: numFilesRemoved, NumTreesRemoved: numTreesRemoved, ReqFn: reqFn, } var hasConflicts bool var msg string if doUpdate { res, err := UpdateContext(UpdateContextParams{ Contexts: contexts, OutdatedRes: outdatedRes, ReqFn: reqFn, }) if err != nil { return nil, fmt.Errorf("failed to update context: %v", err) } hasConflicts = res.HasConflicts msg = res.Msg outdatedRes.Msg = msg } else { var tokensDiff int for _, diff := range tokenDiffsById { tokensDiff += diff } newTotal := totalTokens + tokensDiff outdatedRes.Msg = shared.SummaryForUpdateContext(shared.SummaryForUpdateContextParams{ NumFiles: numFiles, NumTrees: numTrees, NumUrls: numUrls, NumMaps: numMaps, TokensDiff: tokensDiff, TotalTokens: newTotal, }) } if hasConflicts { term.StartSpinner("🏗️ Starting build...") _, err := buildPlanInlineFn(false, nil) term.StopSpinner() fmt.Println() if err != nil { return nil, fmt.Errorf("failed to build plan: %v", err) } } // Warn about any items skipped if len(filesSkippedTooLarge) > 0 || len(filesSkippedAfterSizeLimit) > 0 || len(mapFilesTruncatedTooLarge) > 0 || len(mapFilesSkippedAfterSizeLimit) > 0 { printSkippedFilesMsg(filesSkippedTooLarge, filesSkippedAfterSizeLimit, mapFilesTruncatedTooLarge, mapFilesSkippedAfterSizeLimit) } return &outdatedRes, nil } func tableForContextOutdated(updatedContexts []*shared.Context, tokenDiffsById map[string]int) string { if len(updatedContexts) == 0 { return "" } tableString := &strings.Builder{} table := tablewriter.NewWriter(tableString) table.SetHeader([]string{"Name", "Type", "🪙"}) table.SetAutoWrapText(false) for _, ctx := range updatedContexts { t, icon := ctx.TypeAndIcon() diff := tokenDiffsById[ctx.Id] diffStr := "+" + strconv.Itoa(diff) tableColor := tablewriter.FgHiGreenColor if diff < 0 { diffStr = strconv.Itoa(diff) tableColor = tablewriter.FgHiRedColor } row := []string{ " " + icon + " " + ctx.Name, t, diffStr, } table.Rich(row, []tablewriter.Colors{ {tableColor, tablewriter.Bold}, {tableColor}, {tableColor}, }) } table.Render() return tableString.String() }