fix(desktop): honor remote working directory for new chats and allow typed paths (#11322)

Co-authored-by: pcace <{ID}+{username}@users.noreply.github.com>
This commit is contained in:
Johannes Lee-Frölich
2026-08-25 05:00:56 +00:00
committed by GitHub
parent 520296328f
commit 70bd9d79bc
21 changed files with 389 additions and 8 deletions
+25 -3
View File
@@ -16,7 +16,7 @@ import { ChatState } from '../types/chatState';
import 'react-toastify/dist/ReactToastify.css';
import { View, ViewOptions } from '../utils/navigationUtils';
import { useConfig } from './ConfigContext';
import { getInitialWorkingDir } from '../utils/workingDir';
import { getEffectiveWorkingDir, getInitialWorkingDir } from '../utils/workingDir';
import { createSession } from '../sessions';
import LoadingGoose from './LoadingGoose';
import { UserInput } from '../types/message';
@@ -57,12 +57,25 @@ export default function Hub({
const intl = useIntl();
const { extensionsList } = useConfig();
const [workingDir, setWorkingDir] = useState(getInitialWorkingDir());
const userSelectedWorkingDirRef = useRef(false);
const [isCreatingSession, setIsCreatingSession] = useState(false);
const [nextChatExtensionDraft, setNextChatExtensionDraft] =
useState<NextChatExtensionDraft | null>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const { time, meridiem, hour } = useClock();
// Re-resolve the working dir on mount: GOOSE_WORKING_DIR is fixed at window
// creation, so a configured remote directory may have changed since then.
useEffect(() => {
let active = true;
void getEffectiveWorkingDir().then((dir) => {
if (active && !userSelectedWorkingDirRef.current) setWorkingDir(dir);
});
return () => {
active = false;
};
}, []);
const greeting = useMemo(() => {
if (hour < 12) return intl.formatMessage(i18n.goodMorning);
if (hour < 18) return intl.formatMessage(i18n.goodAfternoon);
@@ -86,6 +99,11 @@ export default function Hub({
setNextChatExtensionDraft(draft);
}, []);
const handleWorkingDirChange = useCallback((dir: string) => {
userSelectedWorkingDirRef.current = true;
setWorkingDir(dir);
}, []);
const handleSubmit = async (input: UserInput) => {
const { msg: userMessage, images } = input;
if (!(images.length > 0 || userMessage.trim()) || isCreatingSession) return;
@@ -101,7 +119,10 @@ export default function Hub({
? { extensionConfigs: selectedExtensions }
: { allExtensions: extensionsList };
const session = await createSession(workingDir, sessionOptions);
// Resolve the effective directory at submit time: the IPC lookup may still
// be pending when the user submits, and an explicit pick must win.
const dir = userSelectedWorkingDirRef.current ? workingDir : await getEffectiveWorkingDir();
const session = await createSession(dir, sessionOptions);
setNextChatExtensionDraft(null);
window.dispatchEvent(new CustomEvent(AppEvents.SESSION_CREATED));
@@ -149,7 +170,8 @@ export default function Hub({
onFilesProcessed={() => {}}
messages={[]}
disableAnimation={false}
onWorkingDirChange={setWorkingDir}
workingDir={workingDir}
onWorkingDirChange={handleWorkingDirChange}
inputRef={inputRef}
nextChatExtensionDraft={draftForMenu}
onNextChatExtensionDraftChange={handleNextChatExtensionDraftChange}
@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Check, FolderDot, FolderOpen, GitBranch, Plus } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/Tooltip';
import { Input } from '../ui/input';
import {
DropdownMenu,
DropdownMenuContent,
@@ -41,8 +42,23 @@ const i18n = defineMessages({
id: 'dirSwitcher.noWorktreesFound',
defaultMessage: 'No worktrees found',
},
enterPath: {
id: 'dirSwitcher.enterPath',
defaultMessage: 'Enter path',
},
enterPathPlaceholder: {
id: 'dirSwitcher.enterPathPlaceholder',
defaultMessage: 'Enter an absolute path (e.g. /home/goose/workspace)',
},
enterPathInvalid: {
id: 'dirSwitcher.enterPathInvalid',
defaultMessage: 'Working directory must be an absolute path',
},
});
const isAbsolutePath = (p: string): boolean =>
p.startsWith('/') || p.startsWith('\\\\') || /^[A-Za-z]:[\\/]/.test(p);
const splitDirPath = (dir: string): { name: string; parent: string } => {
const normalized = dir.replace(/[\\/]+$/, '');
const parts = normalized.split(/[\\/]/);
@@ -84,6 +100,7 @@ export const DirSwitcher: React.FC<DirSwitcherProps> = ({
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [recentDirs, setRecentDirs] = useState<string[]>([]);
const [worktreeDirs, setWorktreeDirs] = useState<string[]>([]);
const [customDirInput, setCustomDirInput] = useState('');
const refreshVersionRef = useRef(0);
const refreshMenuData = useCallback(async () => {
@@ -111,9 +128,6 @@ export const DirSwitcher: React.FC<DirSwitcherProps> = ({
}, [isMenuOpen, refreshMenuData]);
const applyDirectoryChange = async (newDir: string) => {
window.electron.addRecentDir(newDir);
setRecentDirs((previous) => [newDir, ...previous.filter((dir) => dir !== newDir)].slice(0, 10));
if (sessionId) {
onRestartStart?.();
@@ -122,12 +136,18 @@ export const DirSwitcher: React.FC<DirSwitcherProps> = ({
} catch (error) {
console.error('[DirSwitcher] Failed to update working directory:', error);
toast.error(intl.formatMessage(i18n.failedToUpdateWorkingDir));
return;
} finally {
onRestartEnd?.();
}
} else {
await onWorkingDirChange?.(newDir);
}
// Only record the directory after the backend confirmed the change, so a
// rejected path does not pollute the recent-directories list.
window.electron.addRecentDir(newDir);
setRecentDirs((previous) => [newDir, ...previous.filter((dir) => dir !== newDir)].slice(0, 10));
};
const handleDirectoryChange = async () => {
@@ -218,6 +238,31 @@ export const DirSwitcher: React.FC<DirSwitcherProps> = ({
<Check className="ml-auto h-4 w-4 flex-shrink-0" />
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>{intl.formatMessage(i18n.enterPath)}</DropdownMenuLabel>
<div className="px-2 py-1.5">
<Input
value={customDirInput}
placeholder={intl.formatMessage(i18n.enterPathPlaceholder)}
onChange={(e) => setCustomDirInput(e.target.value)}
onKeyDown={(e) => {
// Stop Radix menu typeahead from stealing focus while typing
// a path (e.g. the initial "C" of a Windows drive path).
e.stopPropagation();
if (e.key === 'Enter' && customDirInput.trim()) {
const newDir = customDirInput.trim();
if (!isAbsolutePath(newDir)) {
toast.error(intl.formatMessage(i18n.enterPathInvalid));
return;
}
setCustomDirInput('');
setIsMenuOpen(false);
void applyDirectoryChange(newDir);
}
}}
/>
</div>
<DropdownMenuSeparator />
<DropdownMenuLabel>{intl.formatMessage(i18n.gitWorktrees)}</DropdownMenuLabel>
{filteredWorktreeDirs.length > 0 ? (
+10
View File
@@ -917,6 +917,16 @@
"dirSwitcher.currentDirectory": {
"defaultMessage": "Aktuelles Verzeichnis"
},
"dirSwitcher.enterPath": {
"defaultMessage": "Pfad eingeben"
},
"dirSwitcher.enterPathPlaceholder": {
"defaultMessage": "Absoluten Pfad eingeben (z. B. /home/goose/workspace)"
},
"dirSwitcher.enterPathInvalid": {
"defaultMessage": "Arbeitsverzeichnis muss ein absoluter Pfad sein"
},
"dirSwitcher.failedToUpdateWorkingDir": {
"defaultMessage": "Aktualisieren des Arbeitsverzeichnisses fehlgeschlagen"
},
+9
View File
@@ -938,6 +938,15 @@
"dirSwitcher.currentDirectory": {
"defaultMessage": "Current directory"
},
"dirSwitcher.enterPath": {
"defaultMessage": "Enter path"
},
"dirSwitcher.enterPathInvalid": {
"defaultMessage": "Working directory must be an absolute path"
},
"dirSwitcher.enterPathPlaceholder": {
"defaultMessage": "Enter an absolute path (e.g. /home/goose/workspace)"
},
"dirSwitcher.failedToUpdateWorkingDir": {
"defaultMessage": "Failed to update working directory"
},
+11
View File
@@ -917,6 +917,17 @@
"dirSwitcher.currentDirectory": {
"defaultMessage": "Directorio actual"
},
"dirSwitcher.enterPath": {
"defaultMessage": "Enter path"
},
"dirSwitcher.enterPathPlaceholder": {
"defaultMessage": "Enter an absolute path (e.g. /home/goose/workspace)"
},
"dirSwitcher.enterPathInvalid": {
"defaultMessage": "Working directory must be an absolute path"
},
"dirSwitcher.failedToUpdateWorkingDir": {
"defaultMessage": "No se pudo actualizar el directorio de trabajo"
},
+11
View File
@@ -917,6 +917,17 @@
"dirSwitcher.currentDirectory": {
"defaultMessage": "Répertoire actuel"
},
"dirSwitcher.enterPath": {
"defaultMessage": "Enter path"
},
"dirSwitcher.enterPathPlaceholder": {
"defaultMessage": "Enter an absolute path (e.g. /home/goose/workspace)"
},
"dirSwitcher.enterPathInvalid": {
"defaultMessage": "Working directory must be an absolute path"
},
"dirSwitcher.failedToUpdateWorkingDir": {
"defaultMessage": "Échec de la mise à jour du répertoire de travail"
},
+11
View File
@@ -917,6 +917,17 @@
"dirSwitcher.currentDirectory": {
"defaultMessage": "वर्तमान निर्देशिका"
},
"dirSwitcher.enterPath": {
"defaultMessage": "Enter path"
},
"dirSwitcher.enterPathPlaceholder": {
"defaultMessage": "Enter an absolute path (e.g. /home/goose/workspace)"
},
"dirSwitcher.enterPathInvalid": {
"defaultMessage": "Working directory must be an absolute path"
},
"dirSwitcher.failedToUpdateWorkingDir": {
"defaultMessage": "कार्यशील निर्देशिका अद्यतन करने में विफल"
},
+11
View File
@@ -917,6 +917,17 @@
"dirSwitcher.currentDirectory": {
"defaultMessage": "Direktori saat ini"
},
"dirSwitcher.enterPath": {
"defaultMessage": "Enter path"
},
"dirSwitcher.enterPathPlaceholder": {
"defaultMessage": "Enter an absolute path (e.g. /home/goose/workspace)"
},
"dirSwitcher.enterPathInvalid": {
"defaultMessage": "Working directory must be an absolute path"
},
"dirSwitcher.failedToUpdateWorkingDir": {
"defaultMessage": "Gagal memperbarui direktori kerja"
},
+11
View File
@@ -917,6 +917,17 @@
"dirSwitcher.currentDirectory": {
"defaultMessage": "Cartella corrente"
},
"dirSwitcher.enterPath": {
"defaultMessage": "Enter path"
},
"dirSwitcher.enterPathPlaceholder": {
"defaultMessage": "Enter an absolute path (e.g. /home/goose/workspace)"
},
"dirSwitcher.enterPathInvalid": {
"defaultMessage": "Working directory must be an absolute path"
},
"dirSwitcher.failedToUpdateWorkingDir": {
"defaultMessage": "Impossibile aggiornare la cartella di lavoro"
},
+11
View File
@@ -917,6 +917,17 @@
"dirSwitcher.currentDirectory": {
"defaultMessage": "現在のディレクトリ"
},
"dirSwitcher.enterPath": {
"defaultMessage": "Enter path"
},
"dirSwitcher.enterPathPlaceholder": {
"defaultMessage": "Enter an absolute path (e.g. /home/goose/workspace)"
},
"dirSwitcher.enterPathInvalid": {
"defaultMessage": "Working directory must be an absolute path"
},
"dirSwitcher.failedToUpdateWorkingDir": {
"defaultMessage": "作業ディレクトリの更新に失敗しました"
},
+11
View File
@@ -917,6 +917,17 @@
"dirSwitcher.currentDirectory": {
"defaultMessage": "현재 디렉터리"
},
"dirSwitcher.enterPath": {
"defaultMessage": "Enter path"
},
"dirSwitcher.enterPathPlaceholder": {
"defaultMessage": "Enter an absolute path (e.g. /home/goose/workspace)"
},
"dirSwitcher.enterPathInvalid": {
"defaultMessage": "Working directory must be an absolute path"
},
"dirSwitcher.failedToUpdateWorkingDir": {
"defaultMessage": "작업 디렉터리를 업데이트하지 못했습니다."
},
+11
View File
@@ -917,6 +917,17 @@
"dirSwitcher.currentDirectory": {
"defaultMessage": "Direktori semasa"
},
"dirSwitcher.enterPath": {
"defaultMessage": "Enter path"
},
"dirSwitcher.enterPathPlaceholder": {
"defaultMessage": "Enter an absolute path (e.g. /home/goose/workspace)"
},
"dirSwitcher.enterPathInvalid": {
"defaultMessage": "Working directory must be an absolute path"
},
"dirSwitcher.failedToUpdateWorkingDir": {
"defaultMessage": "Gagal mengemas kini direktori kerja"
},
+11
View File
@@ -917,6 +917,17 @@
"dirSwitcher.currentDirectory": {
"defaultMessage": "Diretório atual"
},
"dirSwitcher.enterPath": {
"defaultMessage": "Enter path"
},
"dirSwitcher.enterPathPlaceholder": {
"defaultMessage": "Enter an absolute path (e.g. /home/goose/workspace)"
},
"dirSwitcher.enterPathInvalid": {
"defaultMessage": "Working directory must be an absolute path"
},
"dirSwitcher.failedToUpdateWorkingDir": {
"defaultMessage": "Falha ao atualizar o diretório de trabalho"
},
+11
View File
@@ -917,6 +917,17 @@
"dirSwitcher.currentDirectory": {
"defaultMessage": "Текущий каталог"
},
"dirSwitcher.enterPath": {
"defaultMessage": "Enter path"
},
"dirSwitcher.enterPathPlaceholder": {
"defaultMessage": "Enter an absolute path (e.g. /home/goose/workspace)"
},
"dirSwitcher.enterPathInvalid": {
"defaultMessage": "Working directory must be an absolute path"
},
"dirSwitcher.failedToUpdateWorkingDir": {
"defaultMessage": "Не удалось обновить рабочий каталог"
},
+11
View File
@@ -917,6 +917,17 @@
"dirSwitcher.currentDirectory": {
"defaultMessage": "Geçerli dizin"
},
"dirSwitcher.enterPath": {
"defaultMessage": "Enter path"
},
"dirSwitcher.enterPathPlaceholder": {
"defaultMessage": "Enter an absolute path (e.g. /home/goose/workspace)"
},
"dirSwitcher.enterPathInvalid": {
"defaultMessage": "Working directory must be an absolute path"
},
"dirSwitcher.failedToUpdateWorkingDir": {
"defaultMessage": "Çalışma dizini güncellenemedi"
},
+11
View File
@@ -917,6 +917,17 @@
"dirSwitcher.currentDirectory": {
"defaultMessage": "Thư mục hiện tại"
},
"dirSwitcher.enterPath": {
"defaultMessage": "Enter path"
},
"dirSwitcher.enterPathPlaceholder": {
"defaultMessage": "Enter an absolute path (e.g. /home/goose/workspace)"
},
"dirSwitcher.enterPathInvalid": {
"defaultMessage": "Working directory must be an absolute path"
},
"dirSwitcher.failedToUpdateWorkingDir": {
"defaultMessage": "Không thể cập nhật thư mục làm việc"
},
+11
View File
@@ -917,6 +917,17 @@
"dirSwitcher.currentDirectory": {
"defaultMessage": "当前目录"
},
"dirSwitcher.enterPath": {
"defaultMessage": "Enter path"
},
"dirSwitcher.enterPathPlaceholder": {
"defaultMessage": "Enter an absolute path (e.g. /home/goose/workspace)"
},
"dirSwitcher.enterPathInvalid": {
"defaultMessage": "Working directory must be an absolute path"
},
"dirSwitcher.failedToUpdateWorkingDir": {
"defaultMessage": "更新工作目录失败"
},
+11
View File
@@ -917,6 +917,17 @@
"dirSwitcher.currentDirectory": {
"defaultMessage": "目前目錄"
},
"dirSwitcher.enterPath": {
"defaultMessage": "Enter path"
},
"dirSwitcher.enterPathPlaceholder": {
"defaultMessage": "Enter an absolute path (e.g. /home/goose/workspace)"
},
"dirSwitcher.enterPathInvalid": {
"defaultMessage": "Working directory must be an absolute path"
},
"dirSwitcher.failedToUpdateWorkingDir": {
"defaultMessage": "無法更新工作目錄"
},
+8
View File
@@ -988,6 +988,11 @@ let appConfig = {
GOOSE_PREDEFINED_MODELS: predefinedModels,
GOOSE_PATH_ROOT: sanitizeGoosePathRoot(process.env),
GOOSE_WORKING_DIR: '',
// Whether the window is bound to an external backend (fixed at window
// creation via gooseServeLeases) and which URL it is bound to.
GOOSE_EXTERNAL_BACKEND: false,
GOOSE_EXTERNAL_BACKEND_URL: '',
GOOSE_EXTERNAL_BACKEND_SOURCE: '',
// Start with the env-var override; the OS region locale is filled in after app.ready
// (see updateLocaleFromSystem below) since getSystemLocale() cannot be called earlier.
GOOSE_LOCALE: process.env.GOOSE_LOCALE || undefined,
@@ -1310,6 +1315,9 @@ const createChat = async (
...appConfig,
GOOSE_LOCALE: getConfiguredGooseLocale(),
GOOSE_WORKING_DIR: workingDir,
GOOSE_EXTERNAL_BACKEND: externalBackend !== null,
GOOSE_EXTERNAL_BACKEND_URL: externalBackend?.url ?? '',
GOOSE_EXTERNAL_BACKEND_SOURCE: externalBackend?.source ?? '',
REQUEST_DIR: dir,
GOOSE_VERSION: version,
recipeDeeplink: recipeDeeplink,
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { resolveWorkingDir } from '../workingDir';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { getEffectiveWorkingDir, resolveWorkingDir } from '../workingDir';
describe('resolveWorkingDir', () => {
it('uses the configured external backend directory when present', () => {
@@ -10,3 +10,87 @@ describe('resolveWorkingDir', () => {
expect(resolveWorkingDir(undefined, undefined, 'C:\\Users\\goose')).toBe('C:\\Users\\goose');
});
});
describe('getEffectiveWorkingDir', () => {
const getSettingMock = vi.fn();
const appConfigGetMock = vi.fn();
const mockWindow = (externalBackend: boolean, boundUrl: string, source = 'settings') => {
appConfigGetMock.mockImplementation((key: string) => {
if (key === 'GOOSE_EXTERNAL_BACKEND') return externalBackend;
if (key === 'GOOSE_EXTERNAL_BACKEND_URL') return boundUrl;
if (key === 'GOOSE_EXTERNAL_BACKEND_SOURCE') return source;
if (key === 'GOOSE_WORKING_DIR') return '/Users/johannes/home/workspace';
return undefined;
});
};
beforeEach(() => {
getSettingMock.mockReset();
appConfigGetMock.mockReset();
(globalThis as Record<string, unknown>).window = {
appConfig: { get: appConfigGetMock },
electron: { getSetting: getSettingMock },
} as unknown as typeof globalThis;
});
it('prefers the configured remote directory when bound to the matching external backend', async () => {
mockWindow(true, 'http://remote:3000/');
getSettingMock.mockResolvedValue({
enabled: true,
url: 'http://remote:3000',
workingDir: ' /home/goose/workspace ',
});
await expect(getEffectiveWorkingDir()).resolves.toBe('/home/goose/workspace');
});
it('honors the configured remote directory for env-mode backends regardless of enabled/url', async () => {
mockWindow(true, 'http://env-backend:3000', 'env');
getSettingMock.mockResolvedValue({
enabled: false,
url: 'http://unrelated:4000',
workingDir: '/home/goose/workspace',
});
await expect(getEffectiveWorkingDir()).resolves.toBe('/home/goose/workspace');
});
it('falls back to the remembered directory for env-mode backends without a configured dir', async () => {
mockWindow(true, 'http://env-backend:3000', 'env');
getSettingMock.mockResolvedValue({ enabled: false, workingDir: ' ' });
await expect(getEffectiveWorkingDir()).resolves.toBe('/Users/johannes/home/workspace');
});
it('ignores the remote directory when the window is bound to the local backend', async () => {
mockWindow(false, '');
getSettingMock.mockResolvedValue({ enabled: true, workingDir: '/home/goose/workspace' });
await expect(getEffectiveWorkingDir()).resolves.toBe('/Users/johannes/home/workspace');
});
it('falls back to the remembered directory when the bound backend no longer matches settings', async () => {
mockWindow(true, 'http://server-a:3000');
getSettingMock.mockResolvedValue({
enabled: true,
url: 'http://server-b:3000',
workingDir: '/home/goose/workspace',
});
await expect(getEffectiveWorkingDir()).resolves.toBe('/Users/johannes/home/workspace');
});
it('falls back to the remembered directory when the external backend is disabled', async () => {
mockWindow(true, 'http://remote:3000');
getSettingMock.mockResolvedValue({ enabled: false, workingDir: '/home/goose/workspace' });
await expect(getEffectiveWorkingDir()).resolves.toBe('/Users/johannes/home/workspace');
});
it('falls back to the remembered directory when the remote directory is blank', async () => {
mockWindow(true, 'http://remote:3000');
getSettingMock.mockResolvedValue({ enabled: true, workingDir: ' ' });
await expect(getEffectiveWorkingDir()).resolves.toBe('/Users/johannes/home/workspace');
});
it('falls back to the remembered directory when the setting cannot be read', async () => {
mockWindow(true, 'http://remote:3000');
getSettingMock.mockRejectedValue(new Error('settings unavailable'));
await expect(getEffectiveWorkingDir()).resolves.toBe('/Users/johannes/home/workspace');
});
});
+49
View File
@@ -3,6 +3,55 @@ export const getInitialWorkingDir = (): string => {
return (window.appConfig?.get('GOOSE_WORKING_DIR') as string) ?? '';
};
/**
* Resolve the working directory for a new chat in the current window.
*
* GOOSE_WORKING_DIR is fixed when the window is created, so it goes stale when
* the user switches to an external backend (or changes the configured remote
* directory) afterwards. The configured remote directory is only applied when
* the window is actually bound to an external backend (fixed at window creation
* via the gooseServeLeases) and that backend still matches the current
* settings; otherwise the remote path would be sent to the local (or a
* different remote) server, where it fails the cwd existence validation.
* Editing the remote working directory in settings still takes effect for new
* chats in the same window. Env-mode backends (GOOSE_EXTERNAL_BACKEND) always
* use the configured directory (matching getActiveExternalBackend), while
* settings-mode backends require the window-bound backend to still match.
*/
export const getEffectiveWorkingDir = async (): Promise<string> => {
const initial = getInitialWorkingDir();
const boundUrl = window.appConfig?.get('GOOSE_EXTERNAL_BACKEND_URL') as string | undefined;
const source = window.appConfig?.get('GOOSE_EXTERNAL_BACKEND_SOURCE') as string | undefined;
if (window.appConfig?.get('GOOSE_EXTERNAL_BACKEND') !== true || !boundUrl) {
return initial;
}
try {
const external = await window.electron.getSetting('externalGoosed');
const remote = external?.workingDir?.trim();
if (!remote) {
return initial;
}
// Env-mode backends use settings.externalGoosed.workingDir regardless of the
// enabled flag or URL (see getActiveExternalBackend); settings-mode requires
// the backend to still match the window-bound URL.
if (source === 'env') {
return remote;
}
if (
external?.enabled &&
external?.url &&
normalizeUrl(boundUrl) === normalizeUrl(external.url)
) {
return remote;
}
} catch {
// Settings unavailable; fall back to the remembered directory.
}
return initial;
};
const normalizeUrl = (url: string): string => url.trim().replace(/\/+$/, '');
export const resolveWorkingDir = (
externalWorkingDir: string | undefined,
requestedWorkingDir: string | undefined,