Merge branch 'bug377034_site'
diff --git a/bundles/org.eclipse.orion.client.core/web/defaults.pref b/bundles/org.eclipse.orion.client.core/web/defaults.pref
index d8218e5..4690bce 100644
--- a/bundles/org.eclipse.orion.client.core/web/defaults.pref
+++ b/bundles/org.eclipse.orion.client.core/web/defaults.pref
@@ -12,6 +12,7 @@
 		"plugins/pageLinksPlugin.html":true,
 		"plugins/preferencesPlugin.html":true,
 		"plugins/taskPlugin.html": true,
-		"plugins/csslintPlugin.html": true
+		"plugins/csslintPlugin.html": true,
+		"plugins/site/sitePlugin.html": true
 	}
 }
diff --git a/bundles/org.eclipse.orion.client.core/web/navigate/table.js b/bundles/org.eclipse.orion.client.core/web/navigate/table.js
index a95fd11..87b6eda 100644
--- a/bundles/org.eclipse.orion.client.core/web/navigate/table.js
+++ b/bundles/org.eclipse.orion.client.core/web/navigate/table.js
@@ -14,10 +14,10 @@
 
 define(['dojo', 'dijit', 'orion/bootstrap', 'orion/selection', 'orion/status', 'orion/progress', 'orion/dialogs',
         'orion/ssh/sshTools', 'orion/commands', 'orion/favorites', 'orion/navoutliner', 'orion/searchClient', 'orion/fileClient', 'orion/operationsClient', 'orion/globalCommands',
-        'orion/fileCommands', 'orion/explorer-table', 'orion/util', 'orion/PageUtil','orion/contentTypes', 'orion/siteService', 'orion/siteCommands',
+        'orion/fileCommands', 'orion/explorer-table', 'orion/util', 'orion/PageUtil','orion/contentTypes',
         'dojo/parser', 'dijit/layout/BorderContainer', 'dijit/layout/ContentPane', 'orion/widgets/eWebBorderContainer'], 
 		function(dojo, dijit, mBootstrap, mSelection, mStatus, mProgress, mDialogs, mSsh, mCommands, mFavorites, mNavOutliner,
-				mSearchClient, mFileClient, mOperationsClient, mGlobalCommands, mFileCommands, mExplorerTable, mUtil, PageUtil, mContentTypes, mSiteService, mSiteCommands) {
+				mSearchClient, mFileClient, mOperationsClient, mGlobalCommands, mFileCommands, mExplorerTable, mUtil, PageUtil, mContentTypes) {
 
 dojo.addOnLoad(function(){
 	mBootstrap.startup().then(function(core) {
@@ -31,7 +31,6 @@
 		new mSsh.SshService(serviceRegistry);
 		new mFavorites.FavoritesService({serviceRegistry: serviceRegistry});
 		var commandService = new mCommands.CommandService({serviceRegistry: serviceRegistry, selection: selection});
-		new mSiteService.SiteService(serviceRegistry);
 		
 		// Git operations
 		//new eclipse.GitService(serviceRegistry);
@@ -100,7 +99,6 @@
 		commandService.registerCommandContribution("fileFolderCommands", "eclipse.downloadFile", 2, "eclipse.fileGroup/eclipse.importExportGroup");
 		commandService.registerCommandContribution("fileFolderCommands", "eclipse.importSFTPCommand", 3, "eclipse.fileGroup/eclipse.importExportGroup");
 		commandService.registerCommandContribution("fileFolderCommands", "eclipse.exportSFTPCommand", 4, "eclipse.fileGroup/eclipse.importExportGroup");
-		commandService.registerCommandContribution("fileFolderCommands", "orion.site.viewon", 5, "eclipse.fileGroup");
 		// new file and new folder in the actions column uses the labeled group
 		commandService.registerCommandContribution("fileFolderCommands", "eclipse.newFile", 1, "eclipse.fileGroup/eclipse.newResources");
 		commandService.registerCommandContribution("fileFolderCommands", "eclipse.newFolder", 2, "eclipse.fileGroup/eclipse.newResources");
@@ -116,7 +114,6 @@
 		commandService.registerCommandContribution("selectionTools", "eclipse.deleteFile", 3, "eclipse.selectionGroup");
 			
 		mFileCommands.createAndPlaceFileCommandsExtension(serviceRegistry, commandService, explorer, "pageActions", "selectionTools", "eclipse.fileGroup", "eclipse.selectionGroup");
-		mSiteCommands.createSiteCommands(serviceRegistry);
 
 		// when new item is fetched, display it in the page title
 		dojo.connect(explorer, "onchange", function(item) {
diff --git a/bundles/org.eclipse.orion.client.core/web/orion/siteCommands.js b/bundles/org.eclipse.orion.client.core/web/orion/siteCommands.js
deleted file mode 100644
index a2b35e2..0000000
--- a/bundles/org.eclipse.orion.client.core/web/orion/siteCommands.js
+++ /dev/null
@@ -1,238 +0,0 @@
-/*******************************************************************************
- * @license
- * Copyright (c) 2011, 2012 IBM Corporation and others.
- * All rights reserved. This program and the accompanying materials are made 
- * available under the terms of the Eclipse Public License v1.0 
- * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution 
- * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). 
- * 
- * Contributors: IBM Corporation - initial API and implementation
- ******************************************************************************/
-
-/*global console define document window*/
-define(['require', 'orion/commands', 'orion/siteUtils'],
-		function(require, mCommands, mSiteUtils) {
-	var Command = mCommands.Command;
-	var sitesCache = null;
-	var workspacesCache = null;
-
-	function SitesCache(siteService) {
-		this.sites = [];
-		var self = this;
-		siteService.getSiteConfigurations().then(
-			function(sites) {
-				self.sites = sites;
-			});
-	}
-
-	function WorkspacesCache(fileService) {
-		var promise = null;
-		this.getWorkspaces = function() {
-			if (!promise) {
-				promise = fileService.loadWorkspaces();
-			}
-			return promise;
-		};
-	}
-
-	function toArray(obj) {
-		return Array.isArray(obj) ? obj : [obj];
-	}
-
-	function oneFileOrFolder(items) {
-		items = toArray(items);
-		if (items.length === 0) {
-			return false;
-		}
-		// Looks like a file object, not a site configuration
-		return items[0].Location && !items[0].Mappings;
-	}
-
-	function makeViewOnSiteChoices(items, userData, serviceRegistry, viewOnCallback) {
-		function insertMappingFor(virtualPath, filePath, mappings) {
-			for (var i=0; i < mappings.length; i++) {
-				var mapping = mappings[i];
-				if (mapping.Target === filePath) {
-					return;
-				}
-			}
-			mappings.push({Source: virtualPath, Target: filePath, FriendlyPath: virtualPath});
-		}
-		function err(error) {
-			serviceRegistry.getService("orion.page.progress").setProgressResult(error);
-		}
-		items = toArray(items);
-		var callback = function(site, selectedItems) {
-			selectedItems = Array.isArray(selectedItems) ? selectedItems : [selectedItems];
-			var item = selectedItems[0];
-			var virtualPath = "/" + item.Name;
-			var siteService = serviceRegistry.getService("orion.sites");
-			var deferred;
-			if (!site) {
-				var name = item.Name + " site";
-				deferred = siteService.makeRelativeFilePath(item.Location).then(function(filePath) {
-					var mappings = [];
-					insertMappingFor(virtualPath, filePath, mappings);
-					return workspacesCache.getWorkspaces().then(function(workspaces) {
-						var workspaceId = workspaces[0].Id;
-						return siteService.createSiteConfiguration(name, workspaceId, mappings, null, {Status: "started"});
-					});
-				});
-			} else {
-				if (site.HostingStatus.Status === "started") {
-					site.HostingStatus.Status = "stopped";
-				}
-				deferred = siteService.makeRelativeFilePath(item.Location).then(function(filePath) {
-					insertMappingFor(virtualPath, filePath, site.Mappings);
-					return siteService.updateSiteConfiguration(site.Location, site).then(function(site) {
-						return siteService.updateSiteConfiguration(site.Location, {HostingStatus: {Status: "started"}});
-					});
-				});
-			}
-			deferred.then(function(site) {
-				// At this point the site is started
-				var a = document.createElement("a");
-				a.href = site.HostingStatus.URL + virtualPath + (item.Directory ? "/" : "");
-				var url = a.href;
-				if (viewOnCallback) {
-					viewOnCallback(url, site);
-				} else {
-					window.location = url;
-				}
-			}, err);
-		};
-		var choices = [];
-		for (var i = 0; i < sitesCache.sites.length; i++) {
-			var site = sitesCache.sites[i];
-			choices.push({name: site.Name, callback: callback.bind(null, site)});
-		}
-		if (choices.length) {
-			choices.push({});	//separator
-		}
-		choices.push({name: "New Site", callback: callback.bind(null, null)});
-		return choices;
-	}
-
-	/**
-	 * Creates & adds commands that act on an individual site configuration.
-	 * @param {orion.serviceregistry.ServiceRegistry} serviceRegistry
-	 * @param {Function} options.createCallback
-	 * @param {Function} options.startCallback
-	 * @param {Function} options.stopCallback
-	 * @param {Function} options.deleteCallback
-	 * @param {Function} options.viewOnCallback
-	 * @param {Function} options.errorCallback
-	 * @name orion.siteCommands#createSiteCommands
-	 * @function
-	 */
-	function createSiteCommands(serviceRegistry, options) {
-		options = options || {};
-		var commandService = serviceRegistry.getService("orion.page.command"),
-		    siteService = serviceRegistry.getService("orion.sites"),
-		    dialogService = serviceRegistry.getService("orion.page.dialog"),
-		    progressService = serviceRegistry.getService("orion.page.progress");
-		var createCommand = new mCommands.Command({
-			name : "Create Site",
-			tooltip: "Create a new site configuration",
-			imageClass: "core-sprite-add",
-			id: "orion.site.create",
-			groupId: "orion.sitesGroup",
-			parameters: new mCommands.ParametersDescription([new mCommands.CommandParameter('name', 'string', 'Name:')]),
-			callback : function(data) {
-				var name = data.parameters && data.parameters.valueFor('name');
-				workspacesCache.getWorkspaces().then(function(workspaces) {
-			        var workspaceId = workspaces && workspaces[0] && workspaces[0].Id;
-			        if (workspaceId && name) {
-				        siteService.createSiteConfiguration(name, workspaceId).then(function(site) {
-							options.createCallback(mSiteUtils.generateEditSiteHref(site), site);
-						}, options.errorCallback);
-			        }
-				});
-			}});
-		commandService.addCommand(createCommand);
-
-		var editCommand = new Command({
-			name: "Edit",
-			tooltip: "Edit the site configuration",
-			imageClass: "core-sprite-edit",
-			id: "orion.site.edit",
-			visibleWhen: function(item) {
-				return item.HostingStatus;
-			},
-			hrefCallback: function(data) { return mSiteUtils.generateEditSiteHref(data.items);}});
-		commandService.addCommand(editCommand);
-
-		var startCommand = new Command({
-			name: "Start",
-			tooltip: "Start the site",
-			imageClass: "core-sprite-start",
-			id: "orion.site.start",
-			visibleWhen: function(item) {
-				return item.HostingStatus && item.HostingStatus.Status === "stopped";
-			},
-			/** @param {SiteConfiguration} [userData] If passed, we'll mutate this site config. */
-			callback: function(data) {
-				var newItem = data.userData || {} /* just update the HostingStatus */;
-				newItem.HostingStatus = { Status: "started" };
-				var deferred = siteService.updateSiteConfiguration(data.items.Location, newItem);
-				progressService.showWhile(deferred).then(options.startCallback, options.errorCallback);
-			}});
-		commandService.addCommand(startCommand);
-
-		var stopCommand = new Command({
-			name: "Stop",
-			tooltip: "Stop the site",
-			imageClass: "core-sprite-stop",
-			id: "orion.site.stop",
-			visibleWhen: function(item) {
-				return item.HostingStatus && item.HostingStatus.Status === "started";
-			},
-			/** @param {SiteConfiguration} [data.userData] If passed, we'll mutate this site config. */
-			callback: function(data) {
-				var newItem = data.userData || {} /* just update the HostingStatus */;
-				newItem.HostingStatus = { Status: "stopped" };
-				var deferred = siteService.updateSiteConfiguration(data.items.Location, newItem);
-				progressService.showWhile(deferred).then(options.stopCallback, options.errorCallback);
-			}});
-		commandService.addCommand(stopCommand);
-
-		var deleteCommand = new Command({
-			name: "Delete",
-			tooltip: "Delete the site configuration",
-			imageClass: "core-sprite-delete",
-			id: "orion.site.delete",
-			visibleWhen: function(item) {
-				return item.HostingStatus && item.HostingStatus.Status === "stopped";
-			},
-			callback: function(data) {
-				var msg = "Are you sure you want to delete the site configuration '" + data.items.Name + "'?";
-				dialogService.confirm(msg, function(confirmed) {
-						if (confirmed) {
-							siteService.deleteSiteConfiguration(data.items.Location).then(options.deleteCallback, options.errorCallback);
-						}
-					});
-			}});
-		commandService.addCommand(deleteCommand);
-
-		var viewOnSiteCommand = new Command({
-			name: "View on site",
-			tooltip: "View this file on a web site hosted by Orion",
-			id: "orion.site.viewon",
-			choiceCallback: function(items, userData) {
-				return makeViewOnSiteChoices(items, userData, serviceRegistry, options.viewOnCallback);
-			},
-			visibleWhen: oneFileOrFolder
-			});
-		commandService.addCommand(viewOnSiteCommand);
-
-		if (!sitesCache) {
-			sitesCache = new SitesCache(siteService);
-		}
-		if (!workspacesCache) {
-			workspacesCache = new WorkspacesCache(serviceRegistry.getService("orion.core.file"));
-		}
-	}
-	return {
-		createSiteCommands: createSiteCommands
-	};
-});
\ No newline at end of file
diff --git a/bundles/org.eclipse.orion.client.core/web/orion/siteService.js b/bundles/org.eclipse.orion.client.core/web/orion/siteService.js
deleted file mode 100644
index 354180d..0000000
--- a/bundles/org.eclipse.orion.client.core/web/orion/siteService.js
+++ /dev/null
@@ -1,322 +0,0 @@
-/*******************************************************************************
- * @license
- * Copyright (c) 2011 IBM Corporation and others.
- * All rights reserved. This program and the accompanying materials are made 
- * available under the terms of the Eclipse Public License v1.0 
- * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution 
- * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). 
- * 
- * Contributors: IBM Corporation - initial API and implementation
- ******************************************************************************/
-
-/*global define document window*/
-/*jslint devel:true regexp:false*/
-
-define(['require', 'dojo', 'orion/auth'], function(require, dojo, mAuth) {
-
-// Service id used for registering or obtaining the site service.
-var SERVICE_ID = "orion.sites";
-
-/**
- * @name orion.sites.SiteConfiguration
- * @class Interface for an in-memory representation of a site configuration resource. Objects of
- * this interface are used as parameters, and returned by, methods of the  {@link orion.sites.SiteService}
- * API.
- */
-	/**#@+
-		@fieldOf orion.sites.SiteConfiguration.prototype
-	*/
-	/**
-	 * The name of the site configuration.
-	 * @name Name
-	 * @type String
-	 */
-	/**
-	 * The workspace id that this site configuration is associated with.
-	 * @name Workspace
-	 * @type String
-	 */
-	/**
-	 * The mappings defined by this site configuration. Each element has the properties 
-	 * <code>Source</code> and <code>Target</code>, both of type {@link String}. 
-	 * @name Mappings
-	 * @type Array
-	 */
-	/**
-	 * Optional: A hint used to derive the domain name when the site is launched as a subdomain. 
-	 * @name HostHint
-	 * @type String
-	 */
-	/**
-	 * Gives information about the status of this site configuration. Has the following properties:<ul>
-	 * <li>{String} <code>Status</code> Status of this site. Value is either <code>"started"</code> or <code>"stopped"</code>.</li>
-	 * <li>{String} <code>URL</code> Optional, gives the URL where the running site can be accessed. Only present
-	 * if the <code>Status</code> is <code>"started"</code>.</li>
-	 * </ul>
-	 * @name HostingStatus
-	 * @type Object
-	 */
-	/**#@-*/
-
-	
-	/**
-	 * Constructs a new SiteService.
-	 * 
-	 * @name orion.sites.SiteService
-	 * @class Defines and implements a service that provides access to the server API for managing site 
-	 * configurations.
-	 * @param {orion.serviceregistry.ServiceRegistry} serviceRegistry The service registry to register ourself with.
-	 */
-	function SiteService(serviceRegistry) {
-		this._serviceRegistration = serviceRegistry.registerService(SERVICE_ID, this);
-		
-		var baseUrl = this.getContext();
-		var fileReferences = serviceRegistry.getServiceReferences("orion.core.file");
-		for (var i=0; i < fileReferences.length; i++) {
-			var top = fileReferences[i].getProperty("top");
-			if (top && this.toFullUrl(top).indexOf(baseUrl) === 0) {
-				this.filePrefix = top;
-				break;
-			}
-		}
-	}
-	
-	SiteService.prototype = /** @lends orion.sites.SiteService.prototype */ {
-		/**
-		 * Retrieves all site configurations defined by the logged-in user.
-		 * @returns {dojo.Deferred} A deferred for the result. Will be resolved with the 
-		 * argument {@link Array} on success, where each element of the array is a
-		 * {@link orion.sites.SiteConfiguration}.
-		 */
-		getSiteConfigurations: function() {
-			return this._doServiceCall("getSiteConfigurations", arguments);
-		},
-		
-		/**
-		 * Loads an individual site configuration from the given location.
-		 * @param {String} locationUrl Location URL of a site configuration resource.
-		 * @returns {dojo.Deferred} A deferred for the result. Will be resolved with the 
-		 * loaded {@link orion.sites.SiteConfiguration} on success.
-		 */
-		loadSiteConfiguration: function(locationUrl) {
-			return this._doServiceCall("loadSiteConfiguration", arguments);
-		},
-		
-		/**
-		 * Creates a site configuration.
-		 * @param {String} name
-		 * @param {String} workspace
-		 * @param {Array} [mappings]
-		 * @param {String} [hostHint] 
-		 * @returns {dojo.Deferred} A deferred for the result. Will be resolved with the 
-		 * created {@link orion.sites.SiteConfiguration} on success.
-		 */
-		createSiteConfiguration: function(name, workspace, mappings, hostHint) {
-			return this._doServiceCall("createSiteConfiguration", arguments);
-		},
-		
-		/**
-		 * Edits an existing site configuration.
-		 * @param {String} locationUrl Location of the site configuration resource to be updated.
-		 * @param {orion.sites.SiteConfiguration} updatedSiteConfig A representation of the updated site.
-		 * Properties that are not changing may be omitted.
-		 * @returns {dojo.Deferred} A deferred for the result. Will be resolved with the updated
-		 * {@link orion.sites.SiteConfiguration} on success.
-		 */
-		updateSiteConfiguration: function(locationUrl, updatedSiteConfig) {
-			return this._doServiceCall("updateSiteConfiguration", arguments);
-		},
-		
-		/**
-		 * Deletes a site configuration.
-		 * @param {String} locationUrl Location of the site configuration resource to be deleted.
-		 * @returns {dojo.Deferred} A deferred for the result. Will be resolved with no argument on success.
-		 */
-		deleteSiteConfiguration: function(locationUrl) {
-			return this._doServiceCall("deleteSiteConfiguration", arguments);
-		},
-		
-		/**
-		 * @private
-		 */
-		_serviceImpl : {
-			getSiteConfigurations: function() {
-				//NOTE: require.toURL needs special logic here to handle "site"
-				var siteUrl = require.toUrl("site._");
-				siteUrl = siteUrl.substring(0,siteUrl.length-2);
-				return dojo.xhrGet({
-					url: siteUrl,
-					preventCache: true,
-					headers: {
-						"Orion-Version": "1"
-					},
-					handleAs: "json",
-					timeout: 15000
-				}).then(function(response) {
-					return response.SiteConfigurations;
-				});
-			},
-			loadSiteConfiguration: function(locationUrl) {
-				return dojo.xhrGet({
-					url: locationUrl,
-					headers: {
-						"Orion-Version": "1"
-					},
-					handleAs: "json",
-					timeout: 15000
-				});
-			},
-			/**
-			 * @param {String} name
-			 * @param {String} workspaceId
-			 * @param {Object} [mappings]
-			 * @param {String} [hostHint]
-			 * @param {String} [status]
-			 */
-			createSiteConfiguration: function(name, workspaceId, mappings, hostHint, hostingStatus) {
-				function hostify(name) {
-					return name.replace(/ /g, "-").replace(/[^A-Za-z0-9-_]/g, "").toLowerCase();
-				}
-				var toCreate = {
-						Name: name,
-						Workspace: workspaceId,
-						HostHint: hostify(name)
-					};
-				if (mappings) { toCreate.Mappings = mappings; }
-				if (hostHint) { toCreate.HostHint = hostHint; }
-				if (hostingStatus) { toCreate.HostingStatus = hostingStatus; }
-
-				//NOTE: require.toURL needs special logic here to handle "site"
-				var siteUrl = require.toUrl("site._");
-				siteUrl = siteUrl.substring(0,siteUrl.length-2);
-				return dojo.xhrPost({
-					url: siteUrl,
-					postData: dojo.toJson(toCreate),
-					headers: {
-						"Content-Type": "application/json; charset=utf-8",
-						"Orion-Version": "1"
-					},
-					handleAs: "json",
-					timeout: 15000
-				});
-			},
-			updateSiteConfiguration: function(locationUrl, updatedSiteConfig) {
-				return dojo.xhrPut({
-					url: locationUrl,
-					putData: dojo.toJson(updatedSiteConfig),
-					headers: {
-						"Content-Type": "application/json; charset=utf-8",
-						"Orion-Version": "1"
-					},
-					handleAs: "json",
-					timeout: 15000
-				});
-			},
-			deleteSiteConfiguration: function(locationUrl) {
-				return dojo.xhrDelete({
-					url: locationUrl,
-					headers: {
-						"Orion-Version": "1"
-					},
-					handleAs: "json",
-					timeout: 15000
-				});
-			}
-		},
-		
-		/**
-		 * Performs a service call, handling authentication and retrying after auth.
-		 * @private
-		 * @returns {dojo.Deferred}
-		 */
-		_doServiceCall: function(methodName, args) {
-			var service = this._serviceImpl;
-			var serviceMethod = this._serviceImpl[methodName];
-			var clientDeferred = new dojo.Deferred();
-			
-			// On success, just forward the result to the client
-			var onSuccess = function(result) {
-				clientDeferred.callback(result);
-			};
-			
-			// On failure we might need to retry
-			var onError = function(error) {
-				if (error.status === 401 || error.status === 403) {
-					mAuth.handleAuthenticationError(error, function(message) {
-						// Try again
-						serviceMethod.apply(service, args).then(
-							function(result) {
-								clientDeferred.callback(result);
-							},
-							function(error) {
-								clientDeferred.errback(error);
-							}
-						);
-					});
-				} else {
-					// Forward other errors to client
-					clientDeferred.errback(error);
-				}
-			};
-			
-			serviceMethod.apply(service, args).then(onSuccess, onError);
-			return clientDeferred;
-		},
-		
-		_makeHostRelative: function(url) {
-			if (url.indexOf(":") !== -1) {
-				return url.substring(url.indexOf(window.location.host) + window.location.host.length);
-			}
-			return url;
-		},
-		
-		getContext: function() {
-			var root = require.toUrl("._");
-			var url = this.toFullUrl(root);
-			return url.substring(0, url.length-2);
-		},
-		
-		toFullUrl: function(url) {
-			var link = document.createElement("a");
-			link.href = url;
-			return link.href;
-		},
-		
-		makeRelativeFilePath: function(location) {
-//			var context = this.getContext();
-//			var fakeUrl = this._makeHostRelative(context);
-//			var relativePath = location.substring(location.indexOf(fakeUrl) + fakeUrl.length);
-//			var path = relativePath.substring(relativePath.indexOf(this.filePrefix) + this.filePrefix.length);
-			var relFilePrefix = this._makeHostRelative(this.filePrefix);
-			var relLocation = this._makeHostRelative(location);
-			var path;
-			if (relLocation.indexOf(relFilePrefix) === 0) {
-				path = relLocation.substring(relFilePrefix.length);
-			}
-			if (path[path.length-1] === "/"){
-				path = path.substring(0, path.length - 1);
-			}
-			return path;
-		},
-		
-		makeFullFilePath: function(target) {
-			function _removeEmptyElements(array) {
-				return dojo.filter(array, function(s){return s !== "";});
-			}
-			var relativePath = require.toUrl(this.filePrefix + target + "._");
-			relativePath = relativePath.substring(0, relativePath.length - 2);
-			var segments = target.split("/");
-			if (_removeEmptyElements(segments).length === 1) {
-				relativePath += "/";
-			}
-			return this.toFullUrl(relativePath);
-		}
-	};
-	SiteService.prototype.constructor = SiteService;
-
-	//return module exports
-	return {
-		SERVICE_ID: SERVICE_ID,
-		SiteService: SiteService
-	};
-});
diff --git a/bundles/org.eclipse.orion.client.core/web/orion/siteTree.js b/bundles/org.eclipse.orion.client.core/web/orion/siteTree.js
deleted file mode 100644
index 9b7b0e0..0000000
--- a/bundles/org.eclipse.orion.client.core/web/orion/siteTree.js
+++ /dev/null
@@ -1,137 +0,0 @@
-/*******************************************************************************
- * @license
- * Copyright (c) 2011 IBM Corporation and others.
- * All rights reserved. This program and the accompanying materials are made 
- * available under the terms of the Eclipse Public License v1.0 
- * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution 
- * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). 
- * 
- * Contributors: IBM Corporation - initial API and implementation
- ******************************************************************************/
-
-/*global define dojo */
-/*jslint browser:true devel:true*/
-
-define(['dojo', 'orion/siteUtils'], function(dojo, mSiteUtils) {
-
-var exports = {};
-
-exports.SiteTreeModel = (function() {
-	/**
-	 * @name orion.sites.SiteTreeModel
-	 * @class Tree model for powering a tree of site configurations.
-	 * @param {orion.sites.SiteService} siteService
-	 * @see orion.treetable.TableTree
-	 */
-	function SiteTreeModel(siteService, id) {
-		this._siteService = siteService;
-		this._root = {};
-		this._id = id;
-	}
-	SiteTreeModel.prototype = /** @lends orion.sites.SiteTreeModel.prototype */{
-		getRoot: function(/**function*/ onItem) {
-			onItem(this._root);
-		},
-		getChildren: function(/**dojo.data.Item*/ parentItem, /**Function(items)*/ onComplete) {
-			if (parentItem.children) {
-				// The parent already has the children fetched
-				onComplete(parentItem.children);
-			} else if (parentItem === this._root) {
-				this._siteService.getSiteConfigurations().then(
-					function(/**Array*/ siteConfigurations) {
-						parentItem.children = siteConfigurations;
-						onComplete(siteConfigurations);
-					});
-			} else {
-				return onComplete([]);
-			}
-		},
-		getId: function(/**dojo.data.Item|String*/ item) {
-			return (item === this._root || item === this._id) ? this._id : item.Id;
-		}
-	};
-	return SiteTreeModel;
-}());
-
-exports.SiteRenderer = (function() {
-	/**
-	 * @name orion.sites.SiteRenderer
-	 * @class A renderer for the site configuration tree.
-	 * @see orion.treetable.TableTree
-	 * @param {orion.commands.CommandService} commandService
-	 */
-	function SiteRenderer (commandService) {
-		this._commandService = commandService;
-	}
-	SiteRenderer.prototype = /** @lends orion.sites.SiteRenderer.prototype */{
-		initTable: function (tableNode, tableTree) {
-			this.tableTree = tableTree;
-			
-			dojo.addClass(tableNode, 'treetable');
-			var thead = dojo.create("thead", null);
-			dojo.create("th", {innerHTML: "Name"}, thead, "last");
-			dojo.create("th", {innerHTML: "Status"}, thead, "last");
-			dojo.create("th", {innerHTML: "URL", className: "urlCol"}, thead, "last");
-			dojo.create("th", {innerHTML: "Actions"}, thead, "last");
-			tableNode.appendChild(thead);
-		},
-		render: function(item, tableRow) {
-			dojo.style(tableRow, "verticalAlign", "baseline");
-			dojo.addClass(tableRow, "treeTableRow");
-			
-			var siteConfigCol = dojo.create("td", {id: tableRow.id + "col1"});
-			var statusCol = dojo.create("td", {id: tableRow.id + "col2"});
-			var urlCol = dojo.create("td", {id: tableRow.id + "col3"});
-			var actionCol = dojo.create("td", {id: tableRow.id + "col4"});
-			
-			// Site config column
-			var href = mSiteUtils.generateEditSiteHref(item);
-			var nameLink = dojo.create("a", {href: href}, siteConfigCol, "last");
-			dojo.place(document.createTextNode(item.Name), nameLink, "last");
-			
-			// Status, URL columns
-			var status = item.HostingStatus;
-			if (typeof status === "object") {
-				if (status.Status === "started") {
-					dojo.place(document.createTextNode("Started"), statusCol, "last");
-					var link = dojo.create("a", {className: "siteURL"}, urlCol, "last");
-					dojo.place(document.createTextNode(status.URL), link, "only");
-					link.href = status.URL;
-				} else {
-					var statusString = status.Status.substring(0,1).toUpperCase() + status.Status.substring(1);
-					dojo.place(document.createTextNode(statusString), statusCol, "only");
-				}
-			} else {
-				dojo.place(document.createTextNode("Unknown"), statusCol, "only");
-			}
-			
-			// Action column
-			var actionsWrapper = dojo.create("span", {id: tableRow.id + "actionswrapper"}, actionCol, "only");
-			
-			// contact the command service to render appropriate commands here.
-			this._commandService.renderCommands("siteCommand", actionsWrapper, item, {} /*handler*/, "tool");
-			
-			dojo.place(siteConfigCol, tableRow, "last");
-			dojo.place(statusCol, tableRow, "last");
-			dojo.place(urlCol, tableRow, "last");
-			dojo.place(actionCol, tableRow, "last");
-		},
-		rowsChanged: function() {
-			dojo.query(".treeTableRow").forEach(function(node, i) {
-				if (i % 2) {
-					dojo.addClass(node, "darkTreeTableRow");
-					dojo.removeClass(node, "lightTreeTableRow");
-				} else {
-					dojo.addClass(node, "lightTreeTableRow");
-					dojo.removeClass(node, "darkTreeTableRow");
-				}
-			});
-		},
-		labelColumnIndex: 0
-	};
-	return SiteRenderer;
-}());
-
-return exports;
-});
-
diff --git a/bundles/org.eclipse.orion.client.core/web/orion/sites/siteClient.js b/bundles/org.eclipse.orion.client.core/web/orion/sites/siteClient.js
new file mode 100644
index 0000000..7c8e7cd
--- /dev/null
+++ b/bundles/org.eclipse.orion.client.core/web/orion/sites/siteClient.js
@@ -0,0 +1,332 @@
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2011, 2012 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made 
+ * available under the terms of the Eclipse Public License v1.0 
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution 
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). 
+ * 
+ * Contributors: IBM Corporation - initial API and implementation
+ ******************************************************************************/
+
+/*global define document window*/
+define(['require', 'orion/Deferred', 'orion/auth', 'orion/fileClient'], function(require, Deferred, mAuth, mFileClient) {
+	/**
+	 * Performs a service call, handling authentication and retrying after auth.
+	 * @returns {Promise}
+	 */
+	function _doServiceCall(service, methodName, args) {
+		var serviceMethod = service[methodName];
+		var clientDeferred = new Deferred();
+		if (typeof serviceMethod !== 'function') {
+			throw 'Service method missing: ' + methodName;
+		}
+		// On success, just forward the result to the client
+		var onSuccess = function(result) {
+			clientDeferred.resolve(result);
+		};
+		
+		// On failure we might need to retry
+		var onError = function(error) {
+			if (error.status === 401 || error.status === 403) {
+				mAuth.handleAuthenticationError(error, function(message) {
+					// Try again
+					serviceMethod.apply(service, args).then(
+						function(result) {
+							clientDeferred.resolve(result);
+						},
+						function(error) {
+							clientDeferred.reject(error);
+						}
+					);
+				});
+			} else {
+				// Forward other errors to client
+				clientDeferred.reject(error);
+			}
+		};
+		serviceMethod.apply(service, args).then(onSuccess, onError);
+		return clientDeferred;
+	}
+
+	function getFileClient(serviceRegistry, filePattern) {
+		return new mFileClient.FileClient(serviceRegistry, function(reference) {
+			var top = reference.getProperty("top");
+			return top && top.indexOf(filePattern) === 0;
+		});
+	}
+
+	/**
+	 * Constructs a new SiteClient.
+	 * @name orion.sites.SiteClient
+	 * @extends orion.sites.SiteService
+	 * @class Convenience API for interacting with a particular {@link orion.sites.SiteService}.
+	 * @param {orion.serviceregistry.ServiceRegistry} serviceRegistry
+	 * @param {orion.serviceregistry.Service} siteService
+	 * @param {orion.serviceregistry.ServiceReference} siteServiceRef
+	 */
+	function SiteClient(serviceRegistry, siteService, siteServiceRef) {
+		this._serviceRegistry = serviceRegistry;
+		this._siteService = siteService;
+		this._selfHost = siteServiceRef.getProperty('canSelfHost');
+		this._sitePattern = siteServiceRef.getProperty('sitePattern');
+		this._filePattern = siteServiceRef.getProperty('filePattern');
+
+		this._getService = function() {
+			return this._siteService;
+		};
+		this._getFilePattern = function() {
+			return this._filePattern;
+		};
+		this._getFileClient = function() {
+			return getFileClient(this._serviceRegistry, this._getFilePattern());
+		};
+		this._canSelfHost = function() {
+			return this._selfHost;
+		};
+	}
+	SiteClient.prototype = {
+		// Convenience methods below
+		isFileMapped: function(site, file) {
+			if (!site) {
+				var d = new Deferred();
+				d.resolve(false);
+				return d;
+			}
+			return this.getURLOnSite(site, file).then(function(url) {
+				return url !== null;
+			});
+		},
+		mapOnSiteAndStart: function(site, file, workspaceId) {
+			var siteClient = this;
+			function insertMappingFor(virtualPath, filePath, mappings) {
+				return siteClient.isFileMapped(site, file).then(function(isFileMapped) {
+					if (!isFileMapped) {
+						mappings.push({Source: virtualPath, Target: filePath, FriendlyPath: virtualPath});
+					}
+					var d = new Deferred();
+					d.resolve();
+					return d;
+				});
+			}
+			var virtualPath = "/" + file.Name;
+			var deferred;
+			if (!site) {
+				// Create a site first
+				var name = file.Name + " site";
+				deferred = siteClient.toInternalForm(file.Location).then(function(filePath) {
+					var mappings = [];
+					return insertMappingFor(virtualPath, filePath, mappings).then(function() {
+						return siteClient.createSiteConfiguration(name, workspaceId, mappings, null, {Status: "started"})
+							.then(function(createdSite) {
+									return createdSite;
+								});
+					});
+				});
+			} else {
+				if (site.HostingStatus.Status === "started") {
+					site.HostingStatus.Status = "stopped";
+				}
+				deferred = siteClient.toInternalForm(file.Location).then(function(filePath) {
+					return insertMappingFor(virtualPath, filePath, site.Mappings).then(function() {
+						// Restart the site so the change will take effect
+						return siteClient.updateSiteConfiguration(site.Location, site).then(function(site) {
+							return siteClient.updateSiteConfiguration(site.Location, {HostingStatus: {Status: "started"}});
+						});
+					});
+				});
+			}
+			return deferred.then(function(site) {
+				return siteClient.getURLOnSite(site, file);
+			});
+		}
+	};
+	// Service methods
+	function proxyServiceMethod(object, name) {
+		object[name] = function() {
+			return _doServiceCall(this._getService(), name, Array.prototype.slice.call(arguments));
+		};
+	}
+	[	'createSiteConfiguration', 'getSiteConfigurations', 'deleteSiteConfiguration', 
+		'loadSiteConfiguration', 'updateSiteConfiguration', 'toFileLocation', 'toInternalForm', 'getMappingObject',
+		'getMappingProposals', 'updateMappingsDisplayStrings', 'parseInternalForm', 'isSelfHostingSite', 
+		'convertToSelfHosting', 'getURLOnSite' 
+	].forEach(function(methodName) {
+			proxyServiceMethod(SiteClient.prototype, methodName);
+		});
+	SiteClient.prototype.constructor = SiteClient;
+
+	function forLocationProperty(serviceRegistry, locationPropertyName, location) {
+		var siteReferences = serviceRegistry.getServiceReferences('orion.site');
+		var references = [];
+		var patterns = [];
+		var services = [];
+		for (var i=0; i < siteReferences.length; i++) {
+			var pattern = siteReferences[i].getProperty(locationPropertyName);
+			var patternEpxr;
+			if (pattern[0] !== '^') {
+				patternEpxr = '^' + pattern;
+			} else {
+				patternEpxr = pattern;
+			}
+			references.push(siteReferences[i]);
+			patterns.push(new RegExp(patternEpxr));
+			services.push(serviceRegistry.getService(siteReferences[i]));
+		}
+
+		var getServiceIndex = function(location) {
+			if (location === '/') {
+				return -1;
+			} else if (!location || (location.length && location.length === 0)) {
+				return 0;
+			}
+			for (var i=0; i < patterns.length; i++) {
+				if (patterns[i].test(location)) {
+					return i;
+				}
+			}
+			throw 'No Matching SiteService for ' + locationPropertyName + ': ' + location;
+		};
+		var serviceIndex = getServiceIndex(location);
+		var service = services[serviceIndex];
+		var serviceRef = references[serviceIndex];
+		return new SiteClient(serviceRegistry, service, serviceRef);
+	}
+
+	/**
+	 * @name SiteClient.forLocation
+	 * @static
+	 * @param {orion.serviceregistry.ServiceRegistry} serviceRegistry
+	 * @param {String} location Location of a site configuration.
+	 * @returns {orion.sites.SiteClient}
+	 */
+	function forLocation(serviceRegistry, location) {
+		return forLocationProperty(serviceRegistry, 'pattern', location);
+	}
+
+	/**
+	 * @name SiteClient.forLocation
+	 * @static
+	 * @param {orion.serviceregistry.ServiceRegistry} serviceRegistry
+	 * @param {String} location Location of a site configuration.
+	 */
+	function forFileLocation(serviceRegistry, fileLocation) {
+		return forLocationProperty(serviceRegistry, 'filePattern', fileLocation);
+	}
+
+	/**
+	 * @name orion.sites.SiteConfiguration
+	 * @class Interface for an in-memory representation of a site configuration resource. Objects of this
+	 * interface are used as parameters, and returned by, methods of the  {@link orion.sites.SiteService} API.
+	 * @property {String} Name The name of the site configuration.
+	 * @property {String} Workspace The workspace id that this site configuration is associated with.
+	 * @property {Array} Mappings The mappings defined by this site configuration. Each element has the properties 
+	 * <code>Source</code> and <code>Target</code>, both of type {@link String}. 
+	 * @property {String} [HostHint] A hint used to derive the domain name when the site is launched as a subdomain. 
+	 * @property {Object} HostingStatus Gives information about the status of this site configuration. Has the following properties:
+	 * <ul>
+	 * <li>{String} <code>Status</code> Status of this site. Value is either <code>'started'</code> or <code>'stopped'</code>.</li>
+	 * <li>{String} <code>URL</code> Optional, gives the URL where the running site can be accessed. Only present
+	 * if the <code>Status</code> is <code>'started'</code>.</li>
+	 * </ul>
+	 */
+
+	/**
+	 * @name orion.sites.SiteService
+	 * @class Interface for a service that manages site configurations.
+	 */
+	/**#@+
+	 * @methodOf orion.sites.SiteService.prototype
+	 */
+		/**
+		 * Creates a site configuration.
+		 * @name createSiteConfiguration
+		 * @param {String} name
+		 * @param {String} workspace
+		 * @param {Array} [mappings]
+		 * @param {String} [hostHint] 
+		 * @returns {orion.sites.SiteConfiguration} The created site configuration.
+		 */
+		/**
+		 * Deletes a site configuration.
+		 * @name deleteSiteConfiguration
+		 * @param {String} locationUrl Location of the site configuration resource to be deleted.
+		 * @returns {void}
+		 */
+		/**
+		 * Retrieves all site configurations defined by the logged-in user.
+		 * @name getSiteConfigurations
+		 * @returns {orion.sites.SiteConfiguration[]} The site configurations.
+		 */
+		/**
+		 * Loads an individual site configuration from the given location.
+		 * @name loadSiteConfiguration
+		 * @param {String} locationUrl Location URL of a site configuration resource.
+		 * @returns {orion.sites.SiteConfiguration} The loaded site configuration.
+		 */
+		/**
+		 * Edits an existing site configuration.
+		 * @name updateSiteConfiguration
+		 * @param {String} locationUrl Location of the site configuration resource to be updated.
+		 * @param {orion.sites.SiteConfiguration} updatedSiteConfig A representation of the updated site. Properties that are not changing
+		 * may be omitted.
+		 * @returns {orion.sites.SiteConfiguration} The updated site configuration.
+		 */
+		/**
+		 * @name toInternalForm
+		 * @param {String} fileLocation
+		 * @returns {String}
+		 */
+		/**
+		 * @name toFileLocation
+		 * @param {String} internalPath
+		 * @returns {String}
+		 */
+		/**
+		 * @name getMappingObject
+		 * @param {orion.sites.SiteConfiguration} site
+		 * @param {String} fileLocation
+		 * @param {String} virtualPath
+		 * @returns {Object}
+		 */
+		/**
+		 * @name getMappingProposals
+		 * @param {orion.sites.SiteConfiguration} site
+		 * @returns {String[]}
+		 */
+		/**
+		 * @name updateMappingsDisplayStrings
+		 * @param {orion.sites.SiteConfiguration} site
+		 * @returns {orion.sites.SiteConfiguration}
+		 */
+		/**
+		 * @name parseInternalForm
+		 * @param {orion.sites.SiteConfiguration} site
+		 * @param {String} displayString
+		 * @returns {String}
+		 */
+		/**
+		 * @name isSelfHostingSite
+		 * @param {orion.sites.SiteConfiguration} site
+		 * @returns {Boolean}
+		 */
+		/**
+		 * @name convertToSelfHosting
+		 * @param {orion.sites.SiteConfiguration} site
+		 * @returns {orion.sites.SiteConfiguration}
+		 */
+		/**
+		 * @name getURLOnSite
+		 * @param {orion.sites.SiteConfiguration} site
+		 * @param {Object} file
+		 * @returns {String}
+		 */
+	/**#@-*/
+
+	return {
+		forLocation: forLocation,
+		forFileLocation: forFileLocation,
+		getFileClient: getFileClient,
+		SiteClient: SiteClient
+	};
+});
diff --git a/bundles/org.eclipse.orion.client.core/web/orion/sites/siteCommands.js b/bundles/org.eclipse.orion.client.core/web/orion/sites/siteCommands.js
new file mode 100644
index 0000000..1f6e250
--- /dev/null
+++ b/bundles/org.eclipse.orion.client.core/web/orion/sites/siteCommands.js
@@ -0,0 +1,213 @@
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2011, 2012 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made 
+ * available under the terms of the Eclipse Public License v1.0 
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution 
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). 
+ * 
+ * Contributors: IBM Corporation - initial API and implementation
+ ******************************************************************************/
+
+/*global console define document window*/
+define(['require', 'orion/commands', 'orion/sites/siteUtils', 'orion/sites/siteClient', 'orion/fileClient'],
+		function(require, mCommands, mSiteUtils, mSiteClient) {
+	var Command = mCommands.Command;
+
+	/**
+	 * Creates & adds commands that act on an site service.
+	 * @param {orion.serviceregistry.ServiceRegistry} serviceRegistry
+	 * @param {Function} options.createCallback
+	 * @param {Function} options.errorCallback
+	 * @name orion.sites.siteCommands#createSiteServiceCommands
+	 */
+	function createSiteServiceCommands(serviceRegistry, options) {
+		function getFileService(siteServiceRef) {
+			return mSiteClient.getFileClient(serviceRegistry, siteServiceRef.getProperty('filePattern'));
+		}
+		options = options || {};
+		var commandService = serviceRegistry.getService("orion.page.command");
+		var createCommand = new mCommands.Command({
+			name : "Create",
+			tooltip: "Create a new site configuration",
+			id: "orion.site.create",
+			parameters: new mCommands.ParametersDescription([new mCommands.CommandParameter('name', 'string', 'Name:')]),
+			visibleWhen: function(bah) {
+				return true;
+			},
+			callback : function(data) {
+				var siteServiceRef = data.items, siteService = serviceRegistry.getService(siteServiceRef);
+				var fileService = getFileService(siteServiceRef);
+				var name = data.parameters && data.parameters.valueFor('name');
+				fileService.loadWorkspaces().then(function(workspaces) {
+			        var workspaceId = workspaces && workspaces[0] && workspaces[0].Id;
+			        if (workspaceId && name) {
+				        siteService.createSiteConfiguration(name, workspaceId).then(function(site) {
+							options.createCallback(mSiteUtils.generateEditSiteHref(site), site);
+						}, options.errorCallback);
+			        }
+				});
+			}});
+		commandService.addCommand(createCommand);
+	}
+
+	/**
+	 * Creates & adds commands that act on an individual site configuration.
+	 * @param {orion.serviceregistry.ServiceRegistry} serviceRegistry
+	 * @name orion.sites.siteCommands#createSiteCommands
+	 */
+	function createSiteCommands(serviceRegistry) {
+		var commandService = serviceRegistry.getService("orion.page.command"),
+		    dialogService = serviceRegistry.getService("orion.page.dialog"),
+		    progressService = serviceRegistry.getService("orion.page.progress");
+		var editCommand = new Command({
+			name: "Edit",
+			tooltip: "Edit the site configuration",
+			imageClass: "core-sprite-edit",
+			id: "orion.site.edit",
+			visibleWhen: function(item) {
+				return item.HostingStatus;
+			},
+			hrefCallback: function(data) { return mSiteUtils.generateEditSiteHref(data.items);}});
+		commandService.addCommand(editCommand);
+
+		var startCommand = new Command({
+			name: "Start",
+			tooltip: "Start the site",
+			imageClass: "core-sprite-start",
+			id: "orion.site.start",
+			visibleWhen: function(item) {
+				return item.HostingStatus && item.HostingStatus.Status === "stopped";
+			},
+			/**
+			 * @param {SiteConfiguration} [userData.site] If passed, we'll mutate this site config.
+			 * @param {Function} [userData.startCallback]
+			 * @param {Function} [userData.errorCallback]
+			 */
+			callback: function(data) {
+				var userData = data.userData;
+				var newItem = userData.site || {} /* just update the HostingStatus */;
+				newItem.HostingStatus = { Status: "started" };
+				var location = data.items.Location;
+				var siteService = mSiteClient.forLocation(serviceRegistry, location);
+				var deferred = siteService.updateSiteConfiguration(location, newItem);
+				progressService.showWhile(deferred).then(userData.startCallback, userData.errorCallback);
+			}});
+		commandService.addCommand(startCommand);
+
+		var stopCommand = new Command({
+			name: "Stop",
+			tooltip: "Stop the site",
+			imageClass: "core-sprite-stop",
+			id: "orion.site.stop",
+			visibleWhen: function(item) {
+				return item.HostingStatus && item.HostingStatus.Status === "started";
+			},
+			/**
+			 * @param {SiteConfiguration} [data.userData.site] If passed, we'll mutate this site config.
+			 * @param {Function} [data.userData.stopCallback]
+			 * @param {Function} [data.userData.errorCallback]
+			 */
+			callback: function(data) {
+				var userData = data.userData;
+				var newItem = userData.site || {} /* just update the HostingStatus */;
+				newItem.HostingStatus = { Status: "stopped" };
+				var location = data.items.Location;
+				var siteService = mSiteClient.forLocation(serviceRegistry, location);
+				var deferred = siteService.updateSiteConfiguration(location, newItem);
+				progressService.showWhile(deferred).then(userData.stopCallback, userData.errorCallback);
+			}});
+		commandService.addCommand(stopCommand);
+
+		var deleteCommand = new Command({
+			name: "Delete",
+			tooltip: "Delete the site configuration",
+			imageClass: "core-sprite-delete",
+			id: "orion.site.delete",
+			visibleWhen: function(item) {
+				return item.HostingStatus && item.HostingStatus.Status === "stopped";
+			},
+			/**
+			 * @param {Function} [data.userData.deleteCallback]
+			 * @param {Function} [data.userData.errorCallback]
+			 */
+			callback: function(data) {
+				var msg = "Are you sure you want to delete the site configuration '" + data.items.Name + "'?";
+				dialogService.confirm(msg, function(confirmed) {
+					if (confirmed) {
+						var location = data.items.Location;
+						var userData = data.userData;
+						var siteService = mSiteClient.forLocation(serviceRegistry, location);
+						siteService.deleteSiteConfiguration(location).then(userData.deleteCallback, userData.errorCallback);
+					}
+				});
+			}});
+		commandService.addCommand(deleteCommand);
+	}
+
+// TODO deal with this
+//	var workspacesCache = null;
+//
+//	function WorkspacesCache(fileService) {
+//		var promise = null;
+//		this.getWorkspaces = function() {
+//			if (!promise) {
+//				promise = fileService.loadWorkspaces();
+//			}
+//			return promise;
+//		};
+//	}
+//
+//	function initCache(serviceRegistry) {
+//		if (!workspacesCache) {
+//			workspacesCache = new WorkspacesCache(serviceRegistry.getService("orion.core.file"));
+//		}
+//	}
+	/**
+	 * @param {orion.serviceregistry.ServiceRegistry} serviceRegistry
+	 * @name orion.sites.siteCommands#createViewOnSiteCommands
+	 */
+	function createViewOnSiteCommands(serviceRegistry, options) {
+		var fileService = serviceRegistry.getService("orion.core.file");
+		var commandService = serviceRegistry.getService("orion.page.command");
+		commandService.addCommand(new Command({
+			name: "Add to site",
+			tooltip: "Add the file to this site",
+			id: "orion.site.add-to",
+			imageClass: "core-sprite-add",
+			visibleWhen: function(item) {
+				// Model tells us whether the file is running on the site configuration
+				return !item.IsFileRunningOn;
+			},
+			/**
+			 * @param {Function} data.userData.addToCallback
+			 * @param {Function} data.userData.errorCallback
+			 * @param {Object} data.userData.file
+			 */
+			callback: function(data) {
+				var file = data.userData.file;
+				var site = data.items.SiteConfiguration;
+				return fileService.loadWorkspaces().then(function(workspaces) {
+					return mSiteClient.forFileLocation(serviceRegistry, file.Location).mapOnSiteAndStart(site, file, workspaces[0].Id);
+				}).then(data.userData.addToCallback, data.userData.errorCallback);
+			}}));
+		// Command that generates a href to view the file on the site if it's mapped
+		commandService.addCommand(new Command({
+			name: "View",
+			tooltip: "View the file on the site",
+			id: "orion.site.view-on-link",
+			visibleWhen: function(item) {
+				return item.IsFileRunningOn;
+			},
+			hrefCallback: function(data) {
+				var file = data.userData.file;
+				var site = data.items.SiteConfiguration;
+				return mSiteClient.forFileLocation(serviceRegistry, file.Location).getURLOnSite(site, file);
+			}}));
+	}
+	return {
+		createSiteServiceCommands: createSiteServiceCommands,
+		createSiteCommands: createSiteCommands,
+		createViewOnSiteCommands: createViewOnSiteCommands
+	};
+});
\ No newline at end of file
diff --git a/bundles/org.eclipse.orion.client.core/web/orion/siteMappingsTable.js b/bundles/org.eclipse.orion.client.core/web/orion/sites/siteMappingsTable.js
similarity index 69%
rename from bundles/org.eclipse.orion.client.core/web/orion/siteMappingsTable.js
rename to bundles/org.eclipse.orion.client.core/web/orion/sites/siteMappingsTable.js
index e60f8e6..7e7c4e1 100644
--- a/bundles/org.eclipse.orion.client.core/web/orion/siteMappingsTable.js
+++ b/bundles/org.eclipse.orion.client.core/web/orion/sites/siteMappingsTable.js
@@ -1,6 +1,6 @@
 /*******************************************************************************
  * @license
- * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * Copyright (c) 2010, 2011, 2012 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials are made 
  * available under the terms of the Eclipse Public License v1.0 
  * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution 
@@ -8,11 +8,10 @@
  * 
  * Contributors: IBM Corporation - initial API and implementation
  ******************************************************************************/
-/*global define */
-/*jslint browser:true regexp:true */
+/*global define document */
 
-define(['require', 'dojo', 'dijit', 'orion/util', 'orion/commands', 'orion/explorer'],
-		function(require, dojo, dijit, mUtil, mCommands, mExplorer) {
+define(['require', 'dojo', 'orion/util', 'orion/commands', 'orion/explorer'],
+		function(require, dojo, mUtil, mCommands, mExplorer) {
 
 var mSiteMappingsTable = {};
 
@@ -22,10 +21,15 @@
 	}
 }
 
+// TODO perhaps site service should be in charge of this.
 function isWorkspacePath(/**String*/ path) {
 	return new RegExp("^/").test(path);
 }
 
+function safePath(str) {
+	return str.replace(/[\r\n\t]/g, "");
+}
+
 mSiteMappingsTable.Model = (function() {
 	function Model(rootPath, fetchItems, items) {
 		this.rootPath = rootPath;
@@ -78,7 +82,8 @@
 					col = dojo.create("td");
 					input = dojo.create("input");
 					dojo.addClass(input, "pathInput");
-					input.value = item.FriendlyPath;
+					// TODO
+					input.value = typeof item.FriendlyPath !== "undefined" ? item.FriendlyPath : item.Target;
 					handler = dojo.hitch(this, function(event) {
 							this.options.onchange(item, "FriendlyPath", event.target.value, event);
 						});
@@ -105,29 +110,23 @@
 		getIsValidCell: function(/**Number*/ col_no, /**Object*/ item, /**HTMLTableRowElement*/ tableRow) {
 			var target = item.Target;
 			var col = document.createElement("td");
-			var href, result;
+			var href;
 			if (isWorkspacePath(target)) {
-				var location = this.options.siteService.makeFullFilePath(target);
-				href = mUtil.safeText(location);
-				col.innerHTML = "<span class=\"validating\">&#8230;</span>";
-				// TODO: should use fileClient here, but without authentication prompt & without retrying
-				//this._fileClient.fetchChildren(location)
-				dojo.xhrGet({
-					url: location,
-					headers: { "Orion-Version": "1" },
-					handleAs: "text"
-				}).then(
-					function(object) {
-						try {
-							object = dojo.fromJson(object);
-						} catch(e) {}
-						var isDirectory = (typeof object === "object" && object.Directory);
-						var spriteClass = isDirectory ? "core-sprite-folder" : "core-sprite-file";
-						var title = (isDirectory ? "Workspace folder" : "Workspace file") + " " + href;
-						col.innerHTML = '<a href="' + href + '" target="_new"><span class="imageSprite ' + spriteClass + '" title="' + title + '"/></a>';
-					}, function(error) {
-						col.innerHTML = '<a href="' + href + '" target="_new"><span class="imageSprite core-sprite-error" title="Workspace resource not found: ' + href + '"/></a>';
-					});
+				var self = this;
+				this.options.siteClient.toFileLocation(target).then(function(loc) {
+					href = mUtil.safeText(loc);
+					col.innerHTML = "<span class=\"validating\">&#8230;</span>";
+					// use file service directly to avoid retrying in case of failure
+					self.options.fileClient._getService(loc).read(loc, true).then(
+						function(object) {
+							var isDirectory = object && object.Directory;
+							var spriteClass = isDirectory ? "core-sprite-folder" : "core-sprite-file";
+							var title = (isDirectory ? "Workspace folder" : "Workspace file") + " " + href;
+							col.innerHTML = '<a href="' + href + '" target="_new"><span class="imageSprite ' + spriteClass + '" title="' + title + '"/></a>';
+						}, function(error) {
+							col.innerHTML = '<a href="' + href + '" target="_new"><span class="imageSprite core-sprite-error" title="Workspace resource not found: ' + href + '"/></a>';
+						});
+				});
 			} else {
 				href = mUtil.safeText(target);
 				col.innerHTML = '<a href="' + href + '" target="_new"><span class="imageSprite core-sprite-link" title="External link to ' + href + '"/></a>';
@@ -147,17 +146,18 @@
 		this.registry = options.serviceRegistry;
 		this.commandService = this.registry.getService("orion.page.command");
 		this.registerCommands();
-		this.siteService = options.siteService;
+		this.siteClient = options.siteClient;
 		this.parentId = options.parentId;
 		this.selection = options.selection;
 		this.renderer = new mSiteMappingsTable.Renderer({
 				checkbox: false, /*TODO make true when we have selection-based commands*/
 				onchange: dojo.hitch(this, this.fieldChanged),
-				siteService: options.siteService,
+				siteConfiguration: options.siteConfiguration,
+				siteClient: options.siteClient,
+				fileClient: options.fileClient,
 				actionScopeId: "siteMappingCommand"
 			}, this);
 		this.myTree = null;
-		this.projectsPromise = options.projects;
 		this._setSiteConfiguration(options.siteConfiguration);
 		this.setDirty(false);
 	}
@@ -173,15 +173,13 @@
 				d.callback(this.siteConfiguration.Mappings);
 				return d;
 			});
-			dojo.when(this.projectsPromise, dojo.hitch(this, function(projects) {
-				this.projects = projects;
-				// Make FriendlyPath
-				this.siteConfiguration.Mappings = dojo.map(this.siteConfiguration.Mappings, dojo.hitch(this, function(mapping) {
-					return this.createMappingObject(mapping.Source, mapping.Target);
-				}));
+			// Refresh display names from the site service
+			var self = this;
+			this.siteClient.updateMappingsDisplayStrings(this.siteConfiguration).then(function(updatedSite) {
+				self.siteConfiguration.Mappings = updatedSite.Mappings;
 				// Build visuals
-				this.createTree(this.parentId, new mSiteMappingsTable.Model(null, fetchItems, this.siteConfiguration.Mappings));
-			}));
+				self.createTree(self.parentId, new mSiteMappingsTable.Model(null, fetchItems, self.siteConfiguration.Mappings));
+			});
 		},
 		render: function() {
 			this.changedItem(this.siteConfiguration.Mappings, this.siteConfiguration.Mappings);
@@ -204,7 +202,7 @@
 				id: "orion.site.mappings.remove",
 				visibleWhen: function(item) {
 					// Only show on a Mappings object
-					return item.Source && item.Target;
+					return typeof item.Source !== "undefined" && typeof item.Target !== "undefined";
 				},
 				callback: dojo.hitch(this, function(data) {
 					//table._hideTooltip();
@@ -257,23 +255,22 @@
 		getItemIndex: function(item) {
 			return this.siteConfiguration.Mappings.indexOf(item);
 		},
-		// TODO: use makeNewItemPlaceHolder() ?
-		_addMapping: function(/**String*/ source, /**String*/ target, /**String*/ friendlyPath) {
-			source = this.safePath(typeof(source) === "string" ? source : this.getNextMountPoint(friendlyPath));
-			target = this.safePath(typeof(target) === "string" ? target : "/");
+		_addMapping: function(object) {
+			var source = object.Souce, target = object.Target, friendlyPath = object.FriendlyPath;
+			source = safePath(typeof(source) === "string" ? source : this.getNextMountPoint(friendlyPath));
+			target = safePath(typeof(target) === "string" ? target : "/");
 			if (!this.mappingExists(source, target)) {
-				var newItem = this.createMappingObject(source, target);
-				this.siteConfiguration.Mappings.push(newItem);
+				this.siteConfiguration.Mappings.push(object);
 			}
 		},
-		addMapping: function(source, target, friendlyPath) {
-			this._addMapping(source, target, friendlyPath);
+		addMapping: function(object) {
+			this._addMapping(object);
 			this.render();
 			this.setDirty(true);
 		},
 		addMappings: function(mappings) {
 			for (var i=0; i < mappings.length; i++) {
-				this._addMapping(mappings[i].Source, mappings[i].Target);
+				this.addMapping(mappings[i]);
 			}
 			this.render();
 			this.setDirty(true);
@@ -316,9 +313,6 @@
 			}
 			return "/web/somePath";
 		},
-		createMappingObject: function(source, target) {
-			return {Source: source, Target: target, FriendlyPath: this.toFriendlyPath(target)};
-		},
 		deleteMapping: function(/**Object*/ mapping) {
 			var index = this.siteConfiguration.Mappings.indexOf(mapping);
 			if (index !== -1) {
@@ -329,55 +323,22 @@
 			this.siteConfiguration.Mappings.splice(0, this.siteConfiguration.Mappings.length);
 		},
 		fieldChanged: function(/**Object*/ item, /**String*/ fieldName, /**String*/ newValue, /**Event*/ event) {
-			newValue = this.safePath(newValue);
+			newValue = safePath(newValue);
 			var oldValue = item[fieldName];
 			if (oldValue !== newValue) {
 				item[fieldName] = newValue;
 				if (fieldName === "FriendlyPath") {
-					this.propagate(newValue, item);
-				}
-				this.renderItemRow(item, fieldName);
-				this.setDirty(true);
-			}
-		},
-		toFriendlyPath: function(target) {
-			var friendlyPath;
-			if (isWorkspacePath(target)) {
-				for (var i=0; i < this.projects.length; i++) {
-					var project = this.projects[i];
-					var name = "/" + project.Name;
-					var location = this.siteService.makeRelativeFilePath(project.Location);
-					if (this.pathsMatch(target, location)) {
-						friendlyPath = name + target.substring(location.length);
-						break;
-					}
+					// Convert displayed string into the internal path representation, update the Target field
+					var friendlyPath = newValue;
+					var self = this;
+					this.siteClient.parseInternalForm(this.siteConfiguration, friendlyPath).then(
+						function(internalPath) {
+							item.Target = internalPath || friendlyPath;
+							self.renderItemRow(item, fieldName);
+							self.setDirty(true);	
+						});
 				}
 			}
-			return friendlyPath || target;
-		},
-		/** Rewrites item's FriendlyPath using the project shortname and sets the result into the Target */
-		propagate: function(friendlyPath, item) {
-			if (isWorkspacePath(friendlyPath)) {
-				for (var i=0; i < this.projects.length; i++) {
-					var project = this.projects[i];
-					var name = "/" + project.Name;
-					var location = this.siteService.makeRelativeFilePath(project.Location);
-					if (this.pathsMatch(friendlyPath, name)) {
-						var rest = friendlyPath.substring(name.length);
-						item.Target = location + rest;
-						return;
-					}
-				}
-			}
-			// Bogus workspace path, or not a workspace path at all
-			item.Target = friendlyPath;
-		},
-		/** @returns true if b is a sub-path of a */
-		pathsMatch: function(a, b) {
-			return a === b || a.indexOf(b + "/") === 0;
-		},
-		safePath: function(str) {
-			return str.replace(/[\r\n\t]/g, "");
 		},
 		setDirty: function(value) {
 			this._isDirty = value;
diff --git a/bundles/org.eclipse.orion.client.core/web/orion/siteUtils.js b/bundles/org.eclipse.orion.client.core/web/orion/sites/siteUtils.js
similarity index 92%
rename from bundles/org.eclipse.orion.client.core/web/orion/siteUtils.js
rename to bundles/org.eclipse.orion.client.core/web/orion/sites/siteUtils.js
index abf383e..6f857bd 100644
--- a/bundles/org.eclipse.orion.client.core/web/orion/siteUtils.js
+++ b/bundles/org.eclipse.orion.client.core/web/orion/sites/siteUtils.js
@@ -11,11 +11,11 @@
 
 /*global define */
 
-define(['require', 'orion/util', 'orion/URITemplate', 'orion/siteUtils'],
+define(['require', 'orion/util', 'orion/URITemplate', 'orion/sites/siteUtils'],
 		function(require, mUtil, URITemplate) {
 	/**
 	 * Returns a relative URL pointing to the editing page for the given site configuration. 
-	 * @param {orion.siteService.SiteConfiguration} site The site configuration
+	 * @param {orion.siteClient.SiteConfiguration} site The site configuration
 	 * @return {String} The URL.
 	 * @name orion.siteUtils#generateEditSiteHref
 	 * @function
diff --git a/bundles/org.eclipse.orion.client.core/web/orion/sites/sitesExplorer.js b/bundles/org.eclipse.orion.client.core/web/orion/sites/sitesExplorer.js
new file mode 100644
index 0000000..09be505
--- /dev/null
+++ b/bundles/org.eclipse.orion.client.core/web/orion/sites/sitesExplorer.js
@@ -0,0 +1,401 @@
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2012 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors: IBM Corporation - initial API and implementation
+ ******************************************************************************/
+/*global define document*/
+define(['dojo', 'orion/Deferred', 'orion/section', 'orion/commands', 'orion/selection', 'orion/sites/siteUtils', 'orion/sites/siteClient', 
+		'orion/sites/siteCommands', 'orion/treetable'],
+		function(dojo, Deferred, mSection, mCommands, mSelection, mSiteUtils, mSiteClient, mSiteCommands, treetable) {
+	var Section = mSection.Section;
+	var TableTree = treetable.TableTree;
+	var SitesTree, ViewOnSiteTree;
+
+	/** 
+	 * @name orion.sites.SiteServicesExplorer
+	 * @class Section-based explorer showing the sites on each site service.
+	 * @param {orion.serviceregistry.ServiceRegistry} options.serviceRegistry
+	 * @param {DomNode} options.parent
+	 */
+	function SiteServicesExplorer(options) {
+		this.registry = options.serviceRegistry;
+		this.parentNode = options.parent;
+		this.siteServiceRefs = this.registry.getServiceReferences('orion.site');
+	}
+	SiteServicesExplorer.prototype = /** @lends orion.sites.SiteServicesExplorer.prototype */ {
+		display: function() {
+			var serviceRegistry = this.registry;
+			var commandService = serviceRegistry.getService('orion.page.command');
+			var serviceRefs = this.siteServiceRefs;
+			for (var i=0; i < serviceRefs.length; i++) {
+				var siteServiceRef = serviceRefs[i];
+				var siteService = this.registry.getService(siteServiceRef);
+				var siteClient = new mSiteClient.SiteClient(serviceRegistry, siteService, siteServiceRef);
+				var sectionId = 'section' + i;
+				var sitesNodeId = sectionId + 'siteNode';
+				var section = new Section(this.parentNode, {
+					explorer: this,
+					id: sectionId,
+					title: 'Site Configurations on ' + siteServiceRef.getProperty('name'),
+					content: '<div id="' + sitesNodeId + '" class="plugin-settings-list"></div>',
+					commandService: commandService,
+					serviceRegistry: serviceRegistry,
+					slideout: true
+				});
+				section.registerCommandContribution('orion.site.create', 100);
+				section.renderCommands(siteServiceRef, 'button');
+
+				var sectionItemActionScopeId = 'section' + i + 'Action';
+				commandService.registerCommandContribution(sectionItemActionScopeId, 'orion.site.edit', 10);
+				commandService.registerCommandContribution(sectionItemActionScopeId, 'orion.site.start', 20);
+				commandService.registerCommandContribution(sectionItemActionScopeId, 'orion.site.stop', 30);
+				commandService.registerCommandContribution(sectionItemActionScopeId, 'orion.site.delete', 40);
+				var refresher = (function(section) {
+					return function() {
+						section.tree.refresh();
+					};
+				}(section));
+				section.tree = new SitesTree({
+					id: sitesNodeId + 'tree',
+					parent: sitesNodeId,
+					actionScopeId: sectionItemActionScopeId,
+					serviceRegistry: this.registry,
+					siteService: siteClient,
+					startCallback: refresher,
+					stopCallback: refresher,
+					deleteCallback: refresher
+				});
+			}
+		},
+	};
+
+	var SiteTreeModel = (function() {
+		/**
+		 * @name orion.sites.SiteTreeModel
+		 * @class Tree model for powering a tree of site configurations.
+		 * @see orion.treetable.TableTree
+		 * @private
+		 */
+		function SiteTreeModel(siteService, id) {
+			this._siteService = siteService;
+			this._root = {};
+			this._id = id;
+		}
+		SiteTreeModel.prototype = /** @lends orion.sites.SiteTreeModel.prototype */{
+			getRoot: function(/**function*/ onItem) {
+				onItem(this._root);
+			},
+			getChildren: function(/**dojo.data.Item*/ parentItem, /**Function(items)*/ onComplete) {
+				if (parentItem.children) {
+					// The parent already has the children fetched
+					onComplete(parentItem.children);
+				} else if (parentItem === this._root) {
+					this._siteService.getSiteConfigurations().then(
+						function(/**Array*/ siteConfigurations) {
+							parentItem.children = siteConfigurations;
+							onComplete(siteConfigurations);
+						});
+				} else {
+					return onComplete([]);
+				}
+			},
+			getId: function(/**dojo.data.Item|String*/ item) {
+				return (item === this._root || item === this._id) ? this._id : item.Id;
+			}
+		};
+		return SiteTreeModel;
+	}());
+	
+	var SitesRenderer = (function() {
+		/**
+		 * @name orion.sites.SitesRenderer
+		 * @class A renderer for a list of site configurations obtained from a site service.
+		 * @see orion.treetable.TableTree
+		 * @private
+		 */
+		function SitesRenderer(options) {
+			this._commandService = options.serviceRegistry.getService("orion.page.command");
+			this._options = options;
+		}
+		SitesRenderer.prototype = /** @lends orion.sites.SitesRenderer.prototype */{
+			initTable: function (tableNode, tableTree) {
+				this.tableTree = tableTree;
+				dojo.addClass(tableNode, "treetable");
+				var thead = dojo.create("thead", null);
+				dojo.create("th", {innerHTML: "Name"}, thead, "last");
+				dojo.create("th", {innerHTML: "Status"}, thead, "last");
+				dojo.create("th", {innerHTML: "URL", className: "urlCol"}, thead, "last");
+				dojo.create("th", {innerHTML: "Actions"}, thead, "last");
+				tableNode.appendChild(thead);
+			},
+			render: function(item, tableRow) {
+				dojo.addClass(tableRow, "treeTableRow sitesTableRow");
+				
+				var siteConfigCol = dojo.create("td", {id: tableRow.id + "col1"});
+				var statusCol = dojo.create("td", {id: tableRow.id + "col2"});
+				var urlCol = dojo.create("td", {id: tableRow.id + "col3"});
+				var actionCol = dojo.create("td", {id: tableRow.id + "col4"});
+				
+				// Site config column
+				var href = mSiteUtils.generateEditSiteHref(item);
+				var nameLink = dojo.create("a", {href: href}, siteConfigCol, "last");
+				dojo.place(document.createTextNode(item.Name), nameLink, "last");
+				
+				// Status, URL columns
+				var status = item.HostingStatus;
+				if (typeof status === "object") {
+					if (status.Status === "started") {
+						dojo.place(document.createTextNode("Started"), statusCol, "last");
+						var link = dojo.create("a", {className: "siteURL"}, urlCol, "last");
+						dojo.place(document.createTextNode(status.URL), link, "only");
+						link.href = status.URL;
+					} else {
+						var statusString = status.Status.substring(0,1).toUpperCase() + status.Status.substring(1);
+						dojo.place(document.createTextNode(statusString), statusCol, "only");
+					}
+				} else {
+					dojo.place(document.createTextNode("Unknown"), statusCol, "only");
+				}
+				
+				// Action column
+				var actionsWrapper = dojo.create("span", {id: tableRow.id + "actionswrapper"}, actionCol, "only");
+				var options = this._options;
+				var userData = {
+					startCallback: options.startCallback,
+					stopCallback: options.stopCallback,
+					deleteCallback: options.deleteCallback,
+					errorCallback: function(err) {
+						options.serviceRegistry.getService('orion.page.message').setProgressResult(err);
+					}
+				};
+				this._commandService.renderCommands(options.actionScopeId, actionsWrapper, item, null /*handler*/, "tool", userData);
+				
+				dojo.place(siteConfigCol, tableRow, "last");
+				dojo.place(statusCol, tableRow, "last");
+				dojo.place(urlCol, tableRow, "last");
+				dojo.place(actionCol, tableRow, "last");
+			},
+			rowsChanged: function() {
+				dojo.query(".treeTableRow").forEach(function(node, i) {
+					if (i % 2) {
+						dojo.addClass(node, "darkTreeTableRow");
+						dojo.removeClass(node, "lightTreeTableRow");
+					} else {
+						dojo.addClass(node, "lightTreeTableRow");
+						dojo.removeClass(node, "darkTreeTableRow");
+					}
+				});
+			},
+			labelColumnIndex: 0
+		};
+		return SitesRenderer;
+	}());
+	
+	var ViewOnSiteTreeModel = (function() {
+		function createModelItems(siteConfigurations, isRunningOns) {
+			function ViewOnSiteModelItem(siteConfig, isFileRunningOn) {
+				this.SiteConfiguration = siteConfig;
+				this.Id = "ViewOnSite" + siteConfig.Id;
+				// Model keeps track of whether the file is available on this site configuration
+				this.IsFileRunningOn = isFileRunningOn;
+			}
+			var modelItems = [];
+			modelItems.push(
+				{	Id: "newsite",
+					Placeholder: true
+				});
+			for (var i=0; i < siteConfigurations.length; i++) {
+				modelItems.push(new ViewOnSiteModelItem(siteConfigurations[i], isRunningOns[i]));
+			}
+			return modelItems;
+		}
+		/** @returns {Deferred} */
+		function isFileRunningOnSite(siteService, site, file) {
+			return siteService.isFileMapped(site, file).then(function(isFileMapped) {
+				var isStarted = site.HostingStatus && site.HostingStatus.Status === "started";
+				return site && file && isStarted && isFileMapped;
+			});
+		}
+		/**
+		 * @param {Object} file
+		 */
+		function ViewOnSiteTreeModel(siteService, id, file) {
+			SiteTreeModel.call(this, siteService, id);
+			this._file = file;
+		}
+		ViewOnSiteTreeModel.prototype = {
+			getRoot: SiteTreeModel.prototype.getRoot,
+			getId: SiteTreeModel.prototype.getId,
+			getChildren: function(parentItem, onComplete) {
+				if (parentItem.children) {
+					onComplete(parentItem.children);
+				} else if (parentItem === this._root) {
+					var self = this;
+					ViewOnSiteTreeModel.createViewOnSiteModelItems(self._siteService, self._file).then(function(modelItems) {
+						parentItem.children = modelItems;
+						onComplete(modelItems);
+					});
+				} else {
+					onComplete([]);
+				}
+			}
+		};
+		/** @returns {Deferred} */
+		ViewOnSiteTreeModel.createViewOnSiteModelItems = function(siteService, file) {
+			return siteService.getSiteConfigurations().then(function(siteConfigurations) {
+				return new Deferred().all(siteConfigurations.map(function(site) {
+					return isFileRunningOnSite(siteService, site, file);
+				})).then(function(isRunningOns) {
+					return createModelItems(siteConfigurations, isRunningOns);
+				});
+			});
+		};
+		return ViewOnSiteTreeModel;
+	}());
+	
+	var ViewOnSiteRenderer = (function() {
+		/**
+		 * @param {Object} options.file
+		 * @param {Function} options.addToCallback
+		 * @param {Function} options.errorCallback
+		 */
+		function ViewOnSiteRenderer(options) {
+			SitesRenderer.apply(this, Array.prototype.slice.call(arguments));
+			this.serviceRegistry = options.serviceRegistry;
+			this.file = options.file;
+			this.addToCallback = options.addToCallback;
+			this.errorCallback = options.errorCallback;
+		}
+		ViewOnSiteRenderer.prototype = {
+			initTable: function (tableNode, tableTree) {
+				this.tableTree = tableTree;
+				dojo.addClass(tableNode, "treetable");
+				var thead = dojo.create("thead", null);
+				dojo.create("th", {innerHTML: "Name"}, thead, "last");
+				dojo.create("th", {innerHTML: "Actions"}, thead, "last");
+				tableNode.appendChild(thead);
+			},
+			render: function(item, tableRow) {
+				var siteConfig = item.SiteConfiguration;
+				dojo.addClass(tableRow, "treeTableRow sitesTableRow");
+				if (item.Placeholder) {
+					dojo.addClass(tableRow, "newSiteRow");
+				}
+				var siteConfigCol = dojo.create("td", {id: tableRow.id + "col1"});
+				var actionCol = dojo.create("td", {id: tableRow.id + "col2"});
+				
+				// Site config column
+				var name = item.Placeholder ? "New Site" : siteConfig.Name;
+				dojo.place(document.createTextNode(name), siteConfigCol, "last");
+
+				// Action column
+				var actionsWrapper = dojo.create("span", {id: tableRow.id + "actionswrapper"}, actionCol, "only");
+
+				var userData = {
+					file: this.file,
+					addToCallback: this.addToCallback,
+					errorCallback: this.errorCallback
+				};
+				this._commandService.renderCommands("viewOnSiteScope", actionsWrapper, item,  null /*handler*/, "tool", userData);
+
+				dojo.place(siteConfigCol, tableRow, "last");
+				dojo.place(actionCol, tableRow, "last");
+			},
+			rowsChanged: SitesRenderer.prototype.rowsChanged,
+			labelColumnIndex: 0
+		};
+		return ViewOnSiteRenderer;
+	}());
+
+	/**
+	 * @param {orion.sites.SiteService} options.siteService
+	 * @param {String} options.id
+	 * @param {DomNode} options.parent
+	 * @param {orion.sites.SiteTreeModel} [options.model]
+	 * @param {orion.sites.SitesRenderer} [options.renderer]
+	 * @class
+	 * @private
+	 */
+	SitesTree = (function() {
+		function SitesTree(options) {
+			this.siteService = options.siteService;
+			var model = this.model = options.model || new SiteTreeModel(this.siteService, options.id);
+			this.treeWidget = new TableTree({
+				id: options.id,
+				parent: options.parent,
+				model: model,
+				showRoot: false,
+				renderer: options.renderer || new SitesRenderer(options)
+			});
+		}
+		SitesTree.prototype = {
+			refresh: function() {
+				var self = this;
+				this.siteService.getSiteConfigurations().then(function(siteConfigs) {
+					self.treeWidget.refresh(self.model._id, siteConfigs, true);
+				});
+			}
+		};
+		return SitesTree;
+	}());
+
+	/**
+	 * @name orion.sites.ViewOnSiteTree
+	 * @class A tree widget that displays a list of sites that a file can be viewed on.
+	 * @param {orion.serviceregistry.ServiceRegistry} options.serviceRegistry
+	 * @param {String} options.fileLocation
+	 *
+	 * @param {String} options.id
+	 * @param {DomNode} options.parent
+	 * @param {orion.sites.SiteTreeModel} [options.model]
+	 * @param {orion.sites.SitesRenderer} [options.renderer]
+	 */
+	ViewOnSiteTree = (function() {
+		function ViewOnSiteTree(options) {
+			var serviceRegistry = options.serviceRegistry;
+			var commandService = serviceRegistry.getService("orion.page.command");
+			var siteService = mSiteClient.forFileLocation(serviceRegistry, options.fileLocation);
+			var self = this;
+			serviceRegistry.getService("orion.core.file").read(options.fileLocation, true).then(function(file) {
+				options.siteService = siteService;
+				options.model = new ViewOnSiteTreeModel(siteService, options.id, file);
+				options.file = self.file = file;
+
+				// TODO should this be done by glue code?
+				commandService.registerCommandContribution("viewOnSiteScope", "orion.site.add-to", 10);
+				commandService.registerCommandContribution("viewOnSiteScope", "orion.site.view-on-link", 20);
+
+				options.addToCallback = function() {
+					self.refresh();
+				};
+				options.errorCallback = function(err) {
+					options.serviceRegistry.getService('orion.page.message').setErrorMessage(err);
+				};
+
+				options.renderer = new ViewOnSiteRenderer(options);
+				SitesTree.call(self, options);
+			});
+		}
+		ViewOnSiteTree.prototype = /** @lends orion.sites.ViewOnSiteTree.prototype */ {
+			refresh: function() {
+				// TODO call helper for this
+				var self = this;
+				ViewOnSiteTreeModel.createViewOnSiteModelItems(self.siteService, self.file).then(
+					function(modelItems) {
+						self.treeWidget.refresh(self.model._id, modelItems, true);
+					});
+			}
+		};
+		return ViewOnSiteTree;
+	}());
+
+	return {
+		SiteServicesExplorer: SiteServicesExplorer,
+		ViewOnSiteTree: ViewOnSiteTree
+	};
+});
\ No newline at end of file
diff --git a/bundles/org.eclipse.orion.client.core/web/orion/widgets/SiteEditor.js b/bundles/org.eclipse.orion.client.core/web/orion/widgets/SiteEditor.js
index 3480a92..4040daf 100644
--- a/bundles/org.eclipse.orion.client.core/web/orion/widgets/SiteEditor.js
+++ b/bundles/org.eclipse.orion.client.core/web/orion/widgets/SiteEditor.js
@@ -1,6 +1,6 @@
 /*******************************************************************************

- * @license
- * Copyright (c) 2010, 2011 IBM Corporation and others.

+ * @license

+ * Copyright (c) 2010, 2011, 2012 IBM Corporation and others.

  * All rights reserved. This program and the accompanying materials are made 

  * available under the terms of the Eclipse Public License v1.0 

  * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution 

@@ -11,7 +11,7 @@
 /*global define orion*/

 /*jslint browser:true */

 

-define(['require', 'dojo', 'dijit', 'orion/util', 'orion/commands', 'orion/siteMappingsTable',

+define(['require', 'dojo', 'dijit', 'orion/util', 'orion/commands', 'orion/sites/siteMappingsTable',

 		'orion/widgets/DirectoryPrompterDialog', 'text!orion/widgets/templates/SiteEditor.html',

 		'dojo/DeferredList', 'dijit/layout/ContentPane', 'dijit/Tooltip', 'dijit/_Templated',

 		'dijit/form/Form', 'dijit/form/TextBox', 'dijit/form/ValidationTextBox'],

@@ -23,24 +23,11 @@
  * @name orion.widgets.SiteEditor

  * @class Editor for an individual site configuration.

  * @param {Object} options Options bag for creating the widget.

- * @param {orion.fileClient.FileClient} options.fileClient

- * @param {orion.sites.SiteService} options.siteService

- * @param {orion.commands.CommandService} options.commandService

- * @param {String} [options.location] Optional URL of a site configuration to load in editor

- * upon creation.

  */

 dojo.declare("orion.widgets.SiteEditor", [dijit.layout.ContentPane, dijit._Templated], {

 	widgetsInTemplate: true,

 	templateString: dojo.cache('orion', 'widgets/templates/SiteEditor.html'),

-	

-	/** dojo.Deferred */

-	_workspaces: null,

-	

-	/** dojo.Deferred */

-	_projects: null,

-	

-	_fetched: false,

-	

+

 	/** SiteConfiguration */

 	_siteConfiguration: null,

 	

@@ -49,7 +36,11 @@
 	

 	/** MappingsTable */

 	mappings: null,

-	

+

+	_mappingProposals: null,

+

+	_isSelfHostingSite: false,

+

 	_isDirty: false,

 	

 	_autoSaveTimer: null,

@@ -57,10 +48,10 @@
 	constructor: function() {

 		this.inherited(arguments);

 		this.options = arguments[0] || {};

-		this.checkOptions(this.options, ["serviceRegistry", "fileClient", "siteService", "commandService", "statusService", "progressService"]);

+		this.checkOptions(this.options, ["serviceRegistry", "fileClient", "siteClient", "commandService", "statusService", "progressService"]);

 

 		this._fileClient = this.options.fileClient;

-		this._siteService = this.options.siteService;

+		this._siteClient = this.options.siteClient;

 		this._commandService = this.options.commandService;

 		this._statusService = this.options.statusService;

 		this._progressService = this.options.progressService;

@@ -70,9 +61,6 @@
 		if (this.options.location) {

 			this.load(this.options.location);

 		}

-		

-		this._workspaces = this._fileClient.loadWorkspaces();

-		this._projects = new dojo.Deferred();

 	},

 	

 	postMixInProperties: function() {

@@ -99,40 +87,23 @@
 		}));

 		

 		dijit.byId("siteForm").onSubmit = dojo.hitch(this, this.save);

-		

-		dojo.when(this._projects, dojo.hitch(this, function(projects) {

-			// Register command used for adding mapping

-			var addMappingCommand = new mCommands.Command({

-				name: "Add",

-				tooltip: "Add a directory mapping to the site configuration",

-				imageClass: "core-sprite-add",

-				id: "orion.site.mappings.add",

-				visibleWhen: function(item) {

-					return true;

-				},

-				choiceCallback: dojo.hitch(this, this._makeAddMenuChoices, projects)});

-			this._commandService.addCommand(addMappingCommand);

-			var toolbarId = this.addMappingToolbar.id;

-			this._commandService.registerCommandContribution(toolbarId, "orion.site.mappings.add", 1);

-			this._commandService.renderCommands(toolbarId, this.addMappingToolbar, this.mappings, this, "button");

-			

+

+		// "Convert to self hosting" command

+		var self = this;

+		dojo.when(this.siteClient._canSelfHost(), function(canSelfHost) {

 			var convertCommand = new mCommands.Command({

 				name: "Convert to Self-Hosting",

 				tooltip: "Enable the site configuration to launch an Orion server running your local client code",

 				imageClass: "core-sprite-add",

 				id: "orion.site.convert",

-				visibleWhen: dojo.hitch(this, function(item) {

-					// Only applies to SiteConfiguration objects

-					return !!item.Location && !this.isSelfHosting(projects);

-				}),

-				callback: dojo.hitch(this, this.convertToSelfHostedSite, this._projects)});

-			this._commandService.addCommand(convertCommand);

-			

-			this._refreshFields();

+				visibleWhen: function(item) {

+					return !!item.Location && canSelfHost && !self._isSelfHostingSite;

+				},

+				// FIXME selfhosting 

+				callback: dojo.hitch(self, self.convertToSelfHostedSite)});

+			self._commandService.addCommand(convertCommand);

+		});

 

-			this._autoSaveTimer = setTimeout(dojo.hitch(this, this.autoSave), AUTOSAVE_INTERVAL);

-		}));

-		

 		// Save command

 		var saveCommand = new mCommands.Command({

 				name: "Save",

@@ -144,6 +115,8 @@
 				},

 				callback: dojo.hitch(this, this.save)});

 		this._commandService.addCommand(saveCommand);

+

+		this._autoSaveTimer = setTimeout(dojo.hitch(this, this.autoSave), AUTOSAVE_INTERVAL);

 	},

 	

 	checkOptions: function(options, names) {

@@ -155,44 +128,39 @@
 	},

 	

 	/**

-	 * this._projects must be resolved before this is called

-	 * @param {Array} Projects in workspace

+	 * @param {Array} proposals 

 	 * @param {Array|Object} items

 	 * @param {Object} userData

 	 * @returns {Array}

 	 */

-	_makeAddMenuChoices: function(projects, items, userData) {

+	_makeAddMenuChoices: function(proposals, items, userData) {

 		items = dojo.isArray(items) ? items[0] : items;

-		var workspaceId = this.getSiteConfiguration().Workspace;

-		projects = projects.sort(function(projectA, projectB) {

-				return projectA.Name.toLowerCase().localeCompare(projectB.Name.toLowerCase());

+		proposals = proposals.sort(function(a, b) {

+				return a.FriendlyPath.toLowerCase().localeCompare(b.FriendlyPath.toLowerCase());

 			});

-		

+		var self = this;

 		/**

-		 * @this An object from the choices array with shape {name:String, path:String, callback:Function}

-		 * @param {Object} item

+		 * @this An object from the choices array with shape {name:String, mapping:Object}

 		 */

-		var editor = this;

-		var addMappingCallback = function(data) {

-			editor.mappings.addMapping(null, this.path, this.name);

+		var callback = function(data) {

+			self.mappings.addMapping(this.mapping);

 		};

-//		var addOther = function() {

-//			editor.mappings.addMapping("/mountPoint", "/FolderId/somepath");

-//		};

 		var addUrl = function() {

-			editor.mappings.addMapping("/web/somePath", "http://");

+			self.mappings.addMapping({

+				Source: "/web/somePath",

+				Target: "http://",

+				FriendlyPath: "http://"

+			});

 		};

-		

-		var choices = dojo.map(projects, function(project) {

+		var choices = proposals.map(function(proposal) {

 				return {

-					name: "/" + project.Name,

+					name: proposal.FriendlyPath,

 					imageClass: "core-sprite-folder",

-					path: editor._siteService.makeRelativeFilePath(project.Location),

-					callback: addMappingCallback

+					mapping: proposal,

+					callback: callback

 				};

 			});

-		

-		if (projects.length > 0) {

+		if (proposals.length > 0) {

 			choices.push({}); // Separator

 		}

 		choices.push({

@@ -204,7 +172,10 @@
 					fileClient: this.fileClient,

 					func: dojo.hitch(this, function(folder) {

 						if (!!folder) {

-							this.mappings.addMapping(null, editor._siteService.makeRelativeFilePath(folder.Location), folder.Name);

+							this._siteClient.getMappingObject(this.getSiteConfiguration(), folder.Location, folder.Name).then(

+								function(mapping) {

+									callback.call({mapping: mapping});

+								});

 						}

 					})});

 				dialog.startup();

@@ -215,16 +186,20 @@
 	},

 

 	// Special feature for setting up self-hosting

-	convertToSelfHostedSite: function(projectsPromise, items, userData) {

+	// TODO ideally this command would be defined entirely by a plugin. It is here because of the DirectoryPrompter dependency

+	convertToSelfHostedSite: function(items, userData) {

 		var dialog = new orion.widgets.DirectoryPrompterDialog({

 			serviceRegistry: this.serviceRegistry,

 			fileClient: this.fileClient,

 			func: dojo.hitch(this, function(folder) {

 				if (folder) {

-					var path = this._siteService.makeRelativeFilePath(folder.Location);

-					this.mappings.deleteAllMappings();

-					this.mappings.addMappings(this.getSelfHostingMappings(path));

-					this.save();

+					var self = this;

+					this._siteClient.convertToSelfHosting(this.getSiteConfiguration(), folder.Location).then(

+						function(updatedSite) {

+							self.mappings.deleteAllMappings();

+							self.mappings.addMappings(updatedSite.Mappings);

+							self.save();

+						});

 				}

 			}),

 			title: "Choose Orion Source Folder",

@@ -233,116 +208,6 @@
 		dialog.show();

 	},

 	

-	isSelfHosting: function(projects) {

-		for (var i=0; i < projects.length; i++) {

-			var path = this._siteService.makeRelativeFilePath(projects[i].Location);

-			var selfHostingMappings = this.getSelfHostingMappings(path);

-			var pass = true;

-			for (var j=0; j < selfHostingMappings.length; j++) {

-				if (!this.mappings.mappingExists(selfHostingMappings[j])) {

-					pass = false;

-				}

-			}

-			if (pass) {

-				return true;

-			}

-		}

-		return false;

-	},

-	

-	getSelfHostingMappings: function(clientRepoPath) {

-		var context = this._siteService.getContext();

-		context = this._siteService._makeHostRelative(context);

-		// TODO: prompt for port? It is not detectable from client side if proxy is used

-		var hostPrefix = "http://localhost" + ":" + "8080" + context;

-		return [

-			{ Source: "/",

-			  Target: clientRepoPath + "/bundles/org.eclipse.orion.client.core/web/index.html"

-			},

-			{ Source: "/",

-			  Target: clientRepoPath + "/bundles/org.eclipse.orion.client.core/web"

-			},

-			{ Source: "/",

-			  Target: clientRepoPath + "/bundles/org.eclipse.orion.client.editor/web"

-			},

-			{ Source: "/org.dojotoolkit/dojo",

-			  Target: clientRepoPath + "/bundles/org.eclipse.orion.client.core/web/dojo"

-			},

-			{ Source: "/org.dojotoolkit/dojox",

-			  Target: clientRepoPath + "/bundles/org.eclipse.orion.client.core/web/dojox"

-			},

-			{ Source: "/file",

-			  Target: hostPrefix + "file"

-			},

-			{ Source: "/prefs",

-			  Target: hostPrefix + "prefs"

-			},

-			{ Source: "/workspace",

-			  Target: hostPrefix + "workspace"

-			},

-			{ Source: "/org.dojotoolkit",

-			  Target: hostPrefix + "org.dojotoolkit"

-			},

-			{ Source: "/users",

-			  Target: hostPrefix + "users"

-			},

-			{ Source: "/authenticationPlugin.html",

-			  Target: hostPrefix + "authenticationPlugin.html"

-			},

-			{ Source: "/login",

-			  Target: hostPrefix + "login"

-			},

-			{ Source: "/loginstatic",

-			  Target: hostPrefix + "loginstatic"

-			},

-			{ Source: "/site",

-			  Target: hostPrefix + "site"

-			},

-			{ Source: "/",

-			  Target: clientRepoPath + "/bundles/org.eclipse.orion.client.git/web"

-			},

-			{ Source: "/gitapi",

-			  Target: hostPrefix + "gitapi"

-			},

-			{ Source: "/",

-			  Target: clientRepoPath + "/bundles/org.eclipse.orion.client.users/web"

-			},

-			{ Source: "/xfer",

-			  Target: hostPrefix + "xfer"

-			},

-			{ Source: "/filesearch",

-			  Target: hostPrefix + "filesearch"

-			},

-			{ Source: "/index.jsp",

-			  Target: hostPrefix + "index.jsp"

-			},

-			{ Source: "/plugins/git",

-			  Target: hostPrefix + "plugins/git"

-			},

-			{ Source: "/plugins/user",

-			  Target: hostPrefix + "plugins/user"

-			},

-			{ Source: "/logout",

-			  Target: hostPrefix + "logout"

-			},

-			{ Source: "/mixloginstatic",

-			  Target: hostPrefix + "mixloginstatic"

-			},

-			{ Source: "/mixlogin/manageopenids",

-			  Target: hostPrefix + "mixlogin/manageopenids"

-			},

-			{ Source: "/openids",

-			  Target: hostPrefix + "openids"

-			},

-			{ Source: "/task",

-			  Target: hostPrefix + "task"

-			},

-			{ Source: "/help",

-			  Target: hostPrefix + "help"

-			}

-		];

-	},

-	

 	/**

 	 * Loads site configuration from a URL into the editor.

 	 * @param {String} location URL of the site configuration to load.

@@ -351,7 +216,7 @@
 	load: function(location) {

 		var deferred = new dojo.Deferred();

 		this._busyWhile(deferred, "Loading...");

-		this._siteService.loadSiteConfiguration(location).then(

+		this._siteClient.loadSiteConfiguration(location).then(

 			dojo.hitch(this, function(siteConfig) {

 				this._setSiteConfiguration(siteConfig);

 				this.setDirty(false);

@@ -362,37 +227,33 @@
 			});

 		return deferred;

 	},

-	

-	/**

-	 * Fetches top-level children of the siteConfiguration's workspace

-	 * @returns {dojo.Deferred}

-	 */

-	_fetchProjects: function(siteConfiguration) {

-		if (!this._fetched) {

-			dojo.when(this._workspaces, dojo.hitch(this, function(workspaces) {

-				var workspace;

-				for (var i=0; i < workspaces.length; i++) {

-					if (workspaces[i].Id === siteConfiguration.Workspace) {

-						workspace = workspaces[i];

-						break;

-					}

-				}

-				if (workspace) {

-					this._fileClient.fetchChildren(workspace.Location).then(

-						dojo.hitch(this, function(projects) {

-							this._fetched = true;

-							this._projects.callback(projects);

-						}),

-						dojo.hitch(this, this._onError));

-				}

-			}));

-		}

-	},

 

 	_setSiteConfiguration: function(siteConfiguration) {

 		this._detachListeners();

 		this._siteConfiguration = siteConfiguration;

-		this._fetchProjects(siteConfiguration);

+

+		// Ask the service for the proposals to put in the dropdown menu

+		if (!this._mappingProposals) {

+			this._mappingProposals = this._siteClient.getMappingProposals(siteConfiguration).then(dojo.hitch(this, function(proposals) {

+				// Register command used for adding mapping

+				var addMappingCommand = new mCommands.Command({

+					name: "Add",

+					tooltip: "Add a directory mapping to the site configuration",

+					imageClass: "core-sprite-add",

+					id: "orion.site.mappings.add",

+					visibleWhen: function(item) {

+						return true;

+					},

+					choiceCallback: dojo.hitch(this, this._makeAddMenuChoices, proposals)});

+				this._commandService.addCommand(addMappingCommand);

+				var toolbarId = this.addMappingToolbar.id;

+				this._commandService.registerCommandContribution(toolbarId, "orion.site.mappings.add", 1);

+				// do we really have to render here

+				this._commandService.renderCommands(toolbarId, this.addMappingToolbar, this.mappings, this, "button");

+			}));

+		}

+

+		this._refreshCommands();

 		this._refreshFields();

 	},

 	

@@ -403,15 +264,37 @@
 	isDirty: function() {

 		return this._isDirty;

 	},

-	

+

+	// Called after setSiteConfiguration and after every save/autosave

+	_refreshCommands: function() {

+		var self = this;

+		function errorHandler(err) {

+			self._onError(err);

+		}

+		function reload(site) {

+			self._setSiteConfiguration(site);

+		}

+		this._siteClient.isSelfHostingSite(this.getSiteConfiguration()).then(function(isSelfHostingSite) {

+			self._isSelfHostingSite = isSelfHostingSite;

+			dojo.empty(self._commandsContainer);

+			var userData = {

+				site: self._siteConfiguration,

+				startCallback: reload,

+				stopCallback: reload,

+				errorCallback: errorHandler

+			};

+			self._commandService.renderCommands(self._commandsContainer.id, self._commandsContainer, self._siteConfiguration, {}, "button", userData);

+		});

+	},

+

 	_refreshFields: function() {

 		this.name.set("value", this._siteConfiguration.Name);

 		this.hostHint.set("value", this._siteConfiguration.HostHint);

 

 		if (!this.mappings) {

 			this.mappings = new mSiteMappingsTable.MappingsTable({serviceRegistry: this.serviceRegistry,

-					siteService: this._siteService, selection: null, parentId: this.mappingsPlaceholder.id,

-					siteConfiguration: this._siteConfiguration, projects: this._projects /**dojo.Deferred*/

+					siteClient: this._siteClient, fileClient: this._fileClient, selection: null, 

+					parentId: this.mappingsPlaceholder.id, siteConfiguration: this._siteConfiguration

 				});

 		} else {

 			this.mappings._setSiteConfiguration(this._siteConfiguration);

@@ -426,10 +309,6 @@
 			dojo.style(this.siteStartedWarning, {display: "none"});

 			mUtil.setText(this.hostingStatus, hostStatus.Status[0].toLocaleUpperCase() + hostStatus.Status.substr(1));

 		}

-		

-		dojo.empty(this._commandsContainer);

-		this._commandService.renderCommands(this._commandsContainer.id, this._commandsContainer, this._siteConfiguration, {},

-			"button", this._siteConfiguration /*userData*/);

 

 		setTimeout(dojo.hitch(this, function() {

 			this._attachListeners(this._siteConfiguration);

@@ -495,12 +374,12 @@
 		var form = dijit.byId("siteForm");

 		if (form.isValid()) {

 			var siteConfig = this._siteConfiguration;

-			// Omit the HostingStatus field before save since it's likely to be updated from

-			// the sites page, and we don't want to overwrite

+			// Omit the HostingStatus field from the object we send since it's likely to be updated from the

+			// sites page, and we don't want to overwrite

 			var status = siteConfig.HostingStatus;

 			delete siteConfig.HostingStatus;

 			var self = this;

-			var deferred = this._siteService.updateSiteConfiguration(siteConfig.Location, siteConfig).then(

+			var deferred = this._siteClient.updateSiteConfiguration(siteConfig.Location, siteConfig).then(

 				function(updatedSiteConfig) {

 					self.setDirty(false);

 					if (refreshUI) {

@@ -508,6 +387,7 @@
 						return updatedSiteConfig;

 					} else {

 						siteConfig.HostingStatus = status;

+						self._refreshCommands();

 						return siteConfig;

 					}

 				});

diff --git a/bundles/org.eclipse.orion.client.core/web/plugins/site/sitePlugin.html b/bundles/org.eclipse.orion.client.core/web/plugins/site/sitePlugin.html
new file mode 100644
index 0000000..fc61c00
--- /dev/null
+++ b/bundles/org.eclipse.orion.client.core/web/plugins/site/sitePlugin.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<script src='../../requirejs/require.js'></script>
+	<script>
+		/*global require*/
+		require({
+		  baseUrl: '../../',
+		  packages: [
+		    {	name: 'dojo',
+				location: 'org.dojotoolkit/dojo',
+				main: 'lib/main-browser',
+				lib: '.'
+		    },
+		    {	name: 'siteplugin',
+				location: 'plugins/site'
+		    }
+		  ],
+		  paths: {
+			  text: 'requirejs/text',
+			  i18n: 'requirejs/i18n',
+			  domReady: 'requirejs/domReady'
+		  }
+		});
+
+		require(["sitePlugin.js"]);
+	</script>
+</head>
+<body>
+</body>
+</html>
\ No newline at end of file
diff --git a/bundles/org.eclipse.orion.client.core/web/plugins/site/sitePlugin.js b/bundles/org.eclipse.orion.client.core/web/plugins/site/sitePlugin.js
new file mode 100644
index 0000000..1014729
--- /dev/null
+++ b/bundles/org.eclipse.orion.client.core/web/plugins/site/sitePlugin.js
@@ -0,0 +1,73 @@
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2012 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made 
+ * available under the terms of the Eclipse Public License v1.0 
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution 
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). 
+ * 
+ * Contributors: IBM Corporation - initial API and implementation
+ ******************************************************************************/
+/*global define document eclipse parent window*/
+define(['../../orion/plugin.js', 'siteplugin/siteServiceImpl'], function(plugin, siteImpl) {
+	function qualify(url) {
+		var a = document.createElement('a');
+		a.href = url;
+		return a.href;
+	}
+	function unqualify(url) {
+		url = qualify(url);
+		try {
+			if (window.location.host === parent.location.host && window.location.protocol === parent.location.protocol) {
+				return url.substring(parent.location.href.indexOf(parent.location.host) + parent.location.host.length);
+			}
+		} catch (e) {}
+		return url;
+	}
+	function filesAndFoldersOnService(filePrefix) {
+		return [
+			{	source: 'Location|Directory'
+			},
+			{	source: 'Location',
+				match: '^' + filePrefix
+			}];
+	}
+
+	// Tightly coupled to the fileClientPlugin
+	var siteBase = unqualify('../../site');
+	var fileBase = unqualify('../../file');
+	var workspaceBase = unqualify('../../workspace');
+	//console.log("sitePlugin siteBase:" + siteBase + ", fileBase:" + fileBase + ", workspaceBase:" + workspaceBase);
+
+	var provider = new eclipse.PluginProvider();
+
+	provider.registerServiceProvider('orion.navigate.command', null, {
+		id: 'orion.site.viewon',
+		name: 'View on Site',
+		tooltip: 'View this file or folder on a web site hosted by Orion',
+		forceSingleItem: true,
+		validationProperties: filesAndFoldersOnService(fileBase),
+		uriTemplate: '{OrionHome}/sites/view.html#,file={Location}'
+	});
+
+	provider.registerServiceProvider('orion.page.link.related', null, {
+		id: 'orion.site.viewon',
+		name: 'View on Site',
+		tooltip: 'View this file or folder on a web site hosted by Orion',
+		validationProperties: filesAndFoldersOnService(fileBase),
+		uriTemplate: '{OrionHome}/sites/view.html#,file={Location}'
+	});
+
+	var host = document.createElement('a');
+	host.href = '/';
+	provider.registerServiceProvider('orion.site',
+		new siteImpl.SiteImpl(fileBase, workspaceBase),
+		{	id: 'orion.site.default',
+			name: '' + host.hostname + ' Orion file system',
+			canSelfHost: true,
+			pattern: siteBase,
+			filePattern: fileBase
+		});
+
+	provider.connect();
+});
diff --git a/bundles/org.eclipse.orion.client.core/web/plugins/site/siteServiceImpl.js b/bundles/org.eclipse.orion.client.core/web/plugins/site/siteServiceImpl.js
new file mode 100644
index 0000000..e09ea89
--- /dev/null
+++ b/bundles/org.eclipse.orion.client.core/web/plugins/site/siteServiceImpl.js
@@ -0,0 +1,340 @@
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2012 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made 
+ * available under the terms of the Eclipse Public License v1.0 
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution 
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). 
+ * 
+ * Contributors: IBM Corporation - initial API and implementation
+ ******************************************************************************/
+/*global define document window*/
+/*jslint regexp:false*/
+define(['require', 'dojo'], function(require, dojo) {
+	function qualifyURL(url) {
+		var link = document.createElement("a");
+		link.href = url;
+		return link.href;
+	}
+	function getContext() {
+		var root = require.toUrl("._");
+		var url = qualifyURL(root);
+		return url.substring(0, url.length-2);
+	}
+	function makeHostRelative(url) {
+		if (url.indexOf(":") !== -1) {
+			return url.substring(url.indexOf(window.location.host) + window.location.host.length);
+		}
+		return url;
+	}
+	function makeURL(site, path, file) {
+		return site.HostingStatus.URL + (path[0] !== "/" ? "/" : "") + path + (file.Directory ? "/" : "");
+	}
+	function isInternalPath(path) {
+		return new RegExp("^/").test(path);
+	}
+	/**
+	 * @returns {String} A display string constructed by replacing the first segment (project id)
+	 * of internalPath with the project's Name.
+	 */
+	function getDisplayString(internalPath, projects) {
+		var displayString;
+		var segments = internalPath.split('/');
+		var firstSegment = segments[1];
+		for (var i=0; i < projects.length; i++) {
+			var project = projects[i];
+			if (project.Id === firstSegment) {
+				segments[1] = project.Name;
+				displayString = segments.join('/');
+				break;
+			}
+		}
+		return displayString;
+	}
+	function Cache(workspaceBase) {
+		this.projects = {};
+		this.getProjects = function(workspaceId) {
+			// TODO would be better to invoke the FileService here but we are inside a plugin so we can't.
+			var headers = { "Orion-Version": "1" };
+			if (!this.projects[workspaceId]) {
+				this.projects[workspaceId] = dojo.xhrGet(
+					{	url: workspaceBase,
+						headers: headers,
+						handleAs: 'json'
+					}).then(function(data) {
+						var workspaces = data.Workspaces;
+						var workspace;
+						for (var i=0; i < workspaces.length; i++) {
+							workspace = workspaces[i];
+							if (workspace.Id === workspaceId) {
+								break;
+							}
+						}
+						return dojo.xhrGet({
+							url: workspace.Location,
+							headers: headers,
+							handleAs: 'json'
+						}).then(function(workspaceData) {
+							return workspaceData.Children || [];
+						});
+					});
+			}
+			return this.projects[workspaceId];
+		};
+	}
+	function getSelfHostingMappings(basePath) {
+		// TODO: prompt for port? It is not detectable from client side if proxy is used
+		var hostPrefix = "http://localhost" + ":" + "8080" + makeHostRelative(getContext());
+		return [
+			["/", basePath + "/bundles/org.eclipse.orion.client.core/web/index.html"],
+			["/", basePath + "/bundles/org.eclipse.orion.client.core/web"],
+			["/", basePath + "/bundles/org.eclipse.orion.client.editor/web"],
+			["/org.dojotoolkit/dojo", basePath + "/bundles/org.eclipse.orion.client.core/web/dojo"],
+			["/org.dojotoolkit/dojox", basePath + "/bundles/org.eclipse.orion.client.core/web/dojox"],
+			["/file", hostPrefix + "file"],
+			["/prefs", hostPrefix + "prefs"],
+			["/workspace", hostPrefix + "workspace"],
+			["/org.dojotoolkit", hostPrefix + "org.dojotoolkit"],
+			["/users", hostPrefix + "users"],
+			["/authenticationPlugin.html", hostPrefix + "authenticationPlugin.html"],
+			["/login", hostPrefix + "login"],
+			["/loginstatic", hostPrefix + "loginstatic"],
+			["/site", hostPrefix + "site"],
+			["/", basePath + "/bundles/org.eclipse.orion.client.git/web"],
+			["/gitapi", hostPrefix + "gitapi"],
+			["/", basePath + "/bundles/org.eclipse.orion.client.users/web"],
+			["/xfer", hostPrefix + "xfer"],
+			["/filesearch", hostPrefix + "filesearch"],
+			["/index.jsp", hostPrefix + "index.jsp"],
+			["/plugins/git", hostPrefix + "plugins/git"],
+			["/plugins/user", hostPrefix + "plugins/user"],
+			["/logout", hostPrefix + "logout"],
+			["/mixloginstatic", hostPrefix + "mixloginstatic"],
+			["/mixlogin/manageopenids", hostPrefix + "mixlogin/manageopenids"],
+			["/openids", hostPrefix + "openids"],
+			["/task", hostPrefix + "task"],
+			["/help", hostPrefix + "help"]
+		].map(function(item) {
+			return {Source: item[0], Target: item[1]};
+		});
+	}
+
+	function SiteImpl(filePrefix, workspacePrefix) {
+		this.filePrefix = filePrefix;
+		this.cache = new Cache(workspacePrefix);
+	}
+	SiteImpl.prototype = {
+		getSiteConfigurations: function() {
+			//NOTE: require.toURL needs special logic here to handle "site"
+			var siteUrl = require.toUrl("site._");
+			siteUrl = siteUrl.substring(0,siteUrl.length-2);
+			return dojo.xhrGet({
+				url: siteUrl,
+				preventCache: true,
+				headers: {
+					"Orion-Version": "1"
+				},
+				handleAs: "json",
+				timeout: 15000
+			}).then(function(response) {
+				return response.SiteConfigurations;
+			});
+		},
+		loadSiteConfiguration: function(locationUrl) {
+			return dojo.xhrGet({
+				url: locationUrl,
+				headers: {
+					"Orion-Version": "1"
+				},
+				handleAs: "json",
+				timeout: 15000
+			});
+		},
+		/**
+		 * @param {String} name
+		 * @param {String} workspaceId
+		 * @param {Object} [mappings]
+		 * @param {String} [hostHint]
+		 * @param {String} [status]
+		 */
+		createSiteConfiguration: function(name, workspaceId, mappings, hostHint, hostingStatus) {
+			function hostify(name) {
+				return name.replace(/ /g, "-").replace(/[^A-Za-z0-9-_]/g, "").toLowerCase();
+			}
+			var toCreate = {
+					Name: name,
+					Workspace: workspaceId,
+					HostHint: hostify(name)
+				};
+			if (mappings) { toCreate.Mappings = mappings; }
+			if (hostHint) { toCreate.HostHint = hostHint; }
+			if (hostingStatus) { toCreate.HostingStatus = hostingStatus; }
+
+			//NOTE: require.toURL needs special logic here to handle "site"
+			var siteUrl = require.toUrl("site._");
+			siteUrl = siteUrl.substring(0,siteUrl.length-2);
+			return dojo.xhrPost({
+				url: siteUrl,
+				postData: JSON.stringify(toCreate),
+				headers: {
+					"Content-Type": "application/json; charset=utf-8",
+					"Orion-Version": "1"
+				},
+				handleAs: "json",
+				timeout: 15000
+			});
+		},
+		updateSiteConfiguration: function(locationUrl, updatedSiteConfig) {
+			return dojo.xhrPut({
+				url: locationUrl,
+				putData: JSON.stringify(updatedSiteConfig),
+				headers: {
+					"Content-Type": "application/json; charset=utf-8",
+					"Orion-Version": "1"
+				},
+				handleAs: "json",
+				timeout: 15000
+			});
+		},
+		deleteSiteConfiguration: function(locationUrl) {
+			return dojo.xhrDelete({
+				url: locationUrl,
+				headers: {
+					"Orion-Version": "1"
+				},
+				handleAs: "json",
+				timeout: 15000
+			});
+		},
+		/**
+		 * @param {String} fileLocation
+		 */
+		toInternalForm: function(fileLocation) {
+			var relFilePrefix = makeHostRelative(this.filePrefix);
+			var relLocation = makeHostRelative(fileLocation);
+			var path;
+			if (relLocation.indexOf(relFilePrefix) === 0) {
+				path = relLocation.substring(relFilePrefix.length);
+			}
+			if (path[path.length-1] === "/"){
+				path = path.substring(0, path.length - 1);
+			}
+			return path;
+		},
+		/**
+		 * @param {String} internalPath
+		 */
+		toFileLocation: function(internalPath) {
+			function _removeEmptyElements(array) {
+				return array.filter(function(s){return s !== "";});
+			}
+			var relativePath = require.toUrl(this.filePrefix + internalPath + "._");
+			relativePath = relativePath.substring(0, relativePath.length - 2);
+			var segments = internalPath.split("/");
+			if (_removeEmptyElements(segments).length === 1) {
+				relativePath += "/";
+			}
+			return makeHostRelative(qualifyURL(relativePath));
+		},
+		/** @returns {Object} */
+		getMappingObject: function(site, fileLocation, virtualPath) {
+			var internalPath = this.toInternalForm(fileLocation);
+			return this.cache.getProjects(site.Workspace).then(function(projects) {
+				var displayString = getDisplayString(internalPath, projects);
+				return {
+					Source: virtualPath,
+					Target: internalPath,
+					FriendlyPath: displayString || virtualPath,
+				};
+			});
+		},
+		getMappingProposals: function(site) {
+			var self = this;
+			return this.cache.getProjects(site.Workspace).then(function(projects) {
+				return projects.map(function(project) {
+					return {
+						Source: '/' + project.Name,
+						Target: self.toInternalForm(project.Location),
+						FriendlyPath: '/' + project.Name
+					};
+				});
+			});
+		},
+		updateMappingsDisplayStrings: function(site) {
+			return this.cache.getProjects(site.Workspace).then(function(projects) {
+				var mappings = site.Mappings;
+				for (var i = 0; i < mappings.length; i++) {
+					var mapping = mappings[i];
+					if (isInternalPath(mapping.Target)) {
+						mapping.FriendlyPath = getDisplayString(mapping.Target, projects);
+					}
+				}
+				return site;
+			});
+		},
+		parseInternalForm: function(site, displayString) {
+			if (isInternalPath(displayString)) {
+				return this.cache.getProjects(site.Workspace).then(function(projects) {
+					// Find project whose Name matches the first segment of display string
+					var segments = displayString.split('/');
+					for (var i=0; i < projects.length; i++) {
+						var project = projects[i];
+						if (segments[1] === project.Name) {
+							// Replace Name by Id to produce the internal form
+							segments[1] = project.Id;
+							return segments.join('/');
+						}
+					}
+				});
+			}
+			return null; // no internal form
+		},
+		isSelfHostingSite: function(site) {
+			function hasMapping(mappings, mapping) {
+				for (var i=0; i < mappings.length; i++) {
+					var m = mappings[i];
+					if (m.Source === mapping.Source || m.Target === mapping.Target) {
+						return true;
+					}
+				}
+				return false;
+			}
+			var self = this;
+			return this.cache.getProjects(site.Workspace).then(function(projects) {
+				// There must be a project for which all self hosting mappings can be generated using the project's Id
+				return projects.some(function(project) {
+					var internalPath = self.toInternalForm(project.Location);
+					var selfHostMappings = getSelfHostingMappings(internalPath);
+					for (var i=0; i < selfHostMappings.length; i++) {
+						if (!hasMapping(site.Mappings, selfHostMappings[i])) {
+							return false;
+						}
+					}
+					return true;
+				});
+			});
+		},
+		convertToSelfHosting: function(site, selfHostfileLocation) {
+			var internalPath = this.toInternalForm(selfHostfileLocation);
+			var mappings = getSelfHostingMappings(internalPath);
+			site.Mappings = mappings;
+			return site;
+		},
+		getURLOnSite: function(site, file) {
+			var mappings = site.Mappings, filePath = this.toInternalForm(file.Location);
+			if (!mappings) {
+				return null;
+			}
+			for (var i=0; i < mappings.length; i++) {
+				var mapping = mappings[i];
+				if (mapping.Target === filePath) {
+					return makeURL(site, mapping.Source, file);
+				}
+			}
+			return null;
+		}
+	};
+	return {
+		SiteImpl: SiteImpl
+	};
+});
\ No newline at end of file
diff --git a/bundles/org.eclipse.orion.client.core/web/settings/maker.js b/bundles/org.eclipse.orion.client.core/web/settings/maker.js
index a33ae5b..e1c6169 100644
--- a/bundles/org.eclipse.orion.client.core/web/settings/maker.js
+++ b/bundles/org.eclipse.orion.client.core/web/settings/maker.js
@@ -13,7 +13,7 @@
 /*global define dojo dijit orion window widgets localStorage*/

 /*jslint browser:true devel:true*/

 

-define(['require', 'dojo', 'orion/bootstrap', 'orion/status', 'orion/commands', 'orion/operationsClient', 'orion/fileClient', 'orion/searchClient', 'orion/dialogs', 'orion/globalCommands', 'orion/siteService', 'orion/siteUtils', 'orion/siteTree', 'orion/treetable', 'dojo/parser', 'dojo/hash', 'dojo/date/locale', 'dijit/layout/BorderContainer', 'dijit/layout/ContentPane', 'orion/widgets/maker/PluginMakerContainer', 'orion/widgets/maker/ScrollingContainerSection', 'orion/widgets/maker/PluginDescriptionSection', 'orion/widgets/maker/PluginCompletionSection', 'dijit/form/Button', 'dijit/ColorPalette'], function(require, dojo, mBootstrap, mStatus, mCommands, mOperationsClient, mFileClient, mSearchClient, mDialogs, mGlobalCommands, mSiteService, mSiteUtils, mSiteTree, mTreeTable) {

+define(['require', 'dojo', 'orion/bootstrap', 'orion/status', 'orion/commands', 'orion/operationsClient', 'orion/fileClient', 'orion/searchClient', 'orion/dialogs', 'orion/globalCommands', 'orion/sites/siteClient', 'orion/sites/siteUtils', 'orion/sites/sitesExplorer', 'orion/treetable', 'dojo/parser', 'dojo/hash', 'dojo/date/locale', 'dijit/layout/BorderContainer', 'dijit/layout/ContentPane', 'orion/widgets/maker/PluginMakerContainer', 'orion/widgets/maker/ScrollingContainerSection', 'orion/widgets/maker/PluginDescriptionSection', 'orion/widgets/maker/PluginCompletionSection', 'dijit/form/Button', 'dijit/ColorPalette'], function(require, dojo, mBootstrap, mStatus, mCommands, mOperationsClient, mFileClient, mSearchClient, mDialogs, mGlobalCommands, mSiteClient, mSiteUtils, mSiteTree, mTreeTable) {

 

 	dojo.addOnLoad(function() {

 		mBootstrap.startup().then(function(core) {

diff --git a/bundles/org.eclipse.orion.client.core/web/sites/site.css b/bundles/org.eclipse.orion.client.core/web/sites/site.css
index 4d8274b..6a86206 100644
--- a/bundles/org.eclipse.orion.client.core/web/sites/site.css
+++ b/bundles/org.eclipse.orion.client.core/web/sites/site.css
@@ -24,11 +24,6 @@
 	height: 100%;
 }
 
-h1 {
-	position: relative;
-	margin-top: 18px;
-}
-
 .statusPane {
 	font-weight: bold;
 }
\ No newline at end of file
diff --git a/bundles/org.eclipse.orion.client.core/web/sites/site.js b/bundles/org.eclipse.orion.client.core/web/sites/site.js
index 29e2681..3a84f7d 100644
--- a/bundles/org.eclipse.orion.client.core/web/sites/site.js
+++ b/bundles/org.eclipse.orion.client.core/web/sites/site.js
@@ -16,9 +16,9 @@
  * Glue code for site.html
  */
 define(['dojo', 'orion/bootstrap', 'orion/status', 'orion/progress', 'orion/commands', 
-	'orion/fileClient', 'orion/operationsClient', 'orion/searchClient', 'orion/dialogs', 'orion/globalCommands', 'orion/util', 'orion/siteService', 'orion/siteCommands', 'orion/siteTree', 'orion/treetable', 'orion/PageUtil',
+	'orion/fileClient', 'orion/operationsClient', 'orion/searchClient', 'orion/dialogs', 'orion/globalCommands', 'orion/util', 'orion/sites/siteClient', 'orion/sites/siteCommands', 'orion/PageUtil',
 	'dojo/parser', 'dojo/hash', 'dijit/layout/BorderContainer', 'dijit/layout/ContentPane', 'orion/widgets/SiteEditor'], 
-	function(dojo, mBootstrap, mStatus, mProgress, mCommands, mFileClient, mOperationsClient, mSearchClient, mDialogs, mGlobalCommands, mUtil, mSiteService, mSiteCommands, mSiteTree, mTreeTable, PageUtil) {
+	function(dojo, mBootstrap, mStatus, mProgress, mCommands, mFileClient, mOperationsClient, mSearchClient, mDialogs, mGlobalCommands, mUtil, mSiteClient, mSiteCommands, PageUtil) {
 
 	dojo.addOnLoad(function() {
 		mBootstrap.startup().then(function(core) {
@@ -27,22 +27,18 @@
 			document.body.style.visibility = "visible";
 			dojo.parser.parse();
 			
-			// Register services
 			var dialogService = new mDialogs.DialogService(serviceRegistry);
 			var operationsClient = new mOperationsClient.OperationsClient(serviceRegistry);
 			var statusService = new mStatus.StatusReportingService(serviceRegistry, operationsClient, "statusPane", "notifications", "notificationArea");
 			var progressService = new mProgress.ProgressService(serviceRegistry, operationsClient);
 			var commandService = new mCommands.CommandService({serviceRegistry: serviceRegistry});
 		
-			var fileClient = new mFileClient.FileClient(serviceRegistry, function(reference) {
-				var pattern = reference.getProperty("pattern");
-				return pattern && pattern.indexOf("/") === 0;
-			});
-			var siteService = new mSiteService.SiteService(serviceRegistry);
+			var siteLocation = PageUtil.matchResourceParameters().resource;
+			var siteClient = mSiteClient.forLocation(serviceRegistry, siteLocation);
+			var fileClient = siteClient._getFileClient();
 			var searcher = new mSearchClient.Searcher({serviceRegistry: serviceRegistry, commandService: commandService, fileService: fileClient});
-			
 			mGlobalCommands.generateBanner("banner", serviceRegistry, commandService, preferences, searcher);
-			
+
 			var updateTitle = function() {
 				var editor = dijit.byId("site-editor");
 				var site = editor && editor.getSiteConfiguration();
@@ -76,7 +72,7 @@
 				widget = new orion.widgets.SiteEditor({
 					serviceRegistry: serviceRegistry,
 					fileClient: fileClient,
-					siteService: siteService,
+					siteClient: siteClient,
 					commandService: commandService,
 					statusService: statusService,
 					progressService: progressService,
@@ -96,15 +92,8 @@
 					return "There are unsaved changes.";
 				}
 			};
-			
-			// Hook up commands stuff
-			var refresher = dojo.hitch(widget, widget._setSiteConfiguration);
-			var errorHandler = dojo.hitch(statusService, statusService.setProgressResult);
-			mSiteCommands.createSiteCommands(serviceRegistry, {
-				startCallback: refresher,
-				stopCallback: refresher,
-				errorCallback: errorHandler
-			});
+
+			mSiteCommands.createSiteCommands(serviceRegistry);
 			commandService.registerCommandContribution("pageActions", "orion.site.start", 1);
 			commandService.registerCommandContribution("pageActions", "orion.site.stop", 2);
 			commandService.registerCommandContribution("pageActions", "orion.site.convert", 3);
diff --git a/bundles/org.eclipse.orion.client.core/web/sites/sites.css b/bundles/org.eclipse.orion.client.core/web/sites/sites.css
index 6261b36..bab443b 100644
--- a/bundles/org.eclipse.orion.client.core/web/sites/sites.css
+++ b/bundles/org.eclipse.orion.client.core/web/sites/sites.css
@@ -14,19 +14,23 @@
 
 @import "../css/commands.css";
 
+@import "../css/sections.css";
+
 html,body {
 	height: 100%;
 }
 
-h1 {
-	position: relative;
-	margin-top: 18px;
-}
-
 .statusPane {
 	font-weight: bold;
 }
 
+.sites {
+	min-width: 800px;
+	max-width: 900px;
+	padding-left: 20px;
+	padding-right: 40px;
+}
+
 .domCommandToolbar {
 	padding-left: 8px;
 }
@@ -35,6 +39,15 @@
 	padding-left: 16px !important;
 }
 
+.treeTableRow {
+	vertical-align: baseline;
+}
+
+.newSiteRow > td {
+	background-color: #f4f4f4;
+	font-weight: bold;
+}
+
 .treetable th {
 	font-weight: bold;
 }
diff --git a/bundles/org.eclipse.orion.client.core/web/sites/sites.html b/bundles/org.eclipse.orion.client.core/web/sites/sites.html
index d4d64ad..0d5263f 100644
--- a/bundles/org.eclipse.orion.client.core/web/sites/sites.html
+++ b/bundles/org.eclipse.orion.client.core/web/sites/sites.html
@@ -4,8 +4,9 @@
 		<meta charset="UTF-8">
 		<title>Site Configurations</title>
 		<link rel="stylesheet" type="text/css" href="sites.css" />
-    	<script type="text/javascript" src="../requirejs/require.js"></script>
-		<script type="text/javascript">
+		<script src="../requirejs/require.js"></script>
+		<script>
+		/*global require*/
 		require({
 			  baseUrl: '..',
 			  packages: [
@@ -38,14 +39,18 @@
 		require(["sites.js"]);
 		</script>
 	</head>
-<body style="visibility:hidden" class="claro">
-	<div id="orion.sites" class="orionPage" dojoType="dijit.layout.BorderContainer" design="headline" gutters="false">
+<body style="visibility:hidden;" class="claro">
+	<div id="orion-sites" class="orionPage" dojoType="dijit.layout.BorderContainer" design="headline" gutters="false">
 		<div class="banner" id="banner" dojoType="dijit.layout.ContentPane" region="top">
 		</div>
 		<div id="centerPane" dojoType="dijit.layout.BorderContainer" gutters="false" region="center" design="headline" liveSplitters="false" splitter="false" role="main">
 			<div class="mainToolbar" id="pageToolbar" dojoType="dijit.layout.ContentPane" splitter="false" region="top">
 			</div>
-			<div class="mainpane" id="site-table" dojoType="dijit.layout.ContentPane" splitter="false"region="center">
+			<div class="mainpane" dojoType="dijit.layout.ContentPane" splitter="false"region="center">
+				<div id="mainNode" class="sites mainPadding">
+					<div id="table" class="displayTable">
+					</div>
+				</div>
 			</div>
 		</div>
 		<div class="footer" id="footer" dojoType="dijit.layout.ContentPane" region="bottom" splitter="false">
diff --git a/bundles/org.eclipse.orion.client.core/web/sites/sites.js b/bundles/org.eclipse.orion.client.core/web/sites/sites.js
index bb14958..04e90bb 100644
--- a/bundles/org.eclipse.orion.client.core/web/sites/sites.js
+++ b/bundles/org.eclipse.orion.client.core/web/sites/sites.js
@@ -1,6 +1,6 @@
 /*******************************************************************************
  * @license
- * Copyright (c) 2011 IBM Corporation and others.
+ * Copyright (c) 2011, 2012 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials are made 
  * available under the terms of the Eclipse Public License v1.0 
  * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution 
@@ -11,15 +11,12 @@
  *******************************************************************************/
 /*global define dojo dijit orion window widgets*/
 /*jslint browser:true*/
-
-/*
- * Glue code for sites.html
- */
-
 define(['require', 'dojo', 'orion/bootstrap', 'orion/status', 'orion/progress', 'orion/commands', 'orion/fileClient', 'orion/operationsClient',
-	        'orion/searchClient', 'orion/dialogs', 'orion/globalCommands', 'orion/siteService', 'orion/siteUtils', 'orion/siteCommands', 'orion/siteTree', 'orion/treetable', 
-	        'dojo/parser', 'dojo/hash', 'dojo/date/locale', 'dijit/layout/BorderContainer', 'dijit/layout/ContentPane'], 
-			function(require, dojo, mBootstrap, mStatus, mProgress, mCommands, mFileClient, mOperationsClient, mSearchClient, mDialogs, mGlobalCommands, mSiteService, mSiteUtils, mSiteCommands, mSiteTree, mTreeTable) {
+		'orion/searchClient', 'orion/dialogs', 'orion/globalCommands', 'orion/sites/siteUtils', 'orion/sites/siteCommands', 
+		'orion/sites/sitesExplorer',
+		'dojo/parser', 'dojo/hash', 'dojo/date/locale', 'dijit/layout/BorderContainer', 'dijit/layout/ContentPane'], 
+		function(require, dojo, mBootstrap, mStatus, mProgress, mCommands, mFileClient, mOperationsClient, mSearchClient, mDialogs, mGlobalCommands,
+			mSiteUtils, mSiteCommands, mSitesExplorer) {
 
 	dojo.addOnLoad(function() {
 		mBootstrap.startup().then(function(core) {
@@ -27,66 +24,36 @@
 			var preferences = core.preferences;
 			document.body.style.visibility = "visible";
 			dojo.parser.parse();
-		
+
 			// Register services
 			var dialogService = new mDialogs.DialogService(serviceRegistry);
 			var operationsClient = new mOperationsClient.OperationsClient(serviceRegistry);
 			var statusService = new mStatus.StatusReportingService(serviceRegistry, operationsClient, "statusPane", "notifications", "notificationArea");
 			var progressService = new mProgress.ProgressService(serviceRegistry, operationsClient);
 			var commandService = new mCommands.CommandService({serviceRegistry: serviceRegistry});
-	
-			var siteService = new mSiteService.SiteService(serviceRegistry);
+
 			var fileClient = new mFileClient.FileClient(serviceRegistry);
 			var searcher = new mSearchClient.Searcher({serviceRegistry: serviceRegistry, commandService: commandService, fileService: fileClient});
-			
-			mGlobalCommands.generateBanner("banner", serviceRegistry, commandService, preferences, searcher);
-			
-			// Create the visuals
-			var treeWidget;
-			(function() {
-				statusService.setMessage("Loading...");
-				var renderer = new mSiteTree.SiteRenderer(commandService);
-				dojo.connect(renderer, "rowsChanged", null, function() {
-					statusService.setMessage("");
-				});
-				treeWidget = new mTreeTable.TableTree({
-					id: "site-table-tree",
-					parent: dojo.byId("site-table"),
-					model: new mSiteTree.SiteTreeModel(siteService, "site-table-tree"),
-					showRoot: false,
-					renderer: renderer
-				});
-			}());
-			
-			(function() {
-				// Reloads the table view after doing a command
-				var refresh = function() {
-					siteService.getSiteConfigurations().then(function(siteConfigs) {
-						statusService.setMessage("");
-						treeWidget.refresh("site-table-tree", siteConfigs, true);
-					});
-				};
-				var errorHandler = dojo.hitch(statusService, statusService.setProgressResult);
+
+			function createCommands() {
+				var errorHandler = statusService.setProgressResult.bind(statusService);
 				var goToUrl = function(url) {
 					window.location = url;
 				};
-				mSiteCommands.createSiteCommands(serviceRegistry, {
+				mSiteCommands.createSiteServiceCommands(serviceRegistry, {
 					createCallback: goToUrl,
-					startCallback: refresh,
-					stopCallback: refresh,
-					deleteCallback: refresh,
-					errorCallback: errorHandler
+					errorHandler: errorHandler
 				});
-
-				// Register command contributions
-				commandService.registerCommandContribution("pageActions", "orion.site.create", 1, null, false, null, new mCommands.URLBinding("createSite", "name"));
-				commandService.registerCommandContribution("siteCommand", "orion.site.edit", 1);
-				commandService.registerCommandContribution("siteCommand", "orion.site.start", 2);
-				commandService.registerCommandContribution("siteCommand", "orion.site.stop", 3);
-				commandService.registerCommandContribution("siteCommand", "orion.site.delete", 4);
-				
-				mGlobalCommands.generateDomCommandsInBanner(commandService, {});
-			}());
+				mSiteCommands.createSiteCommands(serviceRegistry);
+			}
+			var explorer = new mSitesExplorer.SiteServicesExplorer({
+					parent: "table",
+					serviceRegistry: serviceRegistry,
+					selection: null,
+				});
+			mGlobalCommands.generateBanner("banner", serviceRegistry, commandService, preferences, searcher, explorer);
+			createCommands();
+			explorer.display();
 		});
 	});
 });
\ No newline at end of file
diff --git a/bundles/org.eclipse.orion.client.core/web/sites/view.css b/bundles/org.eclipse.orion.client.core/web/sites/view.css
new file mode 100644
index 0000000..261ad1f
--- /dev/null
+++ b/bundles/org.eclipse.orion.client.core/web/sites/view.css
@@ -0,0 +1,2 @@
+@import "sites.css";
+
diff --git a/bundles/org.eclipse.orion.client.core/web/sites/view.html b/bundles/org.eclipse.orion.client.core/web/sites/view.html
new file mode 100644
index 0000000..ed00078
--- /dev/null
+++ b/bundles/org.eclipse.orion.client.core/web/sites/view.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta charset="UTF-8">
+		<title>View on Site</title>
+		<link rel="stylesheet" type="text/css" href="sites.css" />
+		<script src="../requirejs/require.js"></script>
+		<script>
+		/*global require*/
+		require({
+			  baseUrl: '..',
+			  packages: [
+			    {
+			      name: 'dojo',
+			      location: 'org.dojotoolkit/dojo',
+			      main: 'lib/main-browser',
+			      lib: '.'
+			    },
+			    {
+			      name: 'dijit',
+			      location: 'org.dojotoolkit/dijit',
+			      main: 'lib/main',
+			      lib: '.'
+			    },
+			    {
+			      name: 'dojox',
+			      location: 'org.dojotoolkit/dojox',
+			      main: 'lib/main',
+			      lib: '.'
+			    }		    
+			  ],
+			  paths: {
+				  text: 'requirejs/text',
+				  i18n: 'requirejs/i18n',
+				  domReady: 'requirejs/domReady'	    
+			  }
+			});
+		
+		require(["view.js"]);
+		</script>
+	</head>
+<body style="visibility:hidden;" class="claro">
+	<div id="orion-sites" class="orionPage" dojoType="dijit.layout.BorderContainer" design="headline" gutters="false">
+		<div class="banner" id="banner" dojoType="dijit.layout.ContentPane" region="top">
+		</div>
+		<div id="centerPane" dojoType="dijit.layout.BorderContainer" gutters="false" region="center" design="headline" liveSplitters="false" splitter="false" role="main">
+			<div class="mainToolbar" id="pageToolbar" dojoType="dijit.layout.ContentPane" splitter="false" region="top">
+			</div>
+			<div class="mainpane" dojoType="dijit.layout.ContentPane" splitter="false"region="center">
+				<div id="mainNode" class="sites mainPadding">
+					<div id="table" class="displayTable">
+					</div>
+				</div>
+			</div>
+		</div>
+		<div class="footer" id="footer" dojoType="dijit.layout.ContentPane" region="bottom" splitter="false">
+		</div>
+	</div>
+	
+</body>
+</html>
\ No newline at end of file
diff --git a/bundles/org.eclipse.orion.client.core/web/sites/view.js b/bundles/org.eclipse.orion.client.core/web/sites/view.js
new file mode 100644
index 0000000..75a4870
--- /dev/null
+++ b/bundles/org.eclipse.orion.client.core/web/sites/view.js
@@ -0,0 +1,66 @@
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2012 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made 
+ * available under the terms of the Eclipse Public License v1.0 
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution 
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). 
+ *
+ * Contributors:
+ *     IBM Corporation - initial API and implementation
+ *******************************************************************************/
+/*global define document dojo dijit orion window widgets*/
+/*jslint */
+define(['require', 'dojo', 'orion/bootstrap', 'orion/status', 'orion/progress', 'orion/commands', 'orion/fileClient', 'orion/operationsClient',
+		'orion/searchClient', 'orion/globalCommands', 'orion/sites/siteUtils', 'orion/sites/siteCommands', 
+		'orion/sites/sitesExplorer', 'orion/PageUtil',
+		'dojo/parser', 'dojo/hash', 'dojo/date/locale', 'dijit/layout/BorderContainer', 'dijit/layout/ContentPane'], 
+		function(require, dojo, mBootstrap, mStatus, mProgress, mCommands, mFileClient, mOperationsClient, mSearchClient, mGlobalCommands,
+			mSiteUtils, mSiteCommands, mSitesExplorer, PageUtil) {
+
+	dojo.addOnLoad(function() {
+		mBootstrap.startup().then(function(core) {
+			var serviceRegistry = core.serviceRegistry;
+			var preferences = core.preferences;
+			document.body.style.visibility = 'visible';
+			dojo.parser.parse();
+
+			// Register services
+			var operationsClient = new mOperationsClient.OperationsClient(serviceRegistry);
+			var statusService = new mStatus.StatusReportingService(serviceRegistry, operationsClient, 'statusPane', 'notifications', 'notificationArea');
+			var progressService = new mProgress.ProgressService(serviceRegistry, operationsClient);
+			var commandService = new mCommands.CommandService({serviceRegistry: serviceRegistry});
+
+			var fileClient = new mFileClient.FileClient(serviceRegistry);
+			var searcher = new mSearchClient.Searcher({serviceRegistry: serviceRegistry, commandService: commandService, fileService: fileClient});
+
+			var treeWidget;
+			function createTree(file) {
+				var parentId = 'table';
+				if (treeWidget) {
+					dojo.empty(parentId);
+				}
+				treeWidget = new mSitesExplorer.ViewOnSiteTree({
+					id: 'view-on-site-table',
+					parent: parentId,
+					serviceRegistry: serviceRegistry,
+					fileLocation: file
+				});
+			}
+			function processParameters() {
+				var params = PageUtil.matchResourceParameters();
+				var file = params.file;
+				if (file) {
+					createTree(file);
+					mSiteCommands.createViewOnSiteCommands(serviceRegistry);
+				}
+			}
+			dojo.subscribe("/dojo/hashchange", null, function() {
+				processParameters();
+			});
+
+			processParameters();
+			mGlobalCommands.generateBanner('banner', serviceRegistry, commandService, preferences, searcher);
+		});
+	});
+});
\ No newline at end of file