返回 DeepSeek-Reasonix
checkpoints_http.go
根目录 / internal / serve / checkpoints_http.go
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
90 lines GO