-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.go
More file actions
249 lines (222 loc) · 8.72 KB
/
Copy pathapp.go
File metadata and controls
249 lines (222 loc) · 8.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
package main
import (
"context"
"fmt"
"strings"
"mdiff/internal/config"
"mdiff/internal/diffparse"
"mdiff/internal/gitdiff"
"mdiff/internal/llm"
)
// App struct
type App struct {
ctx context.Context
}
// NewApp creates a new App application struct
func NewApp() *App {
return &App{}
}
// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
}
// Greet returns a greeting for the given name
func (a *App) Greet(name string) string {
return fmt.Sprintf("Hello %s, It's show time!", name)
}
// ChangedFiles lists every changed file in the working tree of the repo
// rooted at the current working directory.
func (a *App) ChangedFiles() ([]gitdiff.FileChange, error) {
return gitdiff.ChangedFiles()
}
// FileDiff returns the parsed unified diff for path in the working tree.
func (a *App) FileDiff(path string) (diffparse.FileDiff, error) {
raw, err := gitdiff.FileDiff(path)
if err != nil {
return diffparse.FileDiff{}, err
}
return diffparse.Parse(raw)
}
// RecentCommits returns up to count commits from the repository history,
// newest first, skipping the first skip commits for paging.
func (a *App) RecentCommits(skip, count int) ([]gitdiff.Commit, error) {
return gitdiff.RecentCommits(skip, count)
}
// CommitFiles lists every path changed by the given commit.
func (a *App) CommitFiles(hash string) ([]gitdiff.FileChange, error) {
return gitdiff.CommitFiles(hash)
}
// CommitFileDiff returns the parsed unified diff for path as changed by
// the given commit.
func (a *App) CommitFileDiff(hash, path string) (diffparse.FileDiff, error) {
raw, err := gitdiff.CommitFileDiff(hash, path)
if err != nil {
return diffparse.FileDiff{}, err
}
return diffparse.Parse(raw)
}
// GetConfig returns the current non-secret application settings.
func (a *App) GetConfig() (config.Config, error) {
return config.Load()
}
// SaveConfig writes the non-secret application settings.
func (a *App) SaveConfig(cfg config.Config) error {
return config.Save(cfg)
}
// HasAPIKey reports whether an API key is currently available, without ever
// returning the key itself to the frontend.
func (a *App) HasAPIKey() (bool, error) {
key, _ := config.GetAPIKey()
return key != "", nil
}
// APIKeyUsedFallback reports whether the currently available API key (if
// any) was read from the environment variable fallback rather than the OS
// keyring, so the frontend can surface that distinctly from "no key set".
func (a *App) APIKeyUsedFallback() (bool, error) {
_, usedFallback := config.GetAPIKey()
return usedFallback, nil
}
// SetAPIKey stores a new API key in the OS keyring.
func (a *App) SetAPIKey(key string) error {
return config.SetAPIKey(key)
}
// ExplainFile fetches the raw diff for path and asks the configured LLM to
// explain what changed and why, returning its prose response. It returns a
// clear error, rather than an empty string, if the base URL, model, or API
// key is not configured.
func (a *App) ExplainFile(path string) (string, error) {
diff, err := gitdiff.FileDiff(path)
if err != nil {
return "", err
}
return explainDiff(diff)
}
// ExplainCommitFile fetches the raw diff for path as changed by the given
// commit and asks the configured LLM to explain what changed and why,
// returning its prose response. It returns a clear error, rather than an
// empty string, if the base URL, model, or API key is not configured.
func (a *App) ExplainCommitFile(hash, path string) (string, error) {
diff, err := gitdiff.CommitFileDiff(hash, path)
if err != nil {
return "", err
}
return explainDiff(diff)
}
// ExplainAllChanges gathers the diff for every changed file in the working
// tree, concatenates them into one combined diff blob, and asks the
// configured LLM for a single holistic explanation of the whole changeset
// (its overall intent, not a per-file recap). It returns a clear error if
// the working tree has no changes, rather than calling the LLM with nothing.
func (a *App) ExplainAllChanges() (string, error) {
files, err := gitdiff.ChangedFiles()
if err != nil {
return "", err
}
if len(files) == 0 {
return "", fmt.Errorf("nothing to explain: the working tree has no changes")
}
combined, err := combineDiffs(files, gitdiff.FileDiff)
if err != nil {
return "", err
}
return runExplain(allChangesPrompt(combined))
}
// ExplainAllCommitChanges gathers the diff for every file changed by the
// given commit, concatenates them into one combined diff blob, and asks the
// configured LLM for a single holistic explanation of the whole changeset
// (its overall intent, not a per-file recap). It returns a clear error if
// the commit changed no files, rather than calling the LLM with nothing.
func (a *App) ExplainAllCommitChanges(hash string) (string, error) {
files, err := gitdiff.CommitFiles(hash)
if err != nil {
return "", err
}
if len(files) == 0 {
return "", fmt.Errorf("nothing to explain: commit %s changed no files", hash)
}
combined, err := combineDiffs(files, func(path string) (string, error) {
return gitdiff.CommitFileDiff(hash, path)
})
if err != nil {
return "", err
}
return runExplain(allChangesPrompt(combined))
}
// combineDiffs fetches the diff for each of files via diffFn and
// concatenates them into one blob, with a "--- path ---" separator line
// preceding each file's diff, suitable as input to a holistic
// all-changes explanation prompt.
func combineDiffs(files []gitdiff.FileChange, diffFn func(path string) (string, error)) (string, error) {
var b strings.Builder
for i, f := range files {
diff, err := diffFn(f.Path)
if err != nil {
return "", err
}
if i > 0 {
b.WriteString("\n")
}
fmt.Fprintf(&b, "--- %s ---\n", f.Path)
b.WriteString(diff)
}
return b.String(), nil
}
// explainDiff asks the configured LLM to explain the given raw diff and
// returns its prose response. It returns a clear error, rather than an empty
// string, if the base URL, model, or API key is not configured.
func explainDiff(diff string) (string, error) {
return runExplain(filePrompt(diff))
}
// filePrompt builds the terse per-file explanation prompt for a single diff.
func filePrompt(diff string) string {
return fmt.Sprintf(
"Explain what changed in the following diff and why, in plain prose. "+
"Be as terse as the change deserves: a trivial or mechanical change "+
"(e.g. one ignore-list entry, a formatting fix, a comment tweak) gets "+
"one short sentence, not a full breakdown. Only go longer when the "+
"change is genuinely substantial. No preamble, no restating the diff "+
"line by line, no generic best-practice commentary, light markdown "+
"like **bold** or inline `code` is fine if it genuinely helps but "+
"don't force headings, bullet lists, or heavy structure onto a "+
"short or simple explanation, no summary at the end.\n\n%s",
diff,
)
}
// allChangesPrompt builds the holistic-synthesis prompt for a combined diff
// spanning every file in a changeset, asking for one conceptual explanation
// of the changeset's overall intent rather than a per-file recap.
func allChangesPrompt(diff string) string {
return fmt.Sprintf(
"The following is a combined diff covering every changed file in one "+
"changeset, each file's diff preceded by a \"--- path ---\" separator "+
"line. Explain the overall intent and theme of this changeset as a "+
"whole, synthesizing in your own words what it accomplishes "+
"conceptually. Do not describe what each file does one by one, and do "+
"not just concatenate per-file summaries. Be as terse as the change "+
"deserves: one coherent paragraph or two is correct for a small or "+
"mechanical changeset; only go longer if the change is genuinely large "+
"or touches many unrelated concerns. If the changeset genuinely mixes "+
"multiple unrelated concerns, briefly flag that fact, but do not "+
"invent a multi-concern narrative for a changeset that is actually one "+
"coherent thing. No preamble, no restating diff lines, light markdown "+
"like **bold** or inline `code` is fine if it genuinely helps but "+
"don't force headings, bullet lists, or per-file breakdown onto a "+
"short or simple explanation, no summary at the end.\n\n%s",
diff,
)
}
// runExplain sends prompt to the configured LLM and returns its prose
// response. It returns a clear error, rather than an empty string, if the
// base URL, model, or API key is not configured.
func runExplain(prompt string) (string, error) {
cfg, err := config.Load()
if err != nil {
return "", err
}
apiKey, _ := config.GetAPIKey()
if cfg.BaseURL == "" || cfg.Model == "" || apiKey == "" {
return "", fmt.Errorf("mdiff is not configured for Explain: set the base URL, model, and API key first")
}
return llm.Explain(cfg.BaseURL, cfg.Model, apiKey, prompt)
}