Skip to content

Commit afc6962

Browse files
committed
feat: picker to select the cwd and auto path translation
1 parent 7705b74 commit afc6962

File tree

3 files changed

+237
-4
lines changed

3 files changed

+237
-4
lines changed

src/pages/acp/acp.js

Lines changed: 167 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ACPClient } from "lib/acp/client";
88
import acpHistory from "lib/acp/history";
99
import { ConnectionState } from "lib/acp/models";
1010
import actionStack from "lib/actionStack";
11+
import { addedFolder } from "lib/openFolder";
1112
import mimeType from "mime-types";
1213
import FileBrowser from "pages/fileBrowser";
1314
import 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(/^file:\/\//, "");
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;

src/pages/acp/acp.scss

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,47 @@
100100
color-mix(in srgb, var(--active-color), transparent 85%);
101101
}
102102
}
103+
104+
.acp-cwd-input-row {
105+
display: flex;
106+
align-items: center;
107+
gap: 8px;
108+
109+
input {
110+
flex: 1;
111+
min-width: 0;
112+
}
113+
}
114+
115+
.acp-cwd-pick-btn {
116+
width: 44px;
117+
height: 44px;
118+
display: flex;
119+
align-items: center;
120+
justify-content: center;
121+
border-radius: 10px;
122+
border: 1px solid var(--border-color);
123+
background: var(--popup-background-color);
124+
color: var(--popup-text-color);
125+
cursor: pointer;
126+
transition:
127+
border-color 0.2s,
128+
background-color 0.2s;
129+
130+
&:active {
131+
border-color: var(--active-color);
132+
background: color-mix(
133+
in srgb,
134+
var(--active-color),
135+
transparent 90%
136+
);
137+
}
138+
139+
&:disabled {
140+
opacity: 0.4;
141+
pointer-events: none;
142+
}
143+
}
103144
}
104145

105146
.acp-connect-btn {

src/pages/acp/components/agentForm.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
export default function AgentForm({ onConnect, statusMsg, isConnecting }) {
1+
export default function AgentForm({
2+
onConnect,
3+
onPickCwd,
4+
statusMsg,
5+
isConnecting,
6+
}) {
27
const $urlInput = (
38
<input
49
type="text"
@@ -11,6 +16,24 @@ export default function AgentForm({ onConnect, statusMsg, isConnecting }) {
1116
<input type="text" placeholder="e.g. /home/user/project (optional)" />
1217
);
1318

19+
const $cwdPickBtn = (
20+
<button
21+
type="button"
22+
className="acp-cwd-pick-btn"
23+
title="Select working directory"
24+
disabled={isConnecting}
25+
onclick={async () => {
26+
if (typeof onPickCwd !== "function") return;
27+
const selectedCwd = await onPickCwd($cwdInput.value.trim());
28+
if (typeof selectedCwd === "string" && selectedCwd.trim()) {
29+
$cwdInput.value = selectedCwd.trim();
30+
}
31+
}}
32+
>
33+
<i className="icon folder_open"></i>
34+
</button>
35+
);
36+
1437
const $btn = (
1538
<button
1639
className={`acp-connect-btn${isConnecting ? " connecting" : ""}`}
@@ -44,7 +67,10 @@ export default function AgentForm({ onConnect, statusMsg, isConnecting }) {
4467
</div>
4568
<div className="acp-field">
4669
<label>Working Directory</label>
47-
{$cwdInput}
70+
<div className="acp-cwd-input-row">
71+
{$cwdInput}
72+
{$cwdPickBtn}
73+
</div>
4874
</div>
4975
{$btn}
5076
</div>
@@ -56,6 +82,7 @@ export default function AgentForm({ onConnect, statusMsg, isConnecting }) {
5682
$btn.disabled = connecting;
5783
$btn.className = `acp-connect-btn${connecting ? " connecting" : ""}`;
5884
$btn.textContent = connecting ? "" : "Connect";
85+
$cwdPickBtn.disabled = connecting;
5986
};
6087

6188
$el.setStatus = (msg) => {

0 commit comments

Comments
 (0)