write.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. import { Schema } from "effect"
  2. import * as path from "path"
  3. import { Effect } from "effect"
  4. import * as Tool from "./tool"
  5. import { LSP } from "@/lsp/lsp"
  6. import { createTwoFilesPatch } from "diff"
  7. import DESCRIPTION from "./write.txt"
  8. import { EventV2Bridge } from "@/event-v2-bridge"
  9. import { FileSystem } from "@kirincode-ai/core/filesystem"
  10. import { Watcher } from "@kirincode-ai/core/filesystem/watcher"
  11. import { Format } from "../format"
  12. import { FSUtil } from "@kirincode-ai/core/fs-util"
  13. import { InstanceState } from "@/effect/instance-state"
  14. import { trimDiff } from "./edit"
  15. import { assertExternalDirectoryEffect } from "./external-directory"
  16. import * as Bom from "@/util/bom"
  17. const MAX_PROJECT_DIAGNOSTICS_FILES = 5
  18. export const Parameters = Schema.Struct({
  19. content: Schema.String.annotate({ description: "The content to write to the file" }),
  20. filePath: Schema.String.annotate({
  21. description: "The absolute path to the file to write (must be absolute, not relative)",
  22. }),
  23. })
  24. export const WriteTool = Tool.define(
  25. "write",
  26. Effect.gen(function* () {
  27. const lsp = yield* LSP.Service
  28. const fs = yield* FSUtil.Service
  29. const events = yield* EventV2Bridge.Service
  30. const format = yield* Format.Service
  31. return {
  32. description: DESCRIPTION,
  33. parameters: Parameters,
  34. execute: (params: { content: string; filePath: string }, ctx: Tool.Context) =>
  35. Effect.gen(function* () {
  36. const instance = yield* InstanceState.context
  37. const filepath = path.isAbsolute(params.filePath)
  38. ? params.filePath
  39. : path.join(instance.directory, params.filePath)
  40. yield* assertExternalDirectoryEffect(ctx, filepath)
  41. const exists = yield* fs.existsSafe(filepath)
  42. const source = exists ? yield* Bom.readFile(fs, filepath) : { bom: false, text: "" }
  43. const next = Bom.split(params.content)
  44. const desiredBom = source.bom || next.bom
  45. const contentOld = source.text
  46. const contentNew = next.text
  47. const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, contentNew))
  48. yield* ctx.ask({
  49. permission: "edit",
  50. patterns: [path.relative(instance.worktree, filepath)],
  51. always: ["*"],
  52. metadata: {
  53. filepath,
  54. diff,
  55. },
  56. })
  57. yield* fs.writeWithDirs(filepath, Bom.join(contentNew, desiredBom))
  58. if (yield* format.file(filepath)) {
  59. yield* Bom.syncFile(fs, filepath, desiredBom)
  60. }
  61. yield* events.publish(FileSystem.Event.Edited, { file: filepath })
  62. yield* events.publish(Watcher.Event.Updated, {
  63. file: filepath,
  64. event: exists ? "change" : "add",
  65. })
  66. let output = "Wrote file successfully."
  67. yield* lsp.touchFile(filepath, "document")
  68. const diagnostics = yield* lsp.diagnostics()
  69. const normalizedFilepath = FSUtil.normalizePath(filepath)
  70. let projectDiagnosticsCount = 0
  71. for (const [file, issues] of Object.entries(diagnostics)) {
  72. const current = file === normalizedFilepath
  73. if (!current && projectDiagnosticsCount >= MAX_PROJECT_DIAGNOSTICS_FILES) continue
  74. const block = LSP.Diagnostic.report(current ? filepath : file, issues)
  75. if (!block) continue
  76. if (current) {
  77. output += `\n\nLSP errors detected in this file, please fix:\n${block}`
  78. continue
  79. }
  80. projectDiagnosticsCount++
  81. output += `\n\nLSP errors detected in other files:\n${block}`
  82. }
  83. return {
  84. title: path.relative(instance.worktree, filepath),
  85. metadata: {
  86. diagnostics,
  87. filepath,
  88. exists: exists,
  89. },
  90. output,
  91. }
  92. }).pipe(Effect.orDie),
  93. }
  94. }),
  95. )