saeeol 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/package.json +14 -14
  2. package/src/cli/cmd/tui/component/dialog/dialog-agent.tsx +32 -0
  3. package/src/cli/cmd/tui/component/dialog/dialog-command.tsx +190 -0
  4. package/src/cli/cmd/tui/component/dialog/dialog-console-org.tsx +103 -0
  5. package/src/cli/cmd/tui/component/dialog/dialog-go-upsell.tsx +159 -0
  6. package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +86 -0
  7. package/src/cli/cmd/tui/component/dialog/dialog-model.tsx +238 -0
  8. package/src/cli/cmd/tui/component/dialog/dialog-provider.tsx +343 -0
  9. package/src/cli/cmd/tui/component/dialog/dialog-session-delete-failed.tsx +103 -0
  10. package/src/cli/cmd/tui/component/dialog/dialog-session-list.tsx +301 -0
  11. package/src/cli/cmd/tui/component/dialog/dialog-session-rename.tsx +35 -0
  12. package/src/cli/cmd/tui/component/dialog/dialog-skill.tsx +37 -0
  13. package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +87 -0
  14. package/src/cli/cmd/tui/component/dialog/dialog-status.tsx +190 -0
  15. package/src/cli/cmd/tui/component/dialog/dialog-tag.tsx +44 -0
  16. package/src/cli/cmd/tui/component/dialog/dialog-theme-list.tsx +50 -0
  17. package/src/cli/cmd/tui/component/dialog/dialog-variant.tsx +39 -0
  18. package/src/cli/cmd/tui/component/dialog/dialog-workspace-create.tsx +200 -0
  19. package/src/cli/cmd/tui/component/dialog/dialog-workspace-unavailable.tsx +81 -0
  20. package/src/cli/cmd/tui/component/dialog-agent.tsx +1 -32
  21. package/src/cli/cmd/tui/component/dialog-command.tsx +1 -190
  22. package/src/cli/cmd/tui/component/dialog-console-org.tsx +1 -103
  23. package/src/cli/cmd/tui/component/dialog-go-upsell.tsx +1 -159
  24. package/src/cli/cmd/tui/component/dialog-mcp.tsx +1 -86
  25. package/src/cli/cmd/tui/component/dialog-model.tsx +1 -238
  26. package/src/cli/cmd/tui/component/dialog-provider.tsx +1 -343
  27. package/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx +1 -103
  28. package/src/cli/cmd/tui/component/dialog-session-list.tsx +1 -301
  29. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +1 -35
  30. package/src/cli/cmd/tui/component/dialog-skill.tsx +1 -37
  31. package/src/cli/cmd/tui/component/dialog-stash.tsx +1 -87
  32. package/src/cli/cmd/tui/component/dialog-status.tsx +1 -190
  33. package/src/cli/cmd/tui/component/dialog-tag.tsx +1 -44
  34. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +1 -50
  35. package/src/cli/cmd/tui/component/dialog-variant.tsx +1 -39
  36. package/src/cli/cmd/tui/component/dialog-workspace-create.tsx +1 -200
  37. package/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +1 -81
  38. package/src/session/compaction-helpers.ts +1 -169
  39. package/src/session/compaction.ts +1 -712
  40. package/src/session/core/compaction/compaction-helpers.ts +169 -0
  41. package/src/session/core/compaction/compaction.ts +712 -0
  42. package/src/session/core/compaction/overflow.ts +28 -0
  43. package/src/session/core/instruction.ts +234 -0
  44. package/src/session/core/llm.ts +504 -0
  45. package/src/session/core/network.ts +392 -0
  46. package/src/session/core/processor.ts +731 -0
  47. package/src/session/core/projectors.ts +139 -0
  48. package/src/session/core/resolve-tools.ts +241 -0
  49. package/src/session/core/retry.ts +149 -0
  50. package/src/session/core/revert.ts +173 -0
  51. package/src/session/core/run-state.ts +110 -0
  52. package/src/session/core/schema.ts +35 -0
  53. package/src/session/core/session-types.ts +160 -0
  54. package/src/session/core/session.sql.ts +124 -0
  55. package/src/session/core/session.ts +948 -0
  56. package/src/session/core/shell-exec.ts +205 -0
  57. package/src/session/core/status.ts +100 -0
  58. package/src/session/core/subtask.ts +268 -0
  59. package/src/session/core/summary.ts +173 -0
  60. package/src/session/core/system.ts +114 -0
  61. package/src/session/core/todo.ts +86 -0
  62. package/src/session/core/user-part.ts +293 -0
  63. package/src/session/instruction.ts +1 -234
  64. package/src/session/llm.ts +1 -504
  65. package/src/session/message/message-errors.ts +83 -0
  66. package/src/session/message/message-parts.ts +89 -0
  67. package/src/session/message/message-query.ts +107 -0
  68. package/src/session/message/message-transform.ts +156 -0
  69. package/src/session/message/message-types.ts +68 -0
  70. package/src/session/message/message-v2.ts +73 -0
  71. package/src/session/message/message.ts +192 -0
  72. package/src/session/message-errors.ts +1 -83
  73. package/src/session/message-parts.ts +1 -89
  74. package/src/session/message-query.ts +1 -107
  75. package/src/session/message-transform.ts +1 -156
  76. package/src/session/message-types.ts +1 -68
  77. package/src/session/message-v2.ts +1 -73
  78. package/src/session/message.ts +1 -192
  79. package/src/session/network.ts +1 -392
  80. package/src/session/overflow.ts +1 -28
  81. package/src/session/processor.ts +1 -731
  82. package/src/session/projectors.ts +2 -139
  83. package/src/session/prompt/prompt-command.ts +93 -0
  84. package/src/session/prompt/prompt-loop.ts +299 -0
  85. package/src/session/prompt/prompt-model.ts +44 -0
  86. package/src/session/prompt/prompt-reminders.ts +120 -0
  87. package/src/session/prompt/prompt-resolve.ts +42 -0
  88. package/src/session/prompt/prompt-schemas.ts +128 -0
  89. package/src/session/prompt/prompt-title.ts +55 -0
  90. package/src/session/prompt/prompt-types.ts +47 -0
  91. package/src/session/prompt/prompt-user-msg.ts +80 -0
  92. package/src/session/prompt/prompt.ts +211 -0
  93. package/src/session/prompt-command.ts +1 -93
  94. package/src/session/prompt-loop.ts +1 -299
  95. package/src/session/prompt-model.ts +1 -44
  96. package/src/session/prompt-reminders.ts +1 -120
  97. package/src/session/prompt-resolve.ts +1 -42
  98. package/src/session/prompt-schemas.ts +1 -128
  99. package/src/session/prompt-title.ts +1 -55
  100. package/src/session/prompt-types.ts +1 -47
  101. package/src/session/prompt-user-msg.ts +1 -80
  102. package/src/session/prompt.ts +1 -211
  103. package/src/session/resolve-tools.ts +1 -241
  104. package/src/session/retry.ts +1 -149
  105. package/src/session/revert.ts +1 -173
  106. package/src/session/run-state.ts +1 -110
  107. package/src/session/schema.ts +1 -35
  108. package/src/session/session-types.ts +1 -160
  109. package/src/session/session.sql.ts +1 -124
  110. package/src/session/session.ts +1 -948
  111. package/src/session/shell-exec.ts +1 -205
  112. package/src/session/status.ts +1 -100
  113. package/src/session/subtask.ts +1 -268
  114. package/src/session/summary.ts +1 -173
  115. package/src/session/system.ts +1 -114
  116. package/src/session/todo.ts +1 -86
  117. package/src/session/user-part.ts +1 -293
  118. package/src/tool/apply_patch.ts +1 -334
  119. package/src/tool/bash.ts +1 -656
  120. package/src/tool/core/external-directory.ts +55 -0
  121. package/src/tool/core/invalid.ts +21 -0
  122. package/src/tool/core/recall.ts +164 -0
  123. package/src/tool/core/recall.txt +12 -0
  124. package/src/tool/core/schema.ts +16 -0
  125. package/src/tool/core/tool.ts +162 -0
  126. package/src/tool/core/truncate.ts +160 -0
  127. package/src/tool/core/truncation-dir.ts +4 -0
  128. package/src/tool/diagnostics.ts +1 -20
  129. package/src/tool/edit-replacers.ts +1 -288
  130. package/src/tool/edit-utils.ts +1 -86
  131. package/src/tool/edit.ts +1 -262
  132. package/src/tool/external-directory.ts +1 -55
  133. package/src/tool/file/apply_patch.ts +334 -0
  134. package/src/tool/file/apply_patch.txt +33 -0
  135. package/src/tool/file/bash.ts +656 -0
  136. package/src/tool/file/bash.txt +119 -0
  137. package/src/tool/file/edit-replacers.ts +288 -0
  138. package/src/tool/file/edit-utils.ts +86 -0
  139. package/src/tool/file/edit.ts +262 -0
  140. package/src/tool/file/edit.txt +10 -0
  141. package/src/tool/file/read.ts +389 -0
  142. package/src/tool/file/read.txt +14 -0
  143. package/src/tool/file/write.ts +114 -0
  144. package/src/tool/file/write.txt +8 -0
  145. package/src/tool/glob.ts +1 -115
  146. package/src/tool/grep.ts +1 -151
  147. package/src/tool/integration/diagnostics.ts +20 -0
  148. package/src/tool/integration/lsp.ts +113 -0
  149. package/src/tool/integration/lsp.txt +24 -0
  150. package/src/tool/integration/mcp-exa.ts +73 -0
  151. package/src/tool/integration/package.ts +168 -0
  152. package/src/tool/integration/registry.ts +375 -0
  153. package/src/tool/invalid.ts +1 -21
  154. package/src/tool/lsp.ts +1 -113
  155. package/src/tool/mcp-exa.ts +1 -73
  156. package/src/tool/package.ts +1 -168
  157. package/src/tool/plan.ts +1 -30
  158. package/src/tool/question.ts +1 -52
  159. package/src/tool/read.ts +1 -389
  160. package/src/tool/recall.ts +1 -164
  161. package/src/tool/registry.ts +1 -375
  162. package/src/tool/schema.ts +1 -16
  163. package/src/tool/search/glob.ts +115 -0
  164. package/src/tool/search/glob.txt +6 -0
  165. package/src/tool/search/grep.ts +151 -0
  166. package/src/tool/search/grep.txt +8 -0
  167. package/src/tool/search/warpgrep.ts +107 -0
  168. package/src/tool/search/warpgrep.txt +10 -0
  169. package/src/tool/search/webfetch.ts +202 -0
  170. package/src/tool/search/webfetch.txt +13 -0
  171. package/src/tool/search/websearch.ts +71 -0
  172. package/src/tool/search/websearch.txt +14 -0
  173. package/src/tool/skill.ts +1 -91
  174. package/src/tool/task.ts +1 -197
  175. package/src/tool/todo.ts +1 -62
  176. package/src/tool/tool.ts +1 -162
  177. package/src/tool/truncate.ts +1 -160
  178. package/src/tool/truncation-dir.ts +1 -4
  179. package/src/tool/warpgrep.ts +1 -107
  180. package/src/tool/webfetch.ts +1 -202
  181. package/src/tool/websearch.ts +1 -71
  182. package/src/tool/workflow/plan-enter.txt +14 -0
  183. package/src/tool/workflow/plan-exit.txt +13 -0
  184. package/src/tool/workflow/plan.ts +30 -0
  185. package/src/tool/workflow/question.ts +52 -0
  186. package/src/tool/workflow/question.txt +11 -0
  187. package/src/tool/workflow/skill.ts +91 -0
  188. package/src/tool/workflow/skill.txt +5 -0
  189. package/src/tool/workflow/task.ts +197 -0
  190. package/src/tool/workflow/task.txt +57 -0
  191. package/src/tool/workflow/todo.ts +62 -0
  192. package/src/tool/workflow/todowrite.txt +167 -0
  193. package/src/tool/write.ts +1 -114
@@ -1,288 +1 @@
1
- import { levenshtein, SINGLE_CANDIDATE_SIMILARITY_THRESHOLD, MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD } from "./edit-utils"
2
-
3
- export type Replacer = (content: string, find: string) => Generator<string, void, unknown>
4
-
5
- export const SimpleReplacer: Replacer = function* (_content, find) {
6
- yield find
7
- }
8
-
9
- export const LineTrimmedReplacer: Replacer = function* (content, find) {
10
- const originalLines = content.split("\n")
11
- const searchLines = find.split("\n")
12
- if (searchLines[searchLines.length - 1] === "") {
13
- searchLines.pop()
14
- }
15
- for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
16
- let matches = true
17
- for (let j = 0; j < searchLines.length; j++) {
18
- const originalTrimmed = originalLines[i + j].trim()
19
- const searchTrimmed = searchLines[j].trim()
20
- if (originalTrimmed !== searchTrimmed) {
21
- matches = false
22
- break
23
- }
24
- }
25
- if (matches) {
26
- let matchStartIndex = 0
27
- for (let k = 0; k < i; k++) {
28
- matchStartIndex += originalLines[k].length + 1
29
- }
30
- let matchEndIndex = matchStartIndex
31
- for (let k = 0; k < searchLines.length; k++) {
32
- matchEndIndex += originalLines[i + k].length
33
- if (k < searchLines.length - 1) {
34
- matchEndIndex += 1
35
- }
36
- }
37
- yield content.substring(matchStartIndex, matchEndIndex)
38
- }
39
- }
40
- }
41
-
42
- export const BlockAnchorReplacer: Replacer = function* (content, find) {
43
- const originalLines = content.split("\n")
44
- const searchLines = find.split("\n")
45
- if (searchLines.length < 3) return
46
- if (searchLines[searchLines.length - 1] === "") {
47
- searchLines.pop()
48
- }
49
- const firstLineSearch = searchLines[0].trim()
50
- const lastLineSearch = searchLines[searchLines.length - 1].trim()
51
- const searchBlockSize = searchLines.length
52
- const candidates: Array<{ startLine: number; endLine: number }> = []
53
- for (let i = 0; i < originalLines.length; i++) {
54
- if (originalLines[i].trim() !== firstLineSearch) continue
55
- for (let j = i + 2; j < originalLines.length; j++) {
56
- if (originalLines[j].trim() === lastLineSearch) {
57
- candidates.push({ startLine: i, endLine: j })
58
- break
59
- }
60
- }
61
- }
62
- if (candidates.length === 0) return
63
- if (candidates.length === 1) {
64
- const { startLine, endLine } = candidates[0]
65
- const actualBlockSize = endLine - startLine + 1
66
- let similarity = 0
67
- const linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2)
68
- if (linesToCheck > 0) {
69
- for (let j = 1; j < searchBlockSize - 1 && j < actualBlockSize - 1; j++) {
70
- const originalLine = originalLines[startLine + j].trim()
71
- const searchLine = searchLines[j].trim()
72
- const maxLen = Math.max(originalLine.length, searchLine.length)
73
- if (maxLen === 0) continue
74
- const distance = levenshtein(originalLine, searchLine)
75
- similarity += (1 - distance / maxLen) / linesToCheck
76
- if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) break
77
- }
78
- } else {
79
- similarity = 1.0
80
- }
81
- if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) {
82
- let matchStartIndex = 0
83
- for (let k = 0; k < startLine; k++) {
84
- matchStartIndex += originalLines[k].length + 1
85
- }
86
- let matchEndIndex = matchStartIndex
87
- for (let k = startLine; k <= endLine; k++) {
88
- matchEndIndex += originalLines[k].length
89
- if (k < endLine) matchEndIndex += 1
90
- }
91
- yield content.substring(matchStartIndex, matchEndIndex)
92
- }
93
- return
94
- }
95
- let bestMatch: { startLine: number; endLine: number } | null = null
96
- let maxSimilarity = -1
97
- for (const candidate of candidates) {
98
- const { startLine, endLine } = candidate
99
- const actualBlockSize = endLine - startLine + 1
100
- let similarity = 0
101
- const linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2)
102
- if (linesToCheck > 0) {
103
- for (let j = 1; j < searchBlockSize - 1 && j < actualBlockSize - 1; j++) {
104
- const originalLine = originalLines[startLine + j].trim()
105
- const searchLine = searchLines[j].trim()
106
- const maxLen = Math.max(originalLine.length, searchLine.length)
107
- if (maxLen === 0) continue
108
- const distance = levenshtein(originalLine, searchLine)
109
- similarity += 1 - distance / maxLen
110
- }
111
- similarity /= linesToCheck
112
- } else {
113
- similarity = 1.0
114
- }
115
- if (similarity > maxSimilarity) {
116
- maxSimilarity = similarity
117
- bestMatch = candidate
118
- }
119
- }
120
- if (maxSimilarity >= MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD && bestMatch) {
121
- const { startLine, endLine } = bestMatch
122
- let matchStartIndex = 0
123
- for (let k = 0; k < startLine; k++) {
124
- matchStartIndex += originalLines[k].length + 1
125
- }
126
- let matchEndIndex = matchStartIndex
127
- for (let k = startLine; k <= endLine; k++) {
128
- matchEndIndex += originalLines[k].length
129
- if (k < endLine) matchEndIndex += 1
130
- }
131
- yield content.substring(matchStartIndex, matchEndIndex)
132
- }
133
- }
134
-
135
- export const WhitespaceNormalizedReplacer: Replacer = function* (content, find) {
136
- const normalizeWhitespace = (text: string) => text.replace(/\s+/g, " ").trim()
137
- const normalizedFind = normalizeWhitespace(find)
138
- const lines = content.split("\n")
139
- for (let i = 0; i < lines.length; i++) {
140
- const line = lines[i]
141
- if (normalizeWhitespace(line) === normalizedFind) {
142
- yield line
143
- } else {
144
- const normalizedLine = normalizeWhitespace(line)
145
- if (normalizedLine.includes(normalizedFind)) {
146
- const words = find.trim().split(/\s+/)
147
- if (words.length > 0) {
148
- const pattern = words.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+")
149
- try {
150
- const regex = new RegExp(pattern)
151
- const match = line.match(regex)
152
- if (match) yield match[0]
153
- } catch {
154
- // Invalid regex pattern, skip
155
- }
156
- }
157
- }
158
- }
159
- }
160
- const findLines = find.split("\n")
161
- if (findLines.length > 1) {
162
- for (let i = 0; i <= lines.length - findLines.length; i++) {
163
- const block = lines.slice(i, i + findLines.length)
164
- if (normalizeWhitespace(block.join("\n")) === normalizedFind) {
165
- yield block.join("\n")
166
- }
167
- }
168
- }
169
- }
170
-
171
- export const IndentationFlexibleReplacer: Replacer = function* (content, find) {
172
- const removeIndentation = (text: string) => {
173
- const lines = text.split("\n")
174
- const nonEmptyLines = lines.filter((line) => line.trim().length > 0)
175
- if (nonEmptyLines.length === 0) return text
176
- const minIndent = Math.min(
177
- ...nonEmptyLines.map((line) => {
178
- const match = line.match(/^(\s*)/)
179
- return match ? match[1].length : 0
180
- }),
181
- )
182
- return lines.map((line) => (line.trim().length === 0 ? line : line.slice(minIndent))).join("\n")
183
- }
184
- const normalizedFind = removeIndentation(find)
185
- const contentLines = content.split("\n")
186
- const findLines = find.split("\n")
187
- for (let i = 0; i <= contentLines.length - findLines.length; i++) {
188
- const block = contentLines.slice(i, i + findLines.length).join("\n")
189
- if (removeIndentation(block) === normalizedFind) {
190
- yield block
191
- }
192
- }
193
- }
194
-
195
- export const EscapeNormalizedReplacer: Replacer = function* (content, find) {
196
- const unescapeString = (str: string): string => {
197
- return str.replace(/\\(n|t|r|'|"|`|\\|\n|\$)/g, (_match, capturedChar: string) => {
198
- switch (capturedChar) {
199
- case "n": return "\n"
200
- case "t": return "\t"
201
- case "r": return "\r"
202
- case "'": return "'"
203
- case '"': return '"'
204
- case "`": return "`"
205
- case "\\": return "\\"
206
- case "\n": return "\n"
207
- case "$": return "$"
208
- default: return capturedChar
209
- }
210
- })
211
- }
212
- const unescapedFind = unescapeString(find)
213
- if (content.includes(unescapedFind)) {
214
- yield unescapedFind
215
- }
216
- const lines = content.split("\n")
217
- const findLines = unescapedFind.split("\n")
218
- for (let i = 0; i <= lines.length - findLines.length; i++) {
219
- const block = lines.slice(i, i + findLines.length).join("\n")
220
- const unescapedBlock = unescapeString(block)
221
- if (unescapedBlock === unescapedFind) {
222
- yield block
223
- }
224
- }
225
- }
226
-
227
- export const MultiOccurrenceReplacer: Replacer = function* (content, find) {
228
- let startIndex = 0
229
- while (true) {
230
- const index = content.indexOf(find, startIndex)
231
- if (index === -1) break
232
- yield find
233
- startIndex = index + find.length
234
- }
235
- }
236
-
237
- export const TrimmedBoundaryReplacer: Replacer = function* (content, find) {
238
- const trimmedFind = find.trim()
239
- if (trimmedFind === find) return
240
- if (content.includes(trimmedFind)) {
241
- yield trimmedFind
242
- }
243
- const lines = content.split("\n")
244
- const findLines = find.split("\n")
245
- for (let i = 0; i <= lines.length - findLines.length; i++) {
246
- const block = lines.slice(i, i + findLines.length).join("\n")
247
- if (block.trim() === trimmedFind) {
248
- yield block
249
- }
250
- }
251
- }
252
-
253
- export const ContextAwareReplacer: Replacer = function* (content, find) {
254
- const findLines = find.split("\n")
255
- if (findLines.length < 3) return
256
- if (findLines[findLines.length - 1] === "") {
257
- findLines.pop()
258
- }
259
- const contentLines = content.split("\n")
260
- const firstLine = findLines[0].trim()
261
- const lastLine = findLines[findLines.length - 1].trim()
262
- for (let i = 0; i < contentLines.length; i++) {
263
- if (contentLines[i].trim() !== firstLine) continue
264
- for (let j = i + 2; j < contentLines.length; j++) {
265
- if (contentLines[j].trim() === lastLine) {
266
- const blockLines = contentLines.slice(i, j + 1)
267
- const block = blockLines.join("\n")
268
- if (blockLines.length === findLines.length) {
269
- let matchingLines = 0
270
- let totalNonEmptyLines = 0
271
- for (let k = 1; k < blockLines.length - 1; k++) {
272
- const blockLine = blockLines[k].trim()
273
- const findLine = findLines[k].trim()
274
- if (blockLine.length > 0 || findLine.length > 0) {
275
- totalNonEmptyLines++
276
- if (blockLine === findLine) matchingLines++
277
- }
278
- }
279
- if (totalNonEmptyLines === 0 || matchingLines / totalNonEmptyLines >= 0.5) {
280
- yield block
281
- break
282
- }
283
- }
284
- break
285
- }
286
- }
287
- }
288
- }
1
+ export * from "./file/edit-replacers"
@@ -1,86 +1 @@
1
- import { createTwoFilesPatch, diffLines } from "diff"
2
- import { Snapshot } from "@/snapshot"
3
-
4
- export const SINGLE_CANDIDATE_SIMILARITY_THRESHOLD = 0.0
5
- export const MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD = 0.3
6
-
7
- export function levenshtein(a: string, b: string): number {
8
- if (a === "" || b === "") {
9
- return Math.max(a.length, b.length)
10
- }
11
- const matrix = Array.from({ length: a.length + 1 }, (_, i) =>
12
- Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)),
13
- )
14
- for (let i = 1; i <= a.length; i++) {
15
- for (let j = 1; j <= b.length; j++) {
16
- const cost = a[i - 1] === b[j - 1] ? 0 : 1
17
- matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost)
18
- }
19
- }
20
- return matrix[a.length][b.length]
21
- }
22
-
23
- const MAX_DIFF_CONTENT = 500_000
24
- export function buildFileDiff(file: string, before: string, after: string): Snapshot.FileDiff {
25
- const tooLarge = before.length > MAX_DIFF_CONTENT || after.length > MAX_DIFF_CONTENT
26
- let additions = 0
27
- let deletions = 0
28
- if (!tooLarge) {
29
- for (const change of diffLines(before, after)) {
30
- if (change.added) additions += change.count || 0
31
- if (change.removed) deletions += change.count || 0
32
- }
33
- }
34
- return {
35
- file,
36
- patch: tooLarge ? "" : createTwoFilesPatch(file, file, before, after),
37
- additions,
38
- deletions,
39
- }
40
- }
41
-
42
- export function normalizeLineEndings(text: string): string {
43
- return text.replaceAll("\r\n", "\n")
44
- }
45
-
46
- export function detectLineEnding(text: string): "\n" | "\r\n" {
47
- return text.includes("\r\n") ? "\r\n" : "\n"
48
- }
49
-
50
- export function convertToLineEnding(text: string, ending: "\n" | "\r\n"): string {
51
- if (ending === "\n") return text
52
- return text.replaceAll("\n", "\r\n")
53
- }
54
-
55
- export function trimDiff(diff: string): string {
56
- const lines = diff.split("\n")
57
- const contentLines = lines.filter(
58
- (line) =>
59
- (line.startsWith("+") || line.startsWith("-") || line.startsWith(" ")) &&
60
- !line.startsWith("---") &&
61
- !line.startsWith("+++"),
62
- )
63
- if (contentLines.length === 0) return diff
64
- let min = Infinity
65
- for (const line of contentLines) {
66
- const content = line.slice(1)
67
- if (content.trim().length > 0) {
68
- const match = content.match(/^(\s*)/)
69
- if (match) min = Math.min(min, match[1].length)
70
- }
71
- }
72
- if (min === Infinity || min === 0) return diff
73
- const trimmedLines = lines.map((line) => {
74
- if (
75
- (line.startsWith("+") || line.startsWith("-") || line.startsWith(" ")) &&
76
- !line.startsWith("---") &&
77
- !line.startsWith("+++")
78
- ) {
79
- const prefix = line[0]
80
- const content = line.slice(1)
81
- return prefix + content.slice(min)
82
- }
83
- return line
84
- })
85
- return trimmedLines.join("\n")
86
- }
1
+ export * from "./file/edit-utils"
package/src/tool/edit.ts CHANGED
@@ -1,262 +1 @@
1
- // the approaches in this edit tool are sourced from
2
- // https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-23-25.ts
3
- // https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/utils/editCorrector.ts
4
- // https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-26-25.ts
5
-
6
- import * as path from "path"
7
- import { Effect, Schema, Semaphore } from "effect"
8
- import * as Tool from "./tool"
9
- import { LSP } from "@/lsp/lsp"
10
- import { createTwoFilesPatch } from "diff"
11
- import DESCRIPTION from "./edit.txt"
12
- import { File } from "../file"
13
- import { FileWatcher } from "../file/watcher"
14
- import { Bus } from "../bus"
15
- import { Format } from "../format"
16
- import { InstanceState } from "@/effect/instance-state"
17
- import { Snapshot } from "@/snapshot"
18
- import { assertExternalDirectoryEffect } from "./external-directory"
19
- import { AppFileSystem } from "@saeeol/core/filesystem"
20
- import * as Bom from "@/util/bom"
21
- import { filterDiagnostics } from "./diagnostics"
22
- import { ConfigValidation } from "../overlay/config-validation"
23
- import * as EncodedIO from "../overlay/tool/encoded-io"
24
- import { buildFileDiff, normalizeLineEndings, detectLineEnding, convertToLineEnding, trimDiff } from "./edit-utils"
25
- import {
26
- SimpleReplacer,
27
- LineTrimmedReplacer,
28
- BlockAnchorReplacer,
29
- WhitespaceNormalizedReplacer,
30
- IndentationFlexibleReplacer,
31
- EscapeNormalizedReplacer,
32
- MultiOccurrenceReplacer,
33
- TrimmedBoundaryReplacer,
34
- ContextAwareReplacer,
35
- } from "./edit-replacers"
36
-
37
- export { buildFileDiff, trimDiff } from "./edit-utils"
38
- export type { Replacer } from "./edit-replacers"
39
- export {
40
- SimpleReplacer,
41
- LineTrimmedReplacer,
42
- BlockAnchorReplacer,
43
- WhitespaceNormalizedReplacer,
44
- IndentationFlexibleReplacer,
45
- EscapeNormalizedReplacer,
46
- MultiOccurrenceReplacer,
47
- TrimmedBoundaryReplacer,
48
- ContextAwareReplacer,
49
- } from "./edit-replacers"
50
-
51
- const locks = new Map<string, Semaphore.Semaphore>()
52
-
53
- function lock(filePath: string) {
54
- const resolvedFilePath = AppFileSystem.resolve(filePath)
55
- const hit = locks.get(resolvedFilePath)
56
- if (hit) return hit
57
-
58
- const next = Semaphore.makeUnsafe(1)
59
- locks.set(resolvedFilePath, next)
60
- return next
61
- }
62
-
63
- export const Parameters = Schema.Struct({
64
- filePath: Schema.String.annotate({ description: "The absolute path to the file to modify" }),
65
- oldString: Schema.String.annotate({ description: "The text to replace" }),
66
- newString: Schema.String.annotate({
67
- description: "The text to replace it with (must be different from oldString)",
68
- }),
69
- replaceAll: Schema.optional(Schema.Boolean).annotate({
70
- description: "Replace all occurrences of oldString (default false)",
71
- }),
72
- })
73
-
74
- export function replace(content: string, oldString: string, newString: string, replaceAll = false): string {
75
- if (oldString === newString) {
76
- throw new Error("No changes to apply: oldString and newString are identical.")
77
- }
78
- let notFound = true
79
- for (const replacer of [
80
- SimpleReplacer,
81
- LineTrimmedReplacer,
82
- BlockAnchorReplacer,
83
- WhitespaceNormalizedReplacer,
84
- IndentationFlexibleReplacer,
85
- EscapeNormalizedReplacer,
86
- TrimmedBoundaryReplacer,
87
- ContextAwareReplacer,
88
- MultiOccurrenceReplacer,
89
- ]) {
90
- for (const search of replacer(content, oldString)) {
91
- const index = content.indexOf(search)
92
- if (index === -1) continue
93
- notFound = false
94
- if (replaceAll) {
95
- return content.replaceAll(search, newString)
96
- }
97
- const lastIndex = content.lastIndexOf(search)
98
- if (index !== lastIndex) continue
99
- return content.substring(0, index) + newString + content.substring(index + search.length)
100
- }
101
- }
102
- if (notFound) {
103
- throw new Error(
104
- "Could not find oldString in the file. It must match exactly, including whitespace, indentation, and line endings.",
105
- )
106
- }
107
- throw new Error("Found multiple matches for oldString. Provide more surrounding context to make the match unique.")
108
- }
109
-
110
- export const EditTool = Tool.define(
111
- "edit",
112
- Effect.gen(function* () {
113
- const lsp = yield* LSP.Service
114
- const afs = yield* AppFileSystem.Service
115
- const format = yield* Format.Service
116
- const bus = yield* Bus.Service
117
-
118
- return {
119
- description: DESCRIPTION,
120
- parameters: Parameters,
121
- execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
122
- Effect.gen(function* () {
123
- if (!params.filePath) {
124
- throw new Error("filePath is required")
125
- }
126
-
127
- if (params.oldString === params.newString) {
128
- throw new Error("No changes to apply: oldString and newString are identical.")
129
- }
130
-
131
- const instance = yield* InstanceState.context
132
- const filePath = path.isAbsolute(params.filePath)
133
- ? params.filePath
134
- : path.join(instance.directory, params.filePath)
135
- yield* assertExternalDirectoryEffect(ctx, filePath)
136
-
137
- let diff = ""
138
- let contentOld = ""
139
- let contentNew = ""
140
- let cachedFilediff: Snapshot.FileDiff | undefined
141
- yield* lock(filePath).withPermits(1)(
142
- Effect.gen(function* () {
143
- if (params.oldString === "") {
144
- const existed = yield* afs.existsSafe(filePath)
145
- // derive the BOM flag from the detected encoding label instead of the decoded text.
146
- const pre = existed ? yield* EncodedIO.read(filePath) : { text: "", encoding: "utf-8" }
147
- const source = { bom: pre.encoding === "utf-8-bom", text: pre.text, encoding: pre.encoding }
148
- const next = Bom.split(params.newString)
149
- const desiredBom = source.bom || next.bom
150
- contentOld = source.text
151
- contentNew = next.text
152
- diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
153
- cachedFilediff = buildFileDiff(filePath, contentOld, contentNew)
154
- yield* ctx.ask({
155
- permission: "edit",
156
- patterns: [path.relative(instance.worktree, filePath)],
157
- always: ["*"],
158
- metadata: {
159
- filepath: filePath,
160
- diff,
161
- filediff: cachedFilediff,
162
- },
163
- })
164
- yield* EncodedIO.write(filePath, Bom.join(contentNew, desiredBom), source.encoding)
165
- if (yield* format.file(filePath)) {
166
- contentNew = yield* Bom.syncFile(afs, filePath, desiredBom)
167
- }
168
- yield* bus.publish(File.Event.Edited, { file: filePath })
169
- yield* bus.publish(FileWatcher.Event.Updated, {
170
- file: filePath,
171
- event: existed ? "change" : "add",
172
- })
173
- return
174
- }
175
-
176
- const info = yield* afs.stat(filePath).pipe(Effect.catch(() => Effect.succeed(undefined)))
177
- if (!info) throw new Error(`File ${filePath} not found`)
178
- if (info.type === "Directory") throw new Error(`Path is a directory, not a file: ${filePath}`)
179
- // derive the BOM flag from the detected encoding label instead of the decoded text.
180
- const pre = yield* EncodedIO.read(filePath)
181
- const source = { bom: pre.encoding === "utf-8-bom", text: pre.text, encoding: pre.encoding }
182
- contentOld = source.text
183
-
184
- const ending = detectLineEnding(contentOld)
185
- const old = convertToLineEnding(normalizeLineEndings(params.oldString), ending)
186
- const replacement = convertToLineEnding(normalizeLineEndings(params.newString), ending)
187
-
188
- const next = Bom.split(replace(contentOld, old, replacement, params.replaceAll))
189
- const desiredBom = source.bom || next.bom
190
- contentNew = next.text
191
-
192
- diff = trimDiff(
193
- createTwoFilesPatch(
194
- filePath,
195
- filePath,
196
- normalizeLineEndings(contentOld),
197
- normalizeLineEndings(contentNew),
198
- ),
199
- )
200
- cachedFilediff = buildFileDiff(filePath, contentOld, contentNew)
201
- yield* ctx.ask({
202
- permission: "edit",
203
- patterns: [path.relative(instance.worktree, filePath)],
204
- always: ["*"],
205
- metadata: {
206
- filepath: filePath,
207
- diff,
208
- filediff: cachedFilediff,
209
- },
210
- })
211
-
212
- yield* EncodedIO.write(filePath, Bom.join(contentNew, desiredBom), source.encoding)
213
- if (yield* format.file(filePath)) {
214
- contentNew = yield* Bom.syncFile(afs, filePath, desiredBom)
215
- }
216
- yield* bus.publish(File.Event.Edited, { file: filePath })
217
- yield* bus.publish(FileWatcher.Event.Updated, {
218
- file: filePath,
219
- event: "change",
220
- })
221
- diff = trimDiff(
222
- createTwoFilesPatch(
223
- filePath,
224
- filePath,
225
- normalizeLineEndings(contentOld),
226
- normalizeLineEndings(contentNew),
227
- ),
228
- )
229
- }).pipe(Effect.orDie),
230
- )
231
-
232
- const filediff: Snapshot.FileDiff = cachedFilediff ?? buildFileDiff(filePath, contentOld, contentNew)
233
-
234
- yield* ctx.metadata({
235
- metadata: {
236
- diff,
237
- filediff,
238
- diagnostics: {},
239
- },
240
- })
241
-
242
- let output = "Edit applied successfully."
243
- yield* lsp.touchFile(filePath, "document")
244
- const diagnostics = yield* lsp.diagnostics()
245
- const normalizedFilePath = AppFileSystem.normalizePath(filePath)
246
- const block = LSP.Diagnostic.report(filePath, diagnostics[normalizedFilePath] ?? [])
247
- if (block) output += `\n\nLSP errors detected in this file, please fix:\n${block}`
248
- output += yield* Effect.promise(() => ConfigValidation.check(filePath))
249
-
250
- return {
251
- metadata: {
252
- diagnostics: filterDiagnostics(diagnostics, [normalizedFilePath]),
253
- diff,
254
- filediff,
255
- },
256
- title: `${path.relative(instance.worktree, filePath)}`,
257
- output,
258
- }
259
- }),
260
- }
261
- }),
262
- )
1
+ export * from "./file/edit"
@@ -1,55 +1 @@
1
- import path from "path"
2
- import { Effect } from "effect"
3
- import * as EffectLogger from "@saeeol/core/effect/logger"
4
- import { InstanceState } from "@/effect/instance-state"
5
- import type * as Tool from "./tool"
6
- import { AppFileSystem } from "@saeeol/core/filesystem"
7
-
8
- type Kind = "file" | "directory"
9
-
10
- type Options = {
11
- bypass?: boolean
12
- kind?: Kind
13
- }
14
- function root(dir: string) {
15
- return path.parse(dir).root === dir
16
- }
17
-
18
- function inside(dir: string, file: string) {
19
- return !root(dir) && AppFileSystem.contains(dir, file)
20
- }
21
-
22
- export const assertExternalDirectoryEffect = Effect.fn("Tool.assertExternalDirectory")(function* (
23
- ctx: Tool.Context,
24
- target?: string,
25
- options?: Options,
26
- ) {
27
- if (!target) return
28
-
29
- if (options?.bypass) return
30
-
31
- const ins = yield* InstanceState.context
32
- const full = process.platform === "win32" ? AppFileSystem.normalizePath(target) : target
33
- if (inside(ins.directory, full) || inside(ins.worktree, full)) return
34
-
35
- const kind = options?.kind ?? "file"
36
- const dir = kind === "directory" ? full : path.dirname(full)
37
- const glob =
38
- process.platform === "win32"
39
- ? AppFileSystem.normalizePathPattern(path.join(dir, "*"))
40
- : path.join(dir, "*").replaceAll("\\", "/")
41
-
42
- yield* ctx.ask({
43
- permission: "external_directory",
44
- patterns: [glob],
45
- always: [glob],
46
- metadata: {
47
- filepath: full,
48
- parentDir: dir,
49
- },
50
- })
51
- })
52
-
53
- export async function assertExternalDirectory(ctx: Tool.Context, target?: string, options?: Options) {
54
- return Effect.runPromise(assertExternalDirectoryEffect(ctx, target, options).pipe(Effect.provide(EffectLogger.layer)))
55
- }
1
+ export * from "./core/external-directory"