11import "./acp.scss" ;
22import fsOperation from "fileSystem" ;
3+ import { RequestError } from "@agentclientprotocol/sdk" ;
34import Page from "components/page" ;
45import toast from "components/toast" ;
56import select from "dialogs/select" ;
@@ -33,6 +34,10 @@ export default function AcpPageInclude() {
3334 let isPrompting = false ;
3435 let activePromptSessionId = null ;
3536 const BROWSE_CWD_OPTION = "__acp_cwd_browse__" ;
37+ const ACP_FS_READ_TEXT_FILE = "fs/read_text_file" ;
38+ const ACP_FS_WRITE_TEXT_FILE = "fs/write_text_file" ;
39+
40+ registerFilesystemHandlers ( ) ;
3641
3742 // ─── Connection Form ───
3843 const $form = AgentForm ( {
@@ -60,7 +65,7 @@ export default function AcpPageInclude() {
6065 async function handleConnect ( { url, cwd } ) {
6166 if ( ! url ) return ;
6267
63- const nextCwd = normalizeSessionCwd ( cwd || "" ) ;
68+ const nextCwd = normalizeSessionCwd ( cwd || "/home " ) ;
6469 $form . setValues ( { url, cwd : nextCwd } ) ;
6570 $form . setConnecting ( true ) ;
6671 setFormStatus ( "" ) ;
@@ -84,6 +89,7 @@ export default function AcpPageInclude() {
8489 const packageName = window . BuildInfo ?. packageName || "com.foxdebug.acode" ;
8590 const dataDir = `/data/user/0/${ packageName } ` ;
8691 return {
92+ dataDir,
8793 alpineRoot : `${ dataDir } /files/alpine` ,
8894 publicDir : `${ dataDir } /files/public` ,
8995 } ;
@@ -146,6 +152,251 @@ export default function AcpPageInclude() {
146152 return convertToTerminalCwd ( value , true ) ;
147153 }
148154
155+ function getSessionCwdForFs ( sessionId = "" ) {
156+ const activeSession = client . session ;
157+ if ( activeSession ?. sessionId === sessionId ) {
158+ return normalizeSessionCwd ( activeSession . cwd || "" ) ;
159+ }
160+ return normalizeSessionCwd ( $form . getValues ( ) . cwd || "" ) ;
161+ }
162+
163+ function resolveAgentPath ( path = "" , sessionId = "" ) {
164+ const normalizedPath = normalizePathInput ( path ) ;
165+ if ( ! normalizedPath ) return "" ;
166+
167+ const sessionCwd = getSessionCwdForFs ( sessionId ) ;
168+ const protocol = Url . getProtocol ( normalizedPath ) ;
169+
170+ if ( protocol ) {
171+ if ( protocol === "file:" ) {
172+ return normalizedPath ;
173+ }
174+ if (
175+ protocol === "content:" ||
176+ protocol === "ftp:" ||
177+ protocol === "sftp:" ||
178+ protocol === "http:" ||
179+ protocol === "https:"
180+ ) {
181+ return normalizedPath ;
182+ }
183+ return "" ;
184+ }
185+
186+ const agentPath = normalizedPath . startsWith ( "/" )
187+ ? normalizedPath
188+ : sessionCwd
189+ ? Url . join ( sessionCwd , normalizedPath )
190+ : "" ;
191+ if ( ! agentPath ) return "" ;
192+
193+ const { alpineRoot, publicDir } = getTerminalPaths ( ) ;
194+ if ( agentPath === "~" ) {
195+ return `file://${ alpineRoot } /home` ;
196+ }
197+ if ( agentPath . startsWith ( "~/" ) ) {
198+ return `file://${ alpineRoot } /home/${ agentPath . slice ( 2 ) } ` ;
199+ }
200+ if ( agentPath === "/public" || agentPath . startsWith ( "/public/" ) ) {
201+ const suffix = agentPath . slice ( "/public" . length ) ;
202+ return `file://${ publicDir } ${ suffix } ` ;
203+ }
204+ if ( agentPath === "/home" || agentPath . startsWith ( "/home/" ) ) {
205+ return `file://${ alpineRoot } ${ agentPath } ` ;
206+ }
207+ if (
208+ agentPath . startsWith ( "/sdcard" ) ||
209+ agentPath . startsWith ( "/storage" ) ||
210+ agentPath . startsWith ( "/data" )
211+ ) {
212+ return `file://${ agentPath } ` ;
213+ }
214+ if ( agentPath . startsWith ( "/" ) ) {
215+ return `file://${ alpineRoot } ${ agentPath } ` ;
216+ }
217+
218+ return "" ;
219+ }
220+
221+ function normalizeFsReadRange ( value , { name, min = 1 } = { } ) {
222+ if ( value == null ) return null ;
223+ const num = Number ( value ) ;
224+ if ( ! Number . isInteger ( num ) || num < min ) {
225+ throw RequestError . invalidParams (
226+ { } ,
227+ `${ name || "value" } must be an integer >= ${ min } ` ,
228+ ) ;
229+ }
230+ return num ;
231+ }
232+
233+ function sliceTextByLineRange ( text = "" , line , limit ) {
234+ if ( line == null && limit == null ) return text ;
235+ const allLines = String ( text ) . split ( / \r \n | \n | \r / ) ;
236+ const startLine = line || 1 ;
237+ if ( startLine > allLines . length ) return "" ;
238+ if ( limit === 0 ) return "" ;
239+ if ( limit == null ) {
240+ return allLines . slice ( startLine - 1 ) . join ( "\n" ) ;
241+ }
242+ return allLines . slice ( startLine - 1 , startLine - 1 + limit ) . join ( "\n" ) ;
243+ }
244+
245+ function getOpenEditorFile ( uri = "" ) {
246+ const manager = window . editorManager ;
247+ if ( ! manager ?. getFile || ! uri ) return null ;
248+ const candidates = [ uri ] ;
249+ try {
250+ const decoded = decodeURIComponent ( uri ) ;
251+ if ( decoded && ! candidates . includes ( decoded ) ) candidates . push ( decoded ) ;
252+ } catch {
253+ /* ignore */
254+ }
255+
256+ for ( const candidate of candidates ) {
257+ const file = manager . getFile ( candidate , "uri" ) ;
258+ if ( file ) return file ;
259+ }
260+ return null ;
261+ }
262+
263+ async function readFileTextFromFs ( resolvedPath = "" ) {
264+ const openFileRef = getOpenEditorFile ( resolvedPath ) ;
265+ const unsavedContent = openFileRef ?. session ?. getValue ?. ( ) ;
266+ if ( typeof unsavedContent === "string" ) {
267+ return unsavedContent ;
268+ }
269+
270+ const content = await fsOperation ( resolvedPath ) . readFile ( "utf8" ) ;
271+ if ( typeof content === "string" ) return content ;
272+ if ( content instanceof ArrayBuffer ) {
273+ return new TextDecoder ( ) . decode ( content ) ;
274+ }
275+ return String ( content ?? "" ) ;
276+ }
277+
278+ async function writeFileTextToFs ( resolvedPath = "" , content = "" ) {
279+ const targetFs = fsOperation ( resolvedPath ) ;
280+ const exists = await targetFs . exists ( ) ;
281+ if ( exists ) {
282+ await targetFs . writeFile ( content , "utf8" ) ;
283+ } else {
284+ const parentPath = Url . dirname ( resolvedPath ) ;
285+ const filename = Url . basename ( resolvedPath ) ;
286+ if ( ! parentPath || ! filename ) {
287+ throw RequestError . invalidParams (
288+ { } ,
289+ `Invalid file path: ${ resolvedPath } ` ,
290+ ) ;
291+ }
292+ await fsOperation ( parentPath ) . createFile ( filename , content ) ;
293+ }
294+
295+ const openFileRef = getOpenEditorFile ( resolvedPath ) ;
296+ if ( openFileRef ?. type === "editor" ) {
297+ openFileRef . session ?. setValue ?. ( content ) ;
298+ openFileRef . isUnsaved = false ;
299+ openFileRef . markChanged = false ;
300+ await openFileRef . writeToCache ?. ( ) ;
301+ }
302+ }
303+
304+ function assertValidSessionRequest ( sessionId = "" ) {
305+ const activeSessionId = client . session ?. sessionId ;
306+ if ( ! sessionId || ! activeSessionId || sessionId !== activeSessionId ) {
307+ throw RequestError . invalidParams ( { } , "Invalid or inactive sessionId" ) ;
308+ }
309+ }
310+
311+ function toFsError ( error , requestPath = "" ) {
312+ const message = String ( error ?. message || error || "" ) ;
313+ if ( error instanceof RequestError ) {
314+ return error ;
315+ }
316+ if (
317+ / n o t f o u n d | n o s u c h f i l e | p a t h n o t f o u n d | d o e s n o t e x i s t | f a i l e d t o r e s o l v e / i. test (
318+ message ,
319+ )
320+ ) {
321+ return RequestError . resourceNotFound ( requestPath || undefined ) ;
322+ }
323+ return RequestError . internalError (
324+ { } ,
325+ message || "Filesystem operation failed" ,
326+ ) ;
327+ }
328+
329+ function registerFilesystemHandlers ( ) {
330+ client . registerRequestHandler (
331+ ACP_FS_READ_TEXT_FILE ,
332+ async ( params = { } ) => {
333+ try {
334+ const sessionId = String ( params ?. sessionId || "" ) ;
335+ assertValidSessionRequest ( sessionId ) ;
336+
337+ const rawPath = normalizePathInput ( params ?. path || "" ) ;
338+ if ( ! rawPath ) {
339+ throw RequestError . invalidParams ( { } , "path is required" ) ;
340+ }
341+
342+ const line = normalizeFsReadRange ( params ?. line , {
343+ name : "line" ,
344+ min : 1 ,
345+ } ) ;
346+ const limit = normalizeFsReadRange ( params ?. limit , {
347+ name : "limit" ,
348+ min : 0 ,
349+ } ) ;
350+ const resolvedPath = resolveAgentPath ( rawPath , sessionId ) ;
351+ if ( ! resolvedPath ) {
352+ throw RequestError . invalidParams (
353+ { } ,
354+ `Unsupported filesystem path: ${ rawPath } ` ,
355+ ) ;
356+ }
357+
358+ const text = await readFileTextFromFs ( resolvedPath ) ;
359+ return {
360+ content : sliceTextByLineRange ( text , line , limit ) ,
361+ } ;
362+ } catch ( error ) {
363+ throw toFsError ( error , params ?. path ) ;
364+ }
365+ } ,
366+ ) ;
367+
368+ client . registerRequestHandler (
369+ ACP_FS_WRITE_TEXT_FILE ,
370+ async ( params = { } ) => {
371+ try {
372+ const sessionId = String ( params ?. sessionId || "" ) ;
373+ assertValidSessionRequest ( sessionId ) ;
374+
375+ const rawPath = normalizePathInput ( params ?. path || "" ) ;
376+ if ( ! rawPath ) {
377+ throw RequestError . invalidParams ( { } , "path is required" ) ;
378+ }
379+ if ( typeof params ?. content !== "string" ) {
380+ throw RequestError . invalidParams ( { } , "content must be a string" ) ;
381+ }
382+
383+ const resolvedPath = resolveAgentPath ( rawPath , sessionId ) ;
384+ if ( ! resolvedPath ) {
385+ throw RequestError . invalidParams (
386+ { } ,
387+ `Unsupported filesystem path: ${ rawPath } ` ,
388+ ) ;
389+ }
390+
391+ await writeFileTextToFs ( resolvedPath , params . content ) ;
392+ return { } ;
393+ } catch ( error ) {
394+ throw toFsError ( error , params ?. path ) ;
395+ }
396+ } ,
397+ ) ;
398+ }
399+
149400 function toFolderLabel ( folder = { } ) {
150401 const title = normalizePathInput ( folder . title || "" ) ;
151402 if ( title ) return title ;
@@ -1183,7 +1434,9 @@ export default function AcpPageInclude() {
11831434 }
11841435
11851436 async function loadSelectedSession ( entry ) {
1186- const cwd = normalizeSessionCwd ( entry . cwd || $form . getValues ( ) . cwd || "" ) ;
1437+ const cwd = normalizeSessionCwd (
1438+ entry . cwd || $form . getValues ( ) . cwd || "/home" ,
1439+ ) ;
11871440 if ( ! cwd ) {
11881441 setFormStatus ( "This session is missing a working directory" ) ;
11891442 return ;
0 commit comments