2025-10-26 19:50:41 +00:00
import z from "zod"
2025-06-04 17:33:25 +00:00
import * as path from "path"
2026-04-10 20:57:12 +00:00
import { Effect } from "effect"
2025-06-04 17:33:25 +00:00
import { Tool } from "./tool"
import { LSP } from "../lsp"
2026-01-01 22:54:11 +00:00
import { createTwoFilesPatch } from "diff"
2025-06-04 17:33:25 +00:00
import DESCRIPTION from "./write.txt"
2025-06-27 15:29:20 +00:00
import { Bus } from "../bus"
2026-03-21 04:51:35 +00:00
import { File } from "../file"
2026-01-26 22:46:01 +00:00
import { FileWatcher } from "../file/watcher"
2026-03-26 00:19:24 +00:00
import { Format } from "../format"
2025-06-27 15:29:20 +00:00
import { FileTime } from "../file/time"
2026-04-10 20:57:12 +00:00
import { AppFileSystem } from "../filesystem"
2025-09-01 21:15:49 +00:00
import { Instance } from "../project/instance"
2026-01-01 22:54:11 +00:00
import { trimDiff } from "./edit"
2026-04-10 20:57:12 +00:00
import { assertExternalDirectoryEffect } from "./external-directory"
2025-06-04 17:33:25 +00:00
2025-12-14 01:56:26 +00:00
const MAX_DIAGNOSTICS_PER_FILE = 20
const MAX_PROJECT_DIAGNOSTICS_FILES = 5
2026-04-10 20:57:12 +00:00
export const WriteTool = Tool . defineEffect (
"write" ,
Effect . gen ( function * ( ) {
const lsp = yield * LSP . Service
const fs = yield * AppFileSystem . Service
const filetime = yield * FileTime . Service
2025-06-04 17:33:25 +00:00
2026-04-10 20:57:12 +00:00
return {
description : DESCRIPTION ,
parameters : z.object ( {
content : z.string ( ) . describe ( "The content to write to the file" ) ,
filePath : z.string ( ) . describe ( "The absolute path to the file to write (must be absolute, not relative)" ) ,
} ) ,
execute : ( params : { content : string ; filePath : string } , ctx : Tool.Context ) = >
Effect . gen ( function * ( ) {
2026-04-10 21:01:33 +00:00
const filepath = path . isAbsolute ( params . filePath )
? params . filePath
: path . join ( Instance . directory , params . filePath )
2026-04-10 20:57:12 +00:00
yield * assertExternalDirectoryEffect ( ctx , filepath )
2025-06-04 17:33:25 +00:00
2026-04-10 20:57:12 +00:00
const exists = yield * fs . existsSafe ( filepath )
const contentOld = exists ? yield * fs . readFileString ( filepath ) : ""
if ( exists ) yield * filetime . assert ( ctx . sessionID , filepath )
2025-06-04 17:33:25 +00:00
2026-04-10 20:57:12 +00:00
const diff = trimDiff ( createTwoFilesPatch ( filepath , filepath , contentOld , params . content ) )
yield * Effect . promise ( ( ) = >
ctx . ask ( {
permission : "edit" ,
patterns : [ path . relative ( Instance . worktree , filepath ) ] ,
always : [ "*" ] ,
metadata : {
filepath ,
diff ,
} ,
} ) ,
)
2025-06-04 17:33:25 +00:00
2026-04-10 20:57:12 +00:00
yield * fs . writeWithDirs ( filepath , params . content )
yield * Effect . promise ( ( ) = > Format . file ( filepath ) )
Bus . publish ( File . Event . Edited , { file : filepath } )
yield * Effect . promise ( ( ) = >
Bus . publish ( FileWatcher . Event . Updated , {
file : filepath ,
event : exists ? "change" : "add" ,
} ) ,
)
yield * filetime . read ( ctx . sessionID , filepath )
2025-06-04 17:33:25 +00:00
2026-04-10 20:57:12 +00:00
let output = "Wrote file successfully."
yield * lsp . touchFile ( filepath , true )
const diagnostics = yield * lsp . diagnostics ( )
const normalizedFilepath = AppFileSystem . normalizePath ( filepath )
let projectDiagnosticsCount = 0
for ( const [ file , issues ] of Object . entries ( diagnostics ) ) {
const errors = issues . filter ( ( item ) = > item . severity === 1 )
if ( errors . length === 0 ) continue
const limited = errors . slice ( 0 , MAX_DIAGNOSTICS_PER_FILE )
const suffix =
2026-04-10 21:01:33 +00:00
errors . length > MAX_DIAGNOSTICS_PER_FILE
? ` \ n... and ${ errors . length - MAX_DIAGNOSTICS_PER_FILE } more `
: ""
2026-04-10 20:57:12 +00:00
if ( file === normalizedFilepath ) {
output += ` \ n \ nLSP errors detected in this file, please fix: \ n<diagnostics file=" ${ filepath } "> \ n ${ limited . map ( LSP . Diagnostic . pretty ) . join ( "\n" ) } ${ suffix } \ n</diagnostics> `
continue
}
if ( projectDiagnosticsCount >= MAX_PROJECT_DIAGNOSTICS_FILES ) continue
projectDiagnosticsCount ++
output += ` \ n \ nLSP errors detected in other files: \ n<diagnostics file=" ${ file } "> \ n ${ limited . map ( LSP . Diagnostic . pretty ) . join ( "\n" ) } ${ suffix } \ n</diagnostics> `
}
return {
title : path.relative ( Instance . worktree , filepath ) ,
metadata : {
diagnostics ,
filepath ,
exists : exists ,
} ,
output ,
}
} ) . pipe ( Effect . orDie , Effect . runPromise ) ,
2025-06-04 17:33:25 +00:00
}
2026-04-10 20:57:12 +00:00
} ) ,
)