saeeol 1.2.1 → 1.2.3

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 (151) hide show
  1. package/bin/saeeol.cjs +187 -0
  2. package/npm/bin/saeeol +0 -0
  3. package/package.json +12 -12
  4. package/src/cli/cmd/tui/component/dialog/dialog-agent.tsx +32 -0
  5. package/src/cli/cmd/tui/component/dialog/dialog-command.tsx +190 -0
  6. package/src/cli/cmd/tui/component/dialog/dialog-console-org.tsx +103 -0
  7. package/src/cli/cmd/tui/component/dialog/dialog-go-upsell.tsx +159 -0
  8. package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +86 -0
  9. package/src/cli/cmd/tui/component/dialog/dialog-model.tsx +238 -0
  10. package/src/cli/cmd/tui/component/dialog/dialog-provider.tsx +343 -0
  11. package/src/cli/cmd/tui/component/dialog/dialog-session-delete-failed.tsx +103 -0
  12. package/src/cli/cmd/tui/component/dialog/dialog-session-list.tsx +301 -0
  13. package/src/cli/cmd/tui/component/dialog/dialog-session-rename.tsx +35 -0
  14. package/src/cli/cmd/tui/component/dialog/dialog-skill.tsx +37 -0
  15. package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +87 -0
  16. package/src/cli/cmd/tui/component/dialog/dialog-status.tsx +190 -0
  17. package/src/cli/cmd/tui/component/dialog/dialog-tag.tsx +44 -0
  18. package/src/cli/cmd/tui/component/dialog/dialog-theme-list.tsx +50 -0
  19. package/src/cli/cmd/tui/component/dialog/dialog-variant.tsx +39 -0
  20. package/src/cli/cmd/tui/component/dialog/dialog-workspace-create.tsx +200 -0
  21. package/src/cli/cmd/tui/component/dialog/dialog-workspace-unavailable.tsx +81 -0
  22. package/src/cli/cmd/tui/component/dialog-agent.tsx +1 -32
  23. package/src/cli/cmd/tui/component/dialog-command.tsx +1 -190
  24. package/src/cli/cmd/tui/component/dialog-console-org.tsx +1 -103
  25. package/src/cli/cmd/tui/component/dialog-go-upsell.tsx +1 -159
  26. package/src/cli/cmd/tui/component/dialog-mcp.tsx +1 -86
  27. package/src/cli/cmd/tui/component/dialog-model.tsx +1 -238
  28. package/src/cli/cmd/tui/component/dialog-provider.tsx +1 -343
  29. package/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx +1 -103
  30. package/src/cli/cmd/tui/component/dialog-session-list.tsx +1 -301
  31. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +1 -35
  32. package/src/cli/cmd/tui/component/dialog-skill.tsx +1 -37
  33. package/src/cli/cmd/tui/component/dialog-stash.tsx +1 -87
  34. package/src/cli/cmd/tui/component/dialog-status.tsx +1 -190
  35. package/src/cli/cmd/tui/component/dialog-tag.tsx +1 -44
  36. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +1 -50
  37. package/src/cli/cmd/tui/component/dialog-variant.tsx +1 -39
  38. package/src/cli/cmd/tui/component/dialog-workspace-create.tsx +1 -200
  39. package/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +1 -81
  40. package/src/cli/cmd/tui/context/app/args.tsx +15 -0
  41. package/src/cli/cmd/tui/context/app/directory.ts +15 -0
  42. package/src/cli/cmd/tui/context/app/editor-zed.ts +281 -0
  43. package/src/cli/cmd/tui/context/app/editor.ts +425 -0
  44. package/src/cli/cmd/tui/context/app/helper.tsx +25 -0
  45. package/src/cli/cmd/tui/context/app/project.tsx +109 -0
  46. package/src/cli/cmd/tui/context/app/route.tsx +67 -0
  47. package/src/cli/cmd/tui/context/app/sdk.tsx +142 -0
  48. package/src/cli/cmd/tui/context/app/sync.tsx +713 -0
  49. package/src/cli/cmd/tui/context/app/theme.tsx +307 -0
  50. package/src/cli/cmd/tui/context/app/tui-config.tsx +9 -0
  51. package/src/cli/cmd/tui/context/args.tsx +1 -15
  52. package/src/cli/cmd/tui/context/directory.ts +1 -15
  53. package/src/cli/cmd/tui/context/editor-zed.ts +1 -281
  54. package/src/cli/cmd/tui/context/editor.ts +1 -425
  55. package/src/cli/cmd/tui/context/event.ts +1 -45
  56. package/src/cli/cmd/tui/context/exit.tsx +1 -67
  57. package/src/cli/cmd/tui/context/helper.tsx +1 -25
  58. package/src/cli/cmd/tui/context/keybind.tsx +1 -105
  59. package/src/cli/cmd/tui/context/kv.tsx +1 -76
  60. package/src/cli/cmd/tui/context/local.tsx +1 -478
  61. package/src/cli/cmd/tui/context/plugin-keybinds.ts +1 -41
  62. package/src/cli/cmd/tui/context/project.tsx +1 -109
  63. package/src/cli/cmd/tui/context/prompt.tsx +1 -18
  64. package/src/cli/cmd/tui/context/route.tsx +1 -67
  65. package/src/cli/cmd/tui/context/runtime/event.ts +45 -0
  66. package/src/cli/cmd/tui/context/runtime/exit.tsx +67 -0
  67. package/src/cli/cmd/tui/context/runtime/keybind.tsx +105 -0
  68. package/src/cli/cmd/tui/context/runtime/kv.tsx +76 -0
  69. package/src/cli/cmd/tui/context/runtime/local.tsx +478 -0
  70. package/src/cli/cmd/tui/context/runtime/plugin-keybinds.ts +41 -0
  71. package/src/cli/cmd/tui/context/sdk.tsx +1 -142
  72. package/src/cli/cmd/tui/context/session/prompt.tsx +18 -0
  73. package/src/cli/cmd/tui/context/sync.tsx +1 -713
  74. package/src/cli/cmd/tui/context/theme.tsx +1 -307
  75. package/src/cli/cmd/tui/context/tui-config.tsx +1 -9
  76. package/src/tool/apply_patch.ts +1 -334
  77. package/src/tool/bash.ts +1 -656
  78. package/src/tool/core/external-directory.ts +55 -0
  79. package/src/tool/core/invalid.ts +21 -0
  80. package/src/tool/core/recall.ts +164 -0
  81. package/src/tool/core/recall.txt +12 -0
  82. package/src/tool/core/schema.ts +16 -0
  83. package/src/tool/core/tool.ts +162 -0
  84. package/src/tool/core/truncate.ts +160 -0
  85. package/src/tool/core/truncation-dir.ts +4 -0
  86. package/src/tool/diagnostics.ts +1 -20
  87. package/src/tool/edit-replacers.ts +1 -288
  88. package/src/tool/edit-utils.ts +1 -86
  89. package/src/tool/edit.ts +1 -262
  90. package/src/tool/external-directory.ts +1 -55
  91. package/src/tool/file/apply_patch.ts +334 -0
  92. package/src/tool/file/apply_patch.txt +33 -0
  93. package/src/tool/file/bash.ts +656 -0
  94. package/src/tool/file/bash.txt +119 -0
  95. package/src/tool/file/edit-replacers.ts +288 -0
  96. package/src/tool/file/edit-utils.ts +86 -0
  97. package/src/tool/file/edit.ts +262 -0
  98. package/src/tool/file/edit.txt +10 -0
  99. package/src/tool/file/read.ts +389 -0
  100. package/src/tool/file/read.txt +14 -0
  101. package/src/tool/file/write.ts +114 -0
  102. package/src/tool/file/write.txt +8 -0
  103. package/src/tool/glob.ts +1 -115
  104. package/src/tool/grep.ts +1 -151
  105. package/src/tool/integration/diagnostics.ts +20 -0
  106. package/src/tool/integration/lsp.ts +113 -0
  107. package/src/tool/integration/lsp.txt +24 -0
  108. package/src/tool/integration/mcp-exa.ts +73 -0
  109. package/src/tool/integration/package.ts +168 -0
  110. package/src/tool/integration/registry.ts +375 -0
  111. package/src/tool/invalid.ts +1 -21
  112. package/src/tool/lsp.ts +1 -113
  113. package/src/tool/mcp-exa.ts +1 -73
  114. package/src/tool/package.ts +1 -168
  115. package/src/tool/plan.ts +1 -30
  116. package/src/tool/question.ts +1 -52
  117. package/src/tool/read.ts +1 -389
  118. package/src/tool/recall.ts +1 -164
  119. package/src/tool/registry.ts +1 -375
  120. package/src/tool/schema.ts +1 -16
  121. package/src/tool/search/glob.ts +115 -0
  122. package/src/tool/search/glob.txt +6 -0
  123. package/src/tool/search/grep.ts +151 -0
  124. package/src/tool/search/grep.txt +8 -0
  125. package/src/tool/search/warpgrep.ts +107 -0
  126. package/src/tool/search/warpgrep.txt +10 -0
  127. package/src/tool/search/webfetch.ts +202 -0
  128. package/src/tool/search/webfetch.txt +13 -0
  129. package/src/tool/search/websearch.ts +71 -0
  130. package/src/tool/search/websearch.txt +14 -0
  131. package/src/tool/skill.ts +1 -91
  132. package/src/tool/task.ts +1 -197
  133. package/src/tool/todo.ts +1 -62
  134. package/src/tool/tool.ts +1 -162
  135. package/src/tool/truncate.ts +1 -160
  136. package/src/tool/truncation-dir.ts +1 -4
  137. package/src/tool/warpgrep.ts +1 -107
  138. package/src/tool/webfetch.ts +1 -202
  139. package/src/tool/websearch.ts +1 -71
  140. package/src/tool/workflow/plan-enter.txt +14 -0
  141. package/src/tool/workflow/plan-exit.txt +13 -0
  142. package/src/tool/workflow/plan.ts +30 -0
  143. package/src/tool/workflow/question.ts +52 -0
  144. package/src/tool/workflow/question.txt +11 -0
  145. package/src/tool/workflow/skill.ts +91 -0
  146. package/src/tool/workflow/skill.txt +5 -0
  147. package/src/tool/workflow/task.ts +197 -0
  148. package/src/tool/workflow/task.txt +57 -0
  149. package/src/tool/workflow/todo.ts +62 -0
  150. package/src/tool/workflow/todowrite.txt +167 -0
  151. package/src/tool/write.ts +1 -114
@@ -1,307 +1 @@
1
- import { CliRenderEvents, type TerminalColors } from "@opentui/core"
2
- import path from "path"
3
- import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
4
- import { createSimpleContext } from "./helper"
5
- import { Glob } from "@saeeol/core/util/glob"
6
- import { useKV } from "./kv"
7
- import { useRenderer } from "@opentui/solid"
8
- import { createStore, produce } from "solid-js/store"
9
- import { Global } from "@saeeol/core/global"
10
- import { Filesystem } from "@/util/filesystem"
11
- import { useTuiConfig } from "./tui-config"
12
- import { isRecord } from "@/util/record"
13
- import { selectedForeground, tint } from "./theme/theme-types"
14
- import type { ThemeJson, Theme } from "./theme/theme-types"
15
- import { DEFAULT_THEMES, isValidTheme } from "./theme/theme-themes"
16
- import { resolveTheme } from "./theme/theme-resolve"
17
- import { generateSystem } from "./theme/theme-system"
18
- import { generateSyntax, generateSubtleSyntax } from "./theme/theme-syntax"
19
-
20
- export { selectedForeground, tint }
21
- export type { ThemeJson }
22
- export { DEFAULT_THEMES }
23
- export { resolveTheme }
24
-
25
- type State = {
26
- themes: Record<string, ThemeJson>
27
- mode: "dark" | "light"
28
- lock: "dark" | "light" | undefined
29
- active: string
30
- ready: boolean
31
- }
32
-
33
- const pluginThemes: Record<string, ThemeJson> = {}
34
- let customThemes: Record<string, ThemeJson> = {}
35
- let systemTheme: ThemeJson | undefined
36
-
37
- function listThemes() {
38
- const themes = {
39
- ...DEFAULT_THEMES,
40
- ...pluginThemes,
41
- ...customThemes,
42
- }
43
- if (!systemTheme) return themes
44
- return {
45
- ...themes,
46
- system: systemTheme,
47
- }
48
- }
49
-
50
- function syncThemes() {
51
- setStore("themes", listThemes())
52
- }
53
-
54
- const [store, setStore] = createStore<State>({
55
- themes: listThemes(),
56
- mode: "dark",
57
- lock: undefined,
58
- active: "saeeol",
59
- ready: false,
60
- })
61
-
62
- export function allThemes() {
63
- return store.themes
64
- }
65
-
66
- function isTheme(theme: unknown): theme is ThemeJson {
67
- if (!isRecord(theme)) return false
68
- if (!isRecord(theme.theme)) return false
69
- return true
70
- }
71
-
72
- export function hasTheme(name: string) {
73
- if (!name) return false
74
- return allThemes()[name] !== undefined
75
- }
76
-
77
- export function addTheme(name: string, theme: unknown) {
78
- if (!name) return false
79
- if (!isTheme(theme)) return false
80
- if (hasTheme(name)) return false
81
- pluginThemes[name] = theme
82
- syncThemes()
83
- return true
84
- }
85
-
86
- export function upsertTheme(name: string, theme: unknown) {
87
- if (!name) return false
88
- if (!isTheme(theme)) return false
89
- if (customThemes[name] !== undefined) {
90
- customThemes[name] = theme
91
- } else {
92
- pluginThemes[name] = theme
93
- }
94
- syncThemes()
95
- return true
96
- }
97
-
98
- export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
99
- name: "Theme",
100
- init: (props: { mode: "dark" | "light" }) => {
101
- const renderer = useRenderer()
102
- const config = useTuiConfig()
103
- const kv = useKV()
104
- const pick = (value: unknown) => {
105
- if (value === "dark" || value === "light") return value
106
- return
107
- }
108
-
109
- setStore(
110
- produce((draft) => {
111
- const lock = pick(kv.get("theme_mode_lock"))
112
- const mode = lock ?? pick(renderer.themeMode) ?? props.mode
113
- if (!lock && pick(kv.get("theme_mode")) !== undefined) {
114
- kv.set("theme_mode", undefined)
115
- }
116
- draft.mode = mode
117
- draft.lock = lock
118
- const active = config.theme ?? kv.get("theme", "saeeol")
119
- draft.active = typeof active === "string" ? active : "saeeol"
120
- draft.ready = false
121
- }),
122
- )
123
-
124
- createEffect(() => {
125
- const theme = config.theme
126
- if (theme) setStore("active", theme)
127
- })
128
-
129
- function init() {
130
- void Promise.allSettled([
131
- resolveSystemTheme(store.mode),
132
- getCustomThemes()
133
- .then((custom) => {
134
- customThemes = custom
135
- syncThemes()
136
- })
137
- .catch(() => {
138
- setStore("active", "saeeol")
139
- }),
140
- ]).finally(() => {
141
- setStore("ready", true)
142
- })
143
- }
144
-
145
- onMount(init)
146
-
147
- function resolveSystemTheme(mode: "dark" | "light" = store.mode) {
148
- return renderer
149
- .getPalette({
150
- size: 16,
151
- })
152
- .then((colors: TerminalColors) => {
153
- if (!colors.palette[0]) {
154
- systemTheme = undefined
155
- syncThemes()
156
- if (store.active === "system") {
157
- setStore("active", "saeeol")
158
- }
159
- return
160
- }
161
- systemTheme = generateSystem(colors, mode)
162
- syncThemes()
163
- })
164
- .catch(() => {
165
- systemTheme = undefined
166
- syncThemes()
167
- if (store.active === "system") {
168
- setStore("active", "saeeol")
169
- }
170
- })
171
- }
172
-
173
- function apply(mode: "dark" | "light") {
174
- if (store.lock !== undefined) kv.set("theme_mode", mode)
175
- if (store.mode === mode) return
176
- setStore("mode", mode)
177
- renderer.clearPaletteCache()
178
- void resolveSystemTheme(mode)
179
- }
180
-
181
- function pin(mode: "dark" | "light" = store.mode) {
182
- setStore("lock", mode)
183
- kv.set("theme_mode_lock", mode)
184
- apply(mode)
185
- }
186
-
187
- function free() {
188
- setStore("lock", undefined)
189
- kv.set("theme_mode_lock", undefined)
190
- kv.set("theme_mode", undefined)
191
- const mode = renderer.themeMode
192
- if (mode) apply(mode)
193
- }
194
-
195
- const handle = (mode: "dark" | "light") => {
196
- if (store.lock) return
197
- apply(mode)
198
- }
199
- renderer.on(CliRenderEvents.THEME_MODE, handle)
200
-
201
- const refresh = () => {
202
- renderer.clearPaletteCache()
203
- init()
204
- }
205
- process.on("SIGUSR2", refresh)
206
-
207
- onCleanup(() => {
208
- renderer.off(CliRenderEvents.THEME_MODE, handle)
209
- process.off("SIGUSR2", refresh)
210
- })
211
- const values = createMemo(() => {
212
- const active = store.themes[store.active]
213
- if (active) {
214
- return resolveTheme(active, store.mode)
215
- }
216
-
217
- const saved = kv.get("theme")
218
- if (typeof saved === "string") {
219
- const theme = store.themes[saved]
220
- if (theme) {
221
- return resolveTheme(theme, store.mode)
222
- }
223
- }
224
-
225
- return resolveTheme(store.themes.saeeol, store.mode)
226
- })
227
-
228
- createEffect(() => {
229
- renderer.setBackgroundColor(values().background)
230
- })
231
-
232
- const syntax = createMemo(() => generateSyntax(values()))
233
- const subtleSyntax = createMemo(() => generateSubtleSyntax(values()))
234
- return {
235
- theme: new Proxy({} as Theme, {
236
- get(_target, prop) {
237
- // @ts-expect-error
238
- return values()[prop]
239
- },
240
- }),
241
- get selected() {
242
- return store.active
243
- },
244
- all() {
245
- return allThemes()
246
- },
247
- has(name: string) {
248
- return hasTheme(name)
249
- },
250
- syntax,
251
- subtleSyntax,
252
- mode() {
253
- return store.mode
254
- },
255
- locked() {
256
- return store.lock !== undefined
257
- },
258
- lock() {
259
- pin(store.mode)
260
- },
261
- unlock() {
262
- free()
263
- },
264
- setMode(mode: "dark" | "light") {
265
- pin(mode)
266
- },
267
- set(theme: string) {
268
- if (!hasTheme(theme)) return false
269
- setStore("active", theme)
270
- kv.set("theme", theme)
271
- return true
272
- },
273
- get ready() {
274
- return store.ready
275
- },
276
- }
277
- },
278
- })
279
-
280
- async function getCustomThemes() {
281
- const directories = [
282
- Global.Path.config,
283
- ...(await Array.fromAsync(
284
- Filesystem.up({
285
- targets: [".saeeol", ".saeeol"],
286
- start: process.cwd(),
287
- }),
288
- )),
289
- ]
290
-
291
- const result: Record<string, ThemeJson> = {}
292
- for (const dir of directories) {
293
- for (const item of await Glob.scan("themes/*.json", {
294
- cwd: dir,
295
- absolute: true,
296
- dot: true,
297
- symlink: true,
298
- })) {
299
- const name = path.basename(item, ".json")
300
- if (name in DEFAULT_THEMES) continue
301
- const json = await Filesystem.readJson(item).catch(() => null)
302
- if (!isValidTheme(json)) continue
303
- result[name] = json
304
- }
305
- }
306
- return result
307
- }
1
+ export * from "./app/theme"
@@ -1,9 +1 @@
1
- import { TuiConfig } from "@/cli/cmd/tui/config/tui"
2
- import { createSimpleContext } from "./helper"
3
-
4
- export const { use: useTuiConfig, provider: TuiConfigProvider } = createSimpleContext({
5
- name: "TuiConfig",
6
- init: (props: { config: TuiConfig.Info }) => {
7
- return props.config
8
- },
9
- })
1
+ export * from "./app/tui-config"
@@ -1,334 +1 @@
1
- import * as path from "path"
2
- import { Effect, Schema } from "effect"
3
- import * as Tool from "./tool"
4
- import { Bus } from "../bus"
5
- import { FileWatcher } from "../file/watcher"
6
- import { InstanceState } from "@/effect/instance-state"
7
- import { Patch } from "../patch"
8
- import { createTwoFilesPatch, diffLines } from "diff"
9
- import { assertExternalDirectoryEffect } from "./external-directory"
10
- import { trimDiff } from "./edit"
11
- import { LSP } from "@/lsp/lsp"
12
- import { AppFileSystem } from "@saeeol/core/filesystem"
13
- import DESCRIPTION from "./apply_patch.txt"
14
- import { File } from "../file"
15
- import { filterDiagnostics } from "./diagnostics"
16
- import { ConfigValidation } from "../overlay/config-validation"
17
- import * as EncodedIO from "../overlay/tool/encoded-io"
18
- import { Format } from "../format"
19
- import * as Bom from "@/util/bom"
20
-
21
- export const Parameters = Schema.Struct({
22
- patchText: Schema.String.annotate({ description: "The full patch text that describes all changes to be made" }),
23
- })
24
-
25
- export const ApplyPatchTool = Tool.define(
26
- "apply_patch",
27
- Effect.gen(function* () {
28
- const lsp = yield* LSP.Service
29
- const afs = yield* AppFileSystem.Service
30
- const format = yield* Format.Service
31
- const bus = yield* Bus.Service
32
-
33
- const run = Effect.fn("ApplyPatchTool.execute")(function* (
34
- params: Schema.Schema.Type<typeof Parameters>,
35
- ctx: Tool.Context,
36
- ) {
37
- if (!params.patchText) {
38
- return yield* Effect.fail(new Error("patchText is required"))
39
- }
40
-
41
- // Parse the patch to get hunks
42
- let hunks: Patch.Hunk[]
43
- try {
44
- const parseResult = Patch.parsePatch(params.patchText)
45
- hunks = parseResult.hunks
46
- } catch (error) {
47
- return yield* Effect.fail(new Error(`apply_patch verification failed: ${error}`))
48
- }
49
-
50
- if (hunks.length === 0) {
51
- const normalized = params.patchText.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim()
52
- if (normalized === "*** Begin Patch\n*** End Patch") {
53
- return yield* Effect.fail(new Error("patch rejected: empty patch"))
54
- }
55
- return yield* Effect.fail(new Error("apply_patch verification failed: no hunks found"))
56
- }
57
-
58
- const instance = yield* InstanceState.context
59
-
60
- // Validate file paths and check permissions
61
- const fileChanges: Array<{
62
- filePath: string
63
- oldContent: string
64
- newContent: string
65
- type: "add" | "update" | "delete" | "move"
66
- movePath?: string
67
- diff: string
68
- additions: number
69
- deletions: number
70
- bom: boolean
71
- encoding: string
72
- }> = []
73
-
74
- let totalDiff = ""
75
-
76
- for (const hunk of hunks) {
77
- const filePath = path.resolve(instance.directory, hunk.path)
78
- yield* assertExternalDirectoryEffect(ctx, filePath)
79
-
80
- switch (hunk.type) {
81
- case "add": {
82
- const oldContent = ""
83
- const newContent =
84
- hunk.contents.length === 0 || hunk.contents.endsWith("\n") ? hunk.contents : `${hunk.contents}\n`
85
- const next = Bom.split(newContent)
86
- const diff = trimDiff(createTwoFilesPatch(filePath, filePath, oldContent, next.text))
87
-
88
- let additions = 0
89
- let deletions = 0
90
- for (const change of diffLines(oldContent, next.text)) {
91
- if (change.added) additions += change.count || 0
92
- if (change.removed) deletions += change.count || 0
93
- }
94
-
95
- fileChanges.push({
96
- filePath,
97
- oldContent,
98
- newContent: next.text,
99
- type: "add",
100
- diff,
101
- additions,
102
- deletions,
103
- bom: next.bom,
104
- encoding: "utf-8",
105
- })
106
-
107
- totalDiff += diff + "\n"
108
- break
109
- }
110
-
111
- case "update": {
112
- // Check if file exists for update
113
- const stats = yield* afs.stat(filePath).pipe(Effect.catch(() => Effect.succeed(undefined)))
114
- if (!stats || stats.type === "Directory") {
115
- return yield* Effect.fail(
116
- new Error(`apply_patch verification failed: Failed to read file to update: ${filePath}`),
117
- )
118
- }
119
- // mojibake; the resulting diff, additions/deletions counts, and permission-prompt
120
- // metadata shown to the user must reflect the real file contents.
121
- const read = yield* EncodedIO.read(filePath).pipe(
122
- Effect.catch((error) =>
123
- Effect.fail(
124
- new Error(
125
- `apply_patch verification failed: ${error instanceof Error ? error.message : String(error)}`,
126
- ),
127
- ),
128
- ),
129
- )
130
- const source = Bom.split(read.text)
131
- const oldContent = source.text
132
- let newContent = oldContent
133
- let bom = source.bom
134
- let encoding = read.encoding
135
-
136
- // Apply the update chunks to get new content
137
- try {
138
- const fileUpdate = Patch.deriveNewContentsFromChunks(filePath, hunk.chunks)
139
- newContent = fileUpdate.content
140
- bom = fileUpdate.bom
141
- encoding = fileUpdate.encoding
142
- } catch (error) {
143
- return yield* Effect.fail(new Error(`apply_patch verification failed: ${error}`))
144
- }
145
-
146
- const diff = trimDiff(createTwoFilesPatch(filePath, filePath, oldContent, newContent))
147
-
148
- let additions = 0
149
- let deletions = 0
150
- for (const change of diffLines(oldContent, newContent)) {
151
- if (change.added) additions += change.count || 0
152
- if (change.removed) deletions += change.count || 0
153
- }
154
-
155
- const movePath = hunk.move_path ? path.resolve(instance.directory, hunk.move_path) : undefined
156
- yield* assertExternalDirectoryEffect(ctx, movePath)
157
-
158
- fileChanges.push({
159
- filePath,
160
- oldContent,
161
- newContent,
162
- type: hunk.move_path ? "move" : "update",
163
- movePath,
164
- diff,
165
- additions,
166
- deletions,
167
- bom,
168
- encoding,
169
- })
170
-
171
- totalDiff += diff + "\n"
172
- break
173
- }
174
-
175
- case "delete": {
176
- const deleteRead = yield* EncodedIO.read(filePath).pipe(
177
- Effect.catch((error) =>
178
- Effect.fail(
179
- new Error(
180
- `apply_patch verification failed: ${error instanceof Error ? error.message : String(error)}`,
181
- ),
182
- ),
183
- ),
184
- )
185
- const contentToDelete = deleteRead.text
186
- const source = Bom.split(contentToDelete)
187
- const deleteDiff = trimDiff(createTwoFilesPatch(filePath, filePath, contentToDelete, ""))
188
-
189
- const deletions = contentToDelete.split("\n").length
190
-
191
- fileChanges.push({
192
- filePath,
193
- oldContent: contentToDelete,
194
- newContent: "",
195
- type: "delete",
196
- diff: deleteDiff,
197
- additions: 0,
198
- deletions,
199
- bom: source.bom,
200
- encoding: deleteRead.encoding,
201
- })
202
-
203
- totalDiff += deleteDiff + "\n"
204
- break
205
- }
206
- }
207
- }
208
-
209
- // Build per-file metadata for UI rendering (used for both permission and result)
210
- const files = fileChanges.map((change) => ({
211
- filePath: change.filePath,
212
- relativePath: path.relative(instance.worktree, change.movePath ?? change.filePath).replaceAll("\\", "/"),
213
- type: change.type,
214
- patch: change.diff,
215
- additions: change.additions,
216
- deletions: change.deletions,
217
- movePath: change.movePath,
218
- }))
219
-
220
- // Check permissions if needed
221
- const relativePaths = fileChanges.map((c) => path.relative(instance.worktree, c.filePath).replaceAll("\\", "/"))
222
- yield* ctx.ask({
223
- permission: "edit",
224
- patterns: relativePaths,
225
- always: ["*"],
226
- metadata: {
227
- filepath: relativePaths.join(", "),
228
- diff: totalDiff,
229
- files,
230
- },
231
- })
232
-
233
- // Apply the changes
234
- const updates: Array<{ file: string; event: "add" | "change" | "unlink" }> = []
235
-
236
- for (const change of fileChanges) {
237
- const edited = change.type === "delete" ? undefined : (change.movePath ?? change.filePath)
238
- switch (change.type) {
239
- case "add":
240
- // Create parent directories (recursive: true is safe on existing/root dirs)
241
- yield* EncodedIO.write(change.filePath, Bom.join(change.newContent, change.bom), change.encoding)
242
- updates.push({ file: change.filePath, event: "add" })
243
- break
244
-
245
- case "update":
246
- yield* EncodedIO.write(change.filePath, Bom.join(change.newContent, change.bom), change.encoding)
247
- updates.push({ file: change.filePath, event: "change" })
248
- break
249
-
250
- case "move":
251
- if (change.movePath) {
252
- // Create parent directories (recursive: true is safe on existing/root dirs)
253
- yield* EncodedIO.write(change.movePath!, Bom.join(change.newContent, change.bom), change.encoding)
254
- yield* afs.remove(change.filePath)
255
- updates.push({ file: change.filePath, event: "unlink" })
256
- updates.push({ file: change.movePath, event: "add" })
257
- }
258
- break
259
-
260
- case "delete":
261
- yield* afs.remove(change.filePath)
262
- updates.push({ file: change.filePath, event: "unlink" })
263
- break
264
- }
265
-
266
- if (edited) {
267
- if (yield* format.file(edited)) {
268
- yield* Bom.syncFile(afs, edited, change.bom)
269
- }
270
- yield* bus.publish(File.Event.Edited, { file: edited })
271
- }
272
- }
273
-
274
- // Publish file change events
275
- for (const update of updates) {
276
- yield* bus.publish(FileWatcher.Event.Updated, update)
277
- }
278
-
279
- // Notify LSP of file changes and collect diagnostics
280
- for (const change of fileChanges) {
281
- if (change.type === "delete") continue
282
- const target = change.movePath ?? change.filePath
283
- yield* lsp.touchFile(target, "document")
284
- }
285
- const diagnostics = yield* lsp.diagnostics()
286
-
287
- // Generate output summary
288
- const summaryLines = fileChanges.map((change) => {
289
- if (change.type === "add") {
290
- return `A ${path.relative(instance.worktree, change.filePath).replaceAll("\\", "/")}`
291
- }
292
- if (change.type === "delete") {
293
- return `D ${path.relative(instance.worktree, change.filePath).replaceAll("\\", "/")}`
294
- }
295
- const target = change.movePath ?? change.filePath
296
- return `M ${path.relative(instance.worktree, target).replaceAll("\\", "/")}`
297
- })
298
- let output = `Success. Updated the following files:\n${summaryLines.join("\n")}`
299
- const changedPaths = fileChanges
300
- .filter((c) => c.type !== "delete")
301
- .map((c) => AppFileSystem.normalizePath(c.movePath ?? c.filePath))
302
-
303
- for (const change of fileChanges) {
304
- if (change.type === "delete") continue
305
- const target = change.movePath ?? change.filePath
306
- const block = LSP.Diagnostic.report(target, diagnostics[AppFileSystem.normalizePath(target)] ?? [])
307
- if (!block) continue
308
- const rel = path.relative(instance.worktree, target).replaceAll("\\", "/")
309
- output += `\n\nLSP errors detected in ${rel}, please fix:\n${block}`
310
- }
311
- for (const changed of fileChanges) {
312
- if (changed.type === "delete") continue
313
- output += yield* Effect.promise(() => ConfigValidation.check(changed.movePath ?? changed.filePath))
314
- }
315
-
316
- return {
317
- title: output,
318
- metadata: {
319
- diff: totalDiff,
320
- files,
321
- diagnostics: filterDiagnostics(diagnostics, changedPaths),
322
- },
323
- output,
324
- }
325
- })
326
-
327
- return {
328
- description: DESCRIPTION,
329
- parameters: Parameters,
330
- execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
331
- run(params, ctx).pipe(Effect.orDie),
332
- }
333
- }),
334
- )
1
+ export * from "./file/apply_patch"