| 1 | package serve |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | "sort" |
| 6 | |
| 7 | "reasonix/internal/checkpoint" |
| 8 | ) |
| 9 | |
| 10 | type serveCheckpointMeta struct { |
| 11 | Turn int `json:"turn"` |
| 12 | Prompt string `json:"prompt"` |
| 13 | Files []string `json:"files"` |
| 14 | FileCount int `json:"fileCount"` |
| 15 | FilesTruncated bool `json:"filesTruncated,omitempty"` |
| 16 | TurnFileCount int `json:"turnFileCount"` |
| 17 | Time int64 `json:"time"` |
| 18 | CanCode bool `json:"canCode"` |
| 19 | CanConversation bool `json:"canConversation"` |
| 20 | Coverage string `json:"coverage,omitempty"` |
| 21 | CoverageGaps []string `json:"coverageGaps,omitempty"` |
| 22 | ExpiredFilePayload bool `json:"expiredFilePayload,omitempty"` |
| 23 | ActiveWriters int `json:"activeWriters,omitempty"` |
| 24 | Legacy bool `json:"legacy,omitempty"` |
| 25 | CanUndoFiles bool `json:"canUndoFiles,omitempty"` |
| 26 | DisabledReason string `json:"disabledReason,omitempty"` |
| 27 | } |
| 28 | |
| 29 | const serveCheckpointFilePreviewLimit = 60 |
| 30 | |
| 31 | func serveCheckpointMetas(raw []checkpoint.Meta, hasBoundary func(int) bool) []serveCheckpointMeta { |
| 32 | out := make([]serveCheckpointMeta, 0, len(raw)) |
| 33 | for _, item := range raw { |
| 34 | gaps := make([]string, 0, len(item.CoverageGaps)) |
| 35 | for _, gap := range item.CoverageGaps { |
| 36 | if gap.Detail != "" { |
| 37 | gaps = append(gaps, gap.Reason+": "+gap.Detail) |
| 38 | } else { |
| 39 | gaps = append(gaps, gap.Reason) |
| 40 | } |
| 41 | } |
| 42 | out = append(out, serveCheckpointMeta{ |
| 43 | Turn: item.Turn, Prompt: item.Prompt, Files: append([]string{}, item.Paths...), |
| 44 | TurnFileCount: len(item.Paths), Time: item.Time.UnixMilli(), |
| 45 | CanCode: len(item.Paths) > 0 && item.CanUndoFiles, CanConversation: hasBoundary(item.Turn), |
| 46 | Coverage: string(item.Coverage), CoverageGaps: gaps, ExpiredFilePayload: item.ExpiredFilePayload, |
| 47 | ActiveWriters: len(item.ActiveWriters), Legacy: item.Legacy, CanUndoFiles: item.CanUndoFiles, |
| 48 | DisabledReason: item.DisabledReason, |
| 49 | }) |
| 50 | } |
| 51 | |
| 52 | hasCodeAfter, canCodeAfter := false, true |
| 53 | codeFiles, preview := make(map[string]bool, len(raw)*2), []string{} |
| 54 | //nolint:modernize // the body writes through the index while walking backwards. |
| 55 | for i := len(out) - 1; i >= 0; i-- { |
| 56 | if len(out[i].Files) > 0 { |
| 57 | hasCodeAfter = true |
| 58 | if !out[i].CanUndoFiles { |
| 59 | canCodeAfter = false |
| 60 | } |
| 61 | } |
| 62 | for _, path := range out[i].Files { |
| 63 | if codeFiles[path] { |
| 64 | continue |
| 65 | } |
| 66 | codeFiles[path] = true |
| 67 | idx := sort.SearchStrings(preview, path) |
| 68 | if len(preview) < serveCheckpointFilePreviewLimit { |
| 69 | preview = append(preview, "") |
| 70 | copy(preview[idx+1:], preview[idx:]) |
| 71 | preview[idx] = path |
| 72 | } else if idx < serveCheckpointFilePreviewLimit { |
| 73 | copy(preview[idx+1:], preview[idx:serveCheckpointFilePreviewLimit-1]) |
| 74 | preview[idx] = path |
| 75 | } |
| 76 | } |
| 77 | out[i].CanCode, out[i].FileCount = hasCodeAfter && canCodeAfter, len(codeFiles) |
| 78 | out[i].Files = append([]string{}, preview...) |
| 79 | out[i].FilesTruncated = out[i].FileCount > len(out[i].Files) |
| 80 | } |
| 81 | return out |
| 82 | } |
| 83 | |
| 84 | // checkpoints returns complete rewind capabilities and path previews. File |
| 85 | // contents never cross the Serve boundary. |
| 86 | func (s *Server) checkpoints(w http.ResponseWriter, _ *http.Request) { |
| 87 | ctrl := s.ctl() |
| 88 | writeJSON(w, serveCheckpointMetas(ctrl.Checkpoints(), ctrl.CheckpointHasBoundary)) |
| 89 | } |
| 90 |