@@ -8,6 +8,7 @@ import { ACPClient } from "lib/acp/client";
88import acpHistory from "lib/acp/history" ;
99import { ConnectionState } from "lib/acp/models" ;
1010import actionStack from "lib/actionStack" ;
11+ import { addedFolder } from "lib/openFolder" ;
1112import mimeType from "mime-types" ;
1213import FileBrowser from "pages/fileBrowser" ;
1314import helpers from "utils/helpers" ;
@@ -31,10 +32,12 @@ export default function AcpPageInclude() {
3132 let pendingTurnIndicatorElement = null ;
3233 let isPrompting = false ;
3334 let activePromptSessionId = null ;
35+ const BROWSE_CWD_OPTION = "__acp_cwd_browse__" ;
3436
3537 // ─── Connection Form ───
3638 const $form = AgentForm ( {
3739 onConnect : handleConnect ,
40+ onPickCwd : handlePickWorkingDirectory ,
3841 statusMsg : "" ,
3942 isConnecting : false ,
4043 } ) ;
@@ -57,7 +60,7 @@ export default function AcpPageInclude() {
5760 async function handleConnect ( { url, cwd } ) {
5861 if ( ! url ) return ;
5962
60- const nextCwd = cwd || "" ;
63+ const nextCwd = normalizeSessionCwd ( cwd || "" ) ;
6164 $form . setValues ( { url, cwd : nextCwd } ) ;
6265 $form . setConnecting ( true ) ;
6366 setFormStatus ( "" ) ;
@@ -77,6 +80,168 @@ export default function AcpPageInclude() {
7780 }
7881 }
7982
83+ function getTerminalPaths ( ) {
84+ const packageName = window . BuildInfo ?. packageName || "com.foxdebug.acode" ;
85+ const dataDir = `/data/user/0/${ packageName } ` ;
86+ return {
87+ alpineRoot : `${ dataDir } /files/alpine` ,
88+ publicDir : `${ dataDir } /files/public` ,
89+ } ;
90+ }
91+
92+ function normalizePathInput ( value = "" ) {
93+ return String ( value || "" )
94+ . trim ( )
95+ . replace ( / ^ < | > $ / g, "" )
96+ . replace ( / ^ [ " ' ] | [ " ' ] $ / g, "" ) ;
97+ }
98+
99+ function isTerminalPublicSafUri ( value = "" ) {
100+ return value . startsWith ( "content://com.foxdebug.acode.documents/tree/" ) ;
101+ }
102+
103+ function convertToTerminalCwd ( value = "" , allowRawFallback = false ) {
104+ const normalized = normalizePathInput ( value ) ;
105+ if ( ! normalized ) return "" ;
106+
107+ if ( normalized === "~" ) return "/home" ;
108+ if ( normalized . startsWith ( "~/" ) ) return `/home/${ normalized . slice ( 2 ) } ` ;
109+ if ( normalized === "/home" || normalized . startsWith ( "/home/" ) ) {
110+ return normalized ;
111+ }
112+ if ( normalized === "/public" || normalized . startsWith ( "/public/" ) ) {
113+ return normalized ;
114+ }
115+ if ( isTerminalPublicSafUri ( normalized ) ) {
116+ return "/public" ;
117+ }
118+
119+ const protocol = Url . getProtocol ( normalized ) ;
120+ if ( protocol && protocol !== "file:" ) {
121+ return allowRawFallback ? normalized : "" ;
122+ }
123+
124+ const { alpineRoot, publicDir } = getTerminalPaths ( ) ;
125+ const cleanValue = normalized . replace ( / ^ f i l e : \/ \/ / , "" ) ;
126+ if ( cleanValue . startsWith ( publicDir ) ) {
127+ const suffix = cleanValue . slice ( publicDir . length ) ;
128+ return suffix ? `/public${ suffix } ` : "/public" ;
129+ }
130+ if ( cleanValue . startsWith ( alpineRoot ) ) {
131+ const suffix = cleanValue . slice ( alpineRoot . length ) ;
132+ return suffix ? ( suffix . startsWith ( "/" ) ? suffix : `/${ suffix } ` ) : "/" ;
133+ }
134+ if (
135+ cleanValue . startsWith ( "/sdcard" ) ||
136+ cleanValue . startsWith ( "/storage" ) ||
137+ cleanValue . startsWith ( "/data" )
138+ ) {
139+ return cleanValue ;
140+ }
141+
142+ return allowRawFallback ? normalized : "" ;
143+ }
144+
145+ function normalizeSessionCwd ( value = "" ) {
146+ return convertToTerminalCwd ( value , true ) ;
147+ }
148+
149+ function toFolderLabel ( folder = { } ) {
150+ const title = normalizePathInput ( folder . title || "" ) ;
151+ if ( title ) return title ;
152+ const url = normalizePathInput ( folder . url || "" ) ;
153+ return Url . basename ( url ) || url || "Folder" ;
154+ }
155+
156+ function getDirectorySelectionItems ( currentCwd = "" ) {
157+ const items = [
158+ {
159+ value : BROWSE_CWD_OPTION ,
160+ text : "Browse folder…" ,
161+ icon : "folder_open" ,
162+ } ,
163+ ] ;
164+ const seenValues = new Set ( [ BROWSE_CWD_OPTION ] ) ;
165+ const normalizedCurrent = normalizeSessionCwd ( currentCwd ) ;
166+
167+ const pushItem = ( value , text , icon = "folder" ) => {
168+ if ( ! value || seenValues . has ( value ) ) return ;
169+ seenValues . add ( value ) ;
170+ items . push ( {
171+ value,
172+ text,
173+ icon,
174+ } ) ;
175+ } ;
176+
177+ if ( normalizedCurrent ) {
178+ const currentIsTerminalAccessible = Boolean (
179+ convertToTerminalCwd ( normalizedCurrent , false ) ,
180+ ) ;
181+ pushItem (
182+ normalizedCurrent ,
183+ currentIsTerminalAccessible
184+ ? `Current value<br><small>${ normalizedCurrent } </small>`
185+ : `Current value<br><small>${ normalizedCurrent } • terminal unavailable</small>` ,
186+ currentIsTerminalAccessible ? "radio_button_checked" : "warning" ,
187+ ) ;
188+ }
189+
190+ addedFolder . forEach ( ( folder ) => {
191+ const rawUrl = normalizePathInput ( folder ?. url || "" ) ;
192+ if ( ! rawUrl ) return ;
193+
194+ const converted = convertToTerminalCwd ( rawUrl , false ) ;
195+ const cwdValue = converted || normalizeSessionCwd ( rawUrl ) ;
196+ if ( ! cwdValue ) return ;
197+ const label = toFolderLabel ( folder ) ;
198+ pushItem (
199+ cwdValue ,
200+ converted
201+ ? `${ label } <br><small>${ cwdValue } </small>`
202+ : `${ label } <br><small>${ cwdValue } • terminal unavailable</small>` ,
203+ converted ? "folder" : "warning" ,
204+ ) ;
205+ } ) ;
206+
207+ return items ;
208+ }
209+
210+ async function handlePickWorkingDirectory ( currentCwd = "" ) {
211+ try {
212+ const selected = await select (
213+ "Select Working Directory" ,
214+ getDirectorySelectionItems ( currentCwd ) ,
215+ {
216+ textTransform : false ,
217+ } ,
218+ ) ;
219+ if ( ! selected ) return null ;
220+
221+ if ( selected === BROWSE_CWD_OPTION ) {
222+ const folder = await FileBrowser ( "folder" , "Select working directory" ) ;
223+ const nextCwd = normalizeSessionCwd ( folder ?. url || "" ) ;
224+ if ( ! nextCwd ) {
225+ toast ( "Failed to resolve selected folder" ) ;
226+ return null ;
227+ }
228+ if ( ! convertToTerminalCwd ( folder ?. url || "" , false ) ) {
229+ toast (
230+ "Selected folder supports ACP file access, but terminal tools may be unavailable" ,
231+ ) ;
232+ }
233+ return nextCwd ;
234+ }
235+
236+ return normalizeSessionCwd ( selected ) ;
237+ } catch ( error ) {
238+ if ( ! error ) return null ;
239+ console . error ( "[ACP] Failed to pick working directory:" , error ) ;
240+ toast ( error ?. message || "Failed to choose working directory" ) ;
241+ return null ;
242+ }
243+ }
244+
80245 async function ensureReadyForUrl ( url ) {
81246 if ( client . state === ConnectionState . READY && connectedUrl === url ) return ;
82247
@@ -1018,7 +1183,7 @@ export default function AcpPageInclude() {
10181183 }
10191184
10201185 async function loadSelectedSession ( entry ) {
1021- const cwd = entry . cwd || $form . getValues ( ) . cwd || "" ;
1186+ const cwd = normalizeSessionCwd ( entry . cwd || $form . getValues ( ) . cwd || "" ) ;
10221187 if ( ! cwd ) {
10231188 setFormStatus ( "This session is missing a working directory" ) ;
10241189 return ;
0 commit comments