fix(ui): support remote working directory for external backend (#10827)

Co-authored-by: saixin <saixin.me@outlook.com>
This commit is contained in:
saime428
2026-08-10 00:16:28 -10:00
committed by GitHub
parent 5e7430fefa
commit d1089ed269
21 changed files with 213 additions and 3 deletions
@@ -34,6 +34,19 @@ const i18n = defineMessages({
defaultMessage:
'Enter the HTTP(S) base URL. Goose checks /status and connects to /acp under this base.',
},
workingDir: {
id: 'externalBackendSection.workingDir',
defaultMessage: 'Remote Working Directory (optional)',
},
workingDirPlaceholder: {
id: 'externalBackendSection.workingDirPlaceholder',
defaultMessage: '/home/goose/workspace',
},
workingDirHelp: {
id: 'externalBackendSection.workingDirHelp',
defaultMessage:
'Absolute path on the external backend. Leave blank to send the local working directory.',
},
secretKey: {
id: 'externalBackendSection.secretKey',
defaultMessage: 'Secret Key',
@@ -222,6 +235,24 @@ export default function ExternalBackendSection() {
</p>
</div>
<div className="space-y-2">
<label htmlFor="external-working-dir" className="text-text-primary text-xs">
{intl.formatMessage(i18n.workingDir)}
</label>
<Input
id="external-working-dir"
type="text"
placeholder={intl.formatMessage(i18n.workingDirPlaceholder)}
value={config.workingDir || ''}
onChange={(e) => updateField('workingDir', e.target.value)}
onBlur={() => saveConfig(config)}
disabled={isSaving}
/>
<p className="text-xs text-text-secondary">
{intl.formatMessage(i18n.workingDirHelp)}
</p>
</div>
<div className="space-y-2">
<label htmlFor="external-secret" className="text-text-primary text-xs">
{intl.formatMessage(i18n.secretKey)}
+9
View File
@@ -1226,6 +1226,15 @@
"externalBackendSection.useExternalServerDescription": {
"defaultMessage": "Verbindung zu einem anderswo laufenden goose-Server herstellen (erfordert Neustart der App)"
},
"externalBackendSection.workingDir": {
"defaultMessage": "Remote-Arbeitsverzeichnis (optional)"
},
"externalBackendSection.workingDirHelp": {
"defaultMessage": "Absoluter Pfad auf dem externen Backend. Leer lassen, um das lokale Arbeitsverzeichnis zu senden."
},
"externalBackendSection.workingDirPlaceholder": {
"defaultMessage": "/home/goose/workspace"
},
"goosehintsModal.close": {
"defaultMessage": "Schließen"
},
+9
View File
@@ -1226,6 +1226,15 @@
"externalBackendSection.useExternalServerDescription": {
"defaultMessage": "Connect to an ACP-compatible backend running elsewhere."
},
"externalBackendSection.workingDir": {
"defaultMessage": "Remote Working Directory (optional)"
},
"externalBackendSection.workingDirHelp": {
"defaultMessage": "Absolute path on the external backend. Leave blank to send the local working directory."
},
"externalBackendSection.workingDirPlaceholder": {
"defaultMessage": "/home/goose/workspace"
},
"goosehintsModal.close": {
"defaultMessage": "Close"
},
+9
View File
@@ -1226,6 +1226,15 @@
"externalBackendSection.useExternalServerDescription": {
"defaultMessage": "Conéctate a un servidor goose que se ejecuta en otro lugar (requiere reiniciar la app)"
},
"externalBackendSection.workingDir": {
"defaultMessage": "Directorio de trabajo remoto (opcional)"
},
"externalBackendSection.workingDirHelp": {
"defaultMessage": "Ruta absoluta en el backend externo. Déjelo vacío para enviar el directorio de trabajo local."
},
"externalBackendSection.workingDirPlaceholder": {
"defaultMessage": "/home/goose/workspace"
},
"goosehintsModal.close": {
"defaultMessage": "Cerrar"
},
+9
View File
@@ -1226,6 +1226,15 @@
"externalBackendSection.useExternalServerDescription": {
"defaultMessage": "Se connecter à un serveur goose exécuté ailleurs (nécessite le redémarrage de l'application)"
},
"externalBackendSection.workingDir": {
"defaultMessage": "Répertoire de travail distant (facultatif)"
},
"externalBackendSection.workingDirHelp": {
"defaultMessage": "Chemin absolu sur le backend externe. Laissez vide pour envoyer le répertoire de travail local."
},
"externalBackendSection.workingDirPlaceholder": {
"defaultMessage": "/home/goose/workspace"
},
"goosehintsModal.close": {
"defaultMessage": "Fermer"
},
+9
View File
@@ -1226,6 +1226,15 @@
"externalBackendSection.useExternalServerDescription": {
"defaultMessage": "कहीं और चल रहे goose सर्वर से कनेक्ट करें (ऐप पुनरारंभ की आवश्यकता है)"
},
"externalBackendSection.workingDir": {
"defaultMessage": "रिमोट कार्य निर्देशिका (वैकल्पिक)"
},
"externalBackendSection.workingDirHelp": {
"defaultMessage": "बाहरी बैकएंड पर पूर्ण पथ। स्थानीय कार्य निर्देशिका भेजने के लिए खाली छोड़ दें।"
},
"externalBackendSection.workingDirPlaceholder": {
"defaultMessage": "/home/goose/workspace"
},
"goosehintsModal.close": {
"defaultMessage": "बंद करें"
},
+9
View File
@@ -1226,6 +1226,15 @@
"externalBackendSection.useExternalServerDescription": {
"defaultMessage": "Terhubung ke server goose yang berjalan di tempat lain (memerlukan mulai ulang aplikasi)"
},
"externalBackendSection.workingDir": {
"defaultMessage": "Direktori kerja jarak jauh (opsional)"
},
"externalBackendSection.workingDirHelp": {
"defaultMessage": "Jalur absolut pada backend eksternal. Biarkan kosong untuk mengirim direktori kerja lokal."
},
"externalBackendSection.workingDirPlaceholder": {
"defaultMessage": "/home/goose/workspace"
},
"goosehintsModal.close": {
"defaultMessage": "Tutup"
},
+9
View File
@@ -1226,6 +1226,15 @@
"externalBackendSection.useExternalServerDescription": {
"defaultMessage": "Connettiti a un server goose in esecuzione altrove (richiede il riavvio dell'app)"
},
"externalBackendSection.workingDir": {
"defaultMessage": "Directory di lavoro remota (facoltativa)"
},
"externalBackendSection.workingDirHelp": {
"defaultMessage": "Percorso assoluto sul backend esterno. Lascia vuoto per inviare la directory di lavoro locale."
},
"externalBackendSection.workingDirPlaceholder": {
"defaultMessage": "/home/goose/workspace"
},
"goosehintsModal.close": {
"defaultMessage": "Chiudi"
},
+9
View File
@@ -1226,6 +1226,15 @@
"externalBackendSection.useExternalServerDescription": {
"defaultMessage": "別の場所で実行中の goose サーバーに接続します(アプリの再起動が必要)"
},
"externalBackendSection.workingDir": {
"defaultMessage": "リモート作業ディレクトリ(任意)"
},
"externalBackendSection.workingDirHelp": {
"defaultMessage": "外部バックエンド上の絶対パスです。空欄の場合はローカルの作業ディレクトリを送信します。"
},
"externalBackendSection.workingDirPlaceholder": {
"defaultMessage": "/home/goose/workspace"
},
"goosehintsModal.close": {
"defaultMessage": "閉じる"
},
+9
View File
@@ -1226,6 +1226,15 @@
"externalBackendSection.useExternalServerDescription": {
"defaultMessage": "다른 곳에서 실행 중인 ACP 호환 백엔드에 연결합니다."
},
"externalBackendSection.workingDir": {
"defaultMessage": "원격 작업 디렉터리(선택 사항)"
},
"externalBackendSection.workingDirHelp": {
"defaultMessage": "외부 백엔드의 절대 경로입니다. 비워 두면 로컬 작업 디렉터리를 전송합니다."
},
"externalBackendSection.workingDirPlaceholder": {
"defaultMessage": "/home/goose/workspace"
},
"goosehintsModal.close": {
"defaultMessage": "닫기"
},
+9
View File
@@ -1226,6 +1226,15 @@
"externalBackendSection.useExternalServerDescription": {
"defaultMessage": "Sambung ke pelayan goose yang berjalan di tempat lain (memerlukan memulakan semula aplikasi)"
},
"externalBackendSection.workingDir": {
"defaultMessage": "Direktori kerja jauh (pilihan)"
},
"externalBackendSection.workingDirHelp": {
"defaultMessage": "Laluan mutlak pada backend luaran. Biarkan kosong untuk menghantar direktori kerja setempat."
},
"externalBackendSection.workingDirPlaceholder": {
"defaultMessage": "/home/goose/workspace"
},
"goosehintsModal.close": {
"defaultMessage": "Tutup"
},
+9
View File
@@ -1226,6 +1226,15 @@
"externalBackendSection.useExternalServerDescription": {
"defaultMessage": "Conecte-se a um servidor goose em execução em outro lugar (requer reinicialização do app)"
},
"externalBackendSection.workingDir": {
"defaultMessage": "Diretório de trabalho remoto (opcional)"
},
"externalBackendSection.workingDirHelp": {
"defaultMessage": "Caminho absoluto no backend externo. Deixe em branco para enviar o diretório de trabalho local."
},
"externalBackendSection.workingDirPlaceholder": {
"defaultMessage": "/home/goose/workspace"
},
"goosehintsModal.close": {
"defaultMessage": "Fechar"
},
+9
View File
@@ -1226,6 +1226,15 @@
"externalBackendSection.useExternalServerDescription": {
"defaultMessage": "Подключиться к серверу goose, запущенному в другом месте (требуется перезапуск приложения)"
},
"externalBackendSection.workingDir": {
"defaultMessage": "Удалённый рабочий каталог (необязательно)"
},
"externalBackendSection.workingDirHelp": {
"defaultMessage": "Абсолютный путь на внешнем бэкенде. Оставьте пустым, чтобы передать локальный рабочий каталог."
},
"externalBackendSection.workingDirPlaceholder": {
"defaultMessage": "/home/goose/workspace"
},
"goosehintsModal.close": {
"defaultMessage": "Закрыть"
},
+9
View File
@@ -1226,6 +1226,15 @@
"externalBackendSection.useExternalServerDescription": {
"defaultMessage": "Başka bir yerde çalışan bir goose sunucusuna bağlanın (uygulamanın yeniden başlatılmasını gerektirir)"
},
"externalBackendSection.workingDir": {
"defaultMessage": "Uzak çalışma dizini (isteğe bağlı)"
},
"externalBackendSection.workingDirHelp": {
"defaultMessage": "Harici arka uçtaki mutlak yol. Yerel çalışma dizinini göndermek için boş bırakın."
},
"externalBackendSection.workingDirPlaceholder": {
"defaultMessage": "/home/goose/workspace"
},
"goosehintsModal.close": {
"defaultMessage": "Kapat"
},
+9
View File
@@ -1226,6 +1226,15 @@
"externalBackendSection.useExternalServerDescription": {
"defaultMessage": "Kết nối tới một máy chủ goose đang chạy ở nơi khác (cần khởi động lại ứng dụng)"
},
"externalBackendSection.workingDir": {
"defaultMessage": "Thư mục làm việc từ xa (tùy chọn)"
},
"externalBackendSection.workingDirHelp": {
"defaultMessage": "Đường dẫn tuyệt đối trên backend bên ngoài. Để trống để gửi thư mục làm việc cục bộ."
},
"externalBackendSection.workingDirPlaceholder": {
"defaultMessage": "/home/goose/workspace"
},
"goosehintsModal.close": {
"defaultMessage": "Đóng"
},
+9
View File
@@ -1226,6 +1226,15 @@
"externalBackendSection.useExternalServerDescription": {
"defaultMessage": "连接到运行在其他位置的 goose 服务器(需要重启应用)"
},
"externalBackendSection.workingDir": {
"defaultMessage": "远程工作目录(可选)"
},
"externalBackendSection.workingDirHelp": {
"defaultMessage": "外部后端上的绝对路径。留空时将发送本地工作目录。"
},
"externalBackendSection.workingDirPlaceholder": {
"defaultMessage": "/home/goose/workspace"
},
"goosehintsModal.close": {
"defaultMessage": "关闭"
},
+9
View File
@@ -1226,6 +1226,15 @@
"externalBackendSection.useExternalServerDescription": {
"defaultMessage": "連線至在其他位置執行的 goose 伺服器(需要重新啟動應用程式)"
},
"externalBackendSection.workingDir": {
"defaultMessage": "遠端工作目錄(選用)"
},
"externalBackendSection.workingDirHelp": {
"defaultMessage": "外部後端上的絕對路徑。留空時將傳送本機工作目錄。"
},
"externalBackendSection.workingDirPlaceholder": {
"defaultMessage": "/home/goose/workspace"
},
"goosehintsModal.close": {
"defaultMessage": "關閉"
},
+19 -3
View File
@@ -56,6 +56,7 @@ import type { GooseApp } from './types/apps';
import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer';
import { BLOCKED_PROTOCOLS, WEB_PROTOCOLS } from './utils/urlSecurity';
import { buildCSP } from './utils/csp';
import { resolveWorkingDir } from './utils/workingDir';
function shouldSetupUpdater(): boolean {
// Setup updater if either the flag is enabled OR dev updates are enabled
@@ -890,6 +891,7 @@ interface ExternalBackend {
url: string;
secret: string;
certFingerprint?: string;
workingDir?: string;
}
const getExternalBackendUrlFromEnv = (): string | null => {
@@ -936,7 +938,10 @@ const getServerSecret = (settings: Settings): string => {
const getActiveExternalBackend = (settings: Settings): ExternalBackend | null => {
const envBackend = getExternalBackendFromEnv();
if (envBackend) {
return envBackend;
return {
...envBackend,
workingDir: settings.externalGoosed?.workingDir,
};
}
if (settings.externalGoosed?.enabled && settings.externalGoosed.url) {
@@ -945,6 +950,7 @@ const getActiveExternalBackend = (settings: Settings): ExternalBackend | null =>
url: settings.externalGoosed.url,
secret: getServerSecret(settings),
certFingerprint: settings.externalGoosed.certFingerprint,
workingDir: settings.externalGoosed.workingDir,
};
}
@@ -1068,7 +1074,7 @@ const createChat = async (
}
const serverSecret = externalBackend ? externalBackend.secret : GENERATED_SECRET;
let workingDir = dir || os.homedir();
let workingDir = resolveWorkingDir(externalBackend?.workingDir, dir, os.homedir());
let gooseServeLease: GooseServeLease | null = null;
if (externalBackend) {
@@ -3003,7 +3009,17 @@ async function appMain() {
throw new Error('No backend lease found for launching window');
}
const workingDir = app.getPath('home');
const launchingWorkingDir = await launchingWindow.webContents
.executeJavaScript(`window.appConfig ? window.appConfig.get('GOOSE_WORKING_DIR') : null`)
.catch((error) => {
console.warn('Failed to get working directory from launching window:', error);
return undefined;
});
const workingDir = resolveWorkingDir(
typeof launchingWorkingDir === 'string' ? launchingWorkingDir : undefined,
undefined,
app.getPath('home')
);
const appWindow = new BrowserWindow({
title: formatAppName(gooseApp.name),
width: gooseApp.width ?? 800,
@@ -0,0 +1,12 @@
import { describe, expect, it } from 'vitest';
import { resolveWorkingDir } from '../workingDir';
describe('resolveWorkingDir', () => {
it('uses the configured external backend directory when present', () => {
expect(resolveWorkingDir(' /home/goose ', 'C:\\Users\\goose', 'C:\\Users\\goose')).toBe(
'/home/goose'
);
expect(resolveWorkingDir(' ', 'C:\\work', 'C:\\Users\\goose')).toBe('C:\\work');
expect(resolveWorkingDir(undefined, undefined, 'C:\\Users\\goose')).toBe('C:\\Users\\goose');
});
});
+1
View File
@@ -3,6 +3,7 @@ export interface ExternalBackendConfig {
url: string;
secret: string;
certFingerprint?: string;
workingDir?: string;
}
export interface KeyboardShortcuts {
+6
View File
@@ -2,3 +2,9 @@ export const getInitialWorkingDir = (): string => {
// Fall back to initial config from app startup
return (window.appConfig?.get('GOOSE_WORKING_DIR') as string) ?? '';
};
export const resolveWorkingDir = (
externalWorkingDir: string | undefined,
requestedWorkingDir: string | undefined,
homeDir: string
): string => externalWorkingDir?.trim() || requestedWorkingDir || homeDir;