diff --git a/chrome/content/zotero/xpcom/api.js b/chrome/content/zotero/xpcom/api.js
index c3225dc7b..2a38a569b 100644
--- a/chrome/content/zotero/xpcom/api.js
+++ b/chrome/content/zotero/xpcom/api.js
@@ -36,89 +36,109 @@ Zotero.API = {
 	
 	
 	getResultsFromParams: Zotero.Promise.coroutine(function* (params) {
-		var results;
-		switch (params.scopeObject) {
-			case 'collections':
-				if (params.scopeObjectKey) {
-					var col = yield Zotero.Collections.getByLibraryAndKeyAsync(
-						params.libraryID, params.scopeObjectKey
-					);
-				}
-				else {
-					var col = yield Zotero.Collections.getAsync(params.scopeObjectID);
-				}
-				if (!col) {
-					throw new Error('Invalid collection ID or key');
-				}
-				yield col.loadChildItems();
-				results = col.getChildItems();
-				break;
-			
-			case 'searches':
-				if (params.scopeObjectKey) {
-					var s = yield Zotero.Searches.getByLibraryAndKeyAsync(
-						params.libraryID, params.scopeObjectKey
-					);
-				}
-				else {
-					var s = yield Zotero.Searches.getAsync(params.scopeObjectID);
-				}
-				if (!s) {
-					throw new Error('Invalid search ID or key');
-				}
-				
-				// FIXME: Hack to exclude group libraries for now
-				var s2 = new Zotero.Search();
-				s2.setScope(s);
-				var groups = Zotero.Groups.getAll();
-				for each(var group in groups) {
-					yield s2.addCondition('libraryID', 'isNot', group.libraryID);
-				}
-				var ids = yield s2.search();
-				break;
-			
-			default:
-				if (params.scopeObject) {
-					throw new Error("Invalid scope object '" + params.scopeObject + "'");
-				}
-				
-				if (params.itemKey) {
-					var s = new Zotero.Search;
-					yield s.addCondition('libraryID', 'is', params.libraryID);
-					yield s.addCondition('blockStart');
-					for (let i=0; i<params.itemKey.length; i++) {
-						let itemKey = params.itemKey[i];
-						yield s.addCondition('key', 'is', itemKey);
-					}
-					yield s.addCondition('blockEnd');
-					var ids = yield s.search();
-				}
-				else {
-					// Display all items
-					var s = new Zotero.Search();
-					yield s.addCondition('libraryID', 'is', params.libraryID);
-					yield s.addCondition('noChildren', 'true');
-					var ids = yield s.search();
-				}
+		if (!params.objectType) {
+			throw new Error("objectType not specified");
 		}
 		
-		if (results) {
-			// Filter results by item key
-			if (params.itemKey) {
-				results = results.filter(function (result) {
-					return params.itemKey.indexOf(result.key) !== -1;
-				});
+		var results;
+		
+		if (params.objectType == 'item') {
+			switch (params.scopeObject) {
+				case 'collections':
+					if (params.scopeObjectKey) {
+						var col = yield Zotero.Collections.getByLibraryAndKeyAsync(
+							params.libraryID, params.scopeObjectKey
+						);
+					}
+					else {
+						var col = yield Zotero.Collections.getAsync(params.scopeObjectID);
+					}
+					if (!col) {
+						throw new Error('Invalid collection ID or key');
+					}
+					yield col.loadChildItems();
+					results = col.getChildItems();
+					break;
+				
+				case 'searches':
+					if (params.scopeObjectKey) {
+						var s = yield Zotero.Searches.getByLibraryAndKeyAsync(
+							params.libraryID, params.scopeObjectKey
+						);
+					}
+					else {
+						var s = yield Zotero.Searches.getAsync(params.scopeObjectID);
+					}
+					if (!s) {
+						throw new Error('Invalid search ID or key');
+					}
+					
+					// FIXME: Hack to exclude group libraries for now
+					var s2 = new Zotero.Search();
+					s2.setScope(s);
+					var groups = Zotero.Groups.getAll();
+					for each(var group in groups) {
+						yield s2.addCondition('libraryID', 'isNot', group.libraryID);
+					}
+					var ids = yield s2.search();
+					break;
+				
+				default:
+					if (params.scopeObject) {
+						throw new Error("Invalid scope object '" + params.scopeObject + "'");
+					}
+					
+					var s = new Zotero.Search;
+					if (params.libraryID !== undefined) {
+						yield s.addCondition('libraryID', 'is', params.libraryID);
+					}
+					
+					if (params.objectKey) {
+						yield s.addCondition('key', 'is', params.objectKey);
+					}
+					else if (params.objectID) {
+						Zotero.debug('adding ' + params.objectID);
+						yield s.addCondition('itemID', 'is', params.objectID);
+					}
+					
+					if (params.itemKey) {
+						yield s.addCondition('blockStart');
+						for (let i=0; i<params.itemKey.length; i++) {
+							let itemKey = params.itemKey[i];
+							yield s.addCondition('key', 'is', itemKey);
+						}
+						yield s.addCondition('blockEnd');
+					}
+					
+					// Display all top-level items
+					/*if (params.onlyTopLevel) {
+						yield s.addCondition('noChildren', 'true');
+					}*/
+					
+					var ids = yield s.search();
+			}
+			
+			if (results) {
+				// Filter results by item key
+				if (params.itemKey) {
+					results = results.filter(function (result) {
+						return params.itemKey.indexOf(result.key) !== -1;
+					});
+				}
+			}
+			else if (ids) {
+				// Filter results by item key
+				if (params.itemKey) {
+					ids = ids.filter(function (id) {
+						var [libraryID, key] = Zotero.Items.getLibraryAndKeyFromID(id);
+						return params.itemKey.indexOf(key) !== -1;
+					});
+				}
+				results = yield Zotero.Items.getAsync(ids);
 			}
 		}
-		else if (ids) {
-			// Filter results by item key
-			if (params.itemKey) {
-				ids = ids.filter(function (id) {
-					var [libraryID, key] = Zotero.Items.getLibraryAndKeyFromID(id);
-					return params.itemKey.indexOf(key) !== -1;
-				});
-			}
-			results = yield Zotero.Items.getAsync(ids);
+		else {
+			throw new Error("Unsupported object type '" + params.objectType + "'");
 		}
 		
 		return results;
diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js
index ceda5de17..30a2eaa11 100644
--- a/chrome/content/zotero/xpcom/collectionTreeView.js
+++ b/chrome/content/zotero/xpcom/collectionTreeView.js
@@ -36,10 +36,11 @@
  */
 Zotero.CollectionTreeView = function()
 {
+	Zotero.LibraryTreeView.apply(this);
+	
 	this.itemToSelect = null;
 	this.hideSources = [];
 	
-	this._treebox = null;
 	this._highlightedRows = {};
 	this._unregisterID = Zotero.Notifier.registerObserver(this, ['collection', 'search', 'share', 'group', 'trash', 'bucket'], 'collectionTreeView');
 	this._containerState = {};
@@ -57,6 +58,8 @@ Object.defineProperty(Zotero.CollectionTreeView.prototype, "selectedTreeRow", {
 	}
 });
 
+
+
 /*
  *  Called by the tree itself
  */
@@ -93,6 +96,9 @@ Zotero.CollectionTreeView.prototype.setTree = Zotero.Promise.coroutine(function*
 		var row = yield this.getLastViewedRow();
 		this.selection.select(row);
 		this._treebox.ensureRowIsVisible(row);
+		
+		yield this._runListeners('load');
+		this._initialized = true;
 	}
 	catch (e) {
 		Zotero.debug(e, 1);
diff --git a/chrome/content/zotero/xpcom/itemTreeView.js b/chrome/content/zotero/xpcom/itemTreeView.js
index 01a162d14..28f165be2 100644
--- a/chrome/content/zotero/xpcom/itemTreeView.js
+++ b/chrome/content/zotero/xpcom/itemTreeView.js
@@ -35,18 +35,16 @@
  *  Constructor for the ItemTreeView object
  */
 Zotero.ItemTreeView = function (collectionTreeRow, sourcesOnly) {
+	Zotero.LibraryTreeView.apply(this);
+	
 	this.wrappedJSObject = this;
 	this.rowCount = 0;
 	this.collectionTreeRow = collectionTreeRow;
 	
-	this._initialized = false;
 	this._skipKeypress = false;
 	
 	this._sourcesOnly = sourcesOnly;
 	
-	this._callbacks = [];
-	
-	this._treebox = null;
 	this._ownerDocument = null;
 	this._needsSort = false;
 	
@@ -62,17 +60,6 @@ Zotero.ItemTreeView = function (collectionTreeRow, sourcesOnly) {
 Zotero.ItemTreeView.prototype = Object.create(Zotero.LibraryTreeView.prototype);
 Zotero.ItemTreeView.prototype.type = 'item';
 
-Zotero.ItemTreeView.prototype.addCallback = function(callback) {
-	this._callbacks.push(callback);
-}
-
-
-Zotero.ItemTreeView.prototype._runCallbacks = Zotero.Promise.coroutine(function* () {
-	for each(var cb in this._callbacks) {
-		yield Zotero.Promise.resolve(cb());
-	}
-});
-
 
 /**
  * Called by the tree itself
@@ -251,12 +238,12 @@ Zotero.ItemTreeView.prototype.setTree = Zotero.Promise.coroutine(function* (tree
 		yield this.sort();
 		
 		// Only yield if there are callbacks; otherwise, we're almost done
-		if(this._callbacks.length && this._waitAfter && Date.now() > this._waitAfter) yield Zotero.Promise.resolve();
+		if (this._listeners.load.length && this._waitAfter && Date.now() > this._waitAfter) yield Zotero.Promise.resolve();
 		
 		yield this.expandMatchParents();
 		
-		//Zotero.debug('Running callbacks in itemTreeView.setTree()', 4);
-		yield this._runCallbacks();
+		yield this._runListeners('load');
+		this._initialized = true;
 		
 		if (this._ownerDocument.defaultView.ZoteroPane_Local) {
 			this._ownerDocument.defaultView.ZoteroPane_Local.clearItemsPaneMessage();
@@ -1805,8 +1792,7 @@ Zotero.ItemTreeView.prototype.setFilter = Zotero.Promise.coroutine(function* (ty
 	//this._treebox.endUpdateBatch();
 	this.selection.selectEventsSuppressed = false;
 	
-	//Zotero.debug('Running callbacks in itemTreeView.setFilter()', 4);
-	yield this._runCallbacks();
+	yield this._runListeners('load');
 });
 
 
diff --git a/chrome/content/zotero/xpcom/libraryTreeView.js b/chrome/content/zotero/xpcom/libraryTreeView.js
index 76229df00..91f608e88 100644
--- a/chrome/content/zotero/xpcom/libraryTreeView.js
+++ b/chrome/content/zotero/xpcom/libraryTreeView.js
@@ -23,8 +23,35 @@
     ***** END LICENSE BLOCK *****
 */
 
-Zotero.LibraryTreeView = function () {};
+Zotero.LibraryTreeView = function () {
+	this._initialized = false;
+	this._listeners = {
+		load: []
+	};
+};
+
 Zotero.LibraryTreeView.prototype = {
+	addEventListener: function(event, listener) {
+		if (event == 'load') {
+			// If already initialized run now
+			if (this._initialized) {
+				listener();
+			}
+			else {
+				this._listeners[event].push(listener);
+			}
+		}
+	},
+	
+	
+	_runListeners: Zotero.Promise.coroutine(function* (event) {
+		var listener;
+		while (listener = this._listeners[event].shift()) {
+			yield Zotero.Promise.resolve(listener());
+		}
+	}),
+	
+	
 	/**
 	 *  Called while a drag is over the tree
 	 */
diff --git a/chrome/content/zotero/xpcom/search.js b/chrome/content/zotero/xpcom/search.js
index fe0674a9a..ebeae812c 100644
--- a/chrome/content/zotero/xpcom/search.js
+++ b/chrome/content/zotero/xpcom/search.js
@@ -2189,6 +2189,18 @@ Zotero.SearchConditions = new function(){
 				noLoad: true
 			},
 			
+			{
+				name: 'itemID',
+				operators: {
+					is: true,
+					isNot: true
+				},
+				table: 'items',
+				field: 'itemID',
+				special: true,
+				noLoad: true
+			},
+			
 			{
 				name: 'annotation',
 				operators: {
diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js
index f75d11f59..c2789be76 100644
--- a/chrome/content/zotero/zoteroPane.js
+++ b/chrome/content/zotero/zoteroPane.js
@@ -32,6 +32,7 @@ var ZoteroPane = new function()
 	var _unserialized = false;
 	this.collectionsView = false;
 	this.itemsView = false;
+	this._listeners = {};
 	this.__defineGetter__('loaded', function () _loaded);
 	
 	//Privileged methods
@@ -1099,7 +1100,7 @@ var ZoteroPane = new function()
 	this.onCollectionSelected = Zotero.Promise.coroutine(function* () {
 		var collectionTreeRow = this.getCollectionTreeRow();
 		
-		if (this.itemsView.collectionTreeRow == collectionTreeRow) {
+		if (this.itemsView && this.itemsView.collectionTreeRow == collectionTreeRow) {
 			Zotero.debug("Collection selection hasn't changed");
 			return;
 		}
@@ -1167,7 +1168,14 @@ var ZoteroPane = new function()
 		this.itemsView.onError = function () {
 			ZoteroPane_Local.displayErrorMessage();
 		};
-		this.itemsView.addCallback(this.setTagScope);
+		// If any queued load listeners, set them to run when the tree is ready
+		if (this._listeners.itemsLoaded) {
+			let listener;
+			while (listener = this._listeners.itemsLoaded.shift()) {
+				this.itemsView.addEventListener('load', listener);
+			}
+		}
+		this.itemsView.addEventListener('load', this.setTagScope);
 		document.getElementById('zotero-items-tree').view = this.itemsView;
 		
 		// Add events to treecolpicker to update menu before showing/hiding
@@ -1953,34 +1961,77 @@ var ZoteroPane = new function()
 			return false;
 		}
 		
-		if (!this.itemsView) {
-			Components.utils.reportError("Items view not set in ZoteroPane_Local.selectItem()");
-			return false;
+		// Restore window if it's in the dock
+		if (window.windowState == Components.interfaces.nsIDOMChromeWindow.STATE_MINIMIZED) {
+			window.restore();
 		}
 		
-		var currentLibraryID = this.getSelectedLibraryID();
-		// If in a different library
-		if (item.libraryID != currentLibraryID) {
-			Zotero.debug("Library ID differs; switching library");
-			yield this.collectionsView.selectLibrary(item.libraryID);
-		}
-		// Force switch to library view
-		else if (!this.collectionsView.selectedTreeRow.isLibrary() && inLibrary) {
-			Zotero.debug("Told to select in library; switching to library");
-			yield this.collectionsView.selectLibrary(item.libraryID);
+		if (!this.collectionsView) {
+			throw new Error("Collections view not loaded");
 		}
 		
-		var selected = yield this.itemsView.selectItem(itemID, expand);
-		if (!selected) {
-			Zotero.debug("Item was not selected; switching to library");
-			yield this.collectionsView.selectLibrary(item.libraryID);
-			yield this.itemsView.selectItem(itemID, expand);
-		}
+		var self = this;
+		this.collectionsView.addEventListener('load', function () {
+			Zotero.spawn(function* () {
+				var currentLibraryID = self.getSelectedLibraryID();
+				// If in a different library
+				if (item.libraryID != currentLibraryID) {
+					Zotero.debug("Library ID differs; switching library");
+					yield self.collectionsView.selectLibrary(item.libraryID);
+				}
+				// Force switch to library view
+				else if (!self.collectionsView.selectedTreeRow.isLibrary() && inLibrary) {
+					Zotero.debug("Told to select in library; switching to library");
+					yield self.collectionsView.selectLibrary(item.libraryID);
+				}
+				
+				self.addEventListener('itemsLoaded', function () {
+					Zotero.spawn(function* () {
+						var selected = yield self.itemsView.selectItem(itemID, expand);
+						if (!selected) {
+							Zotero.debug("Item was not selected; switching to library");
+							yield self.collectionsView.selectLibrary(item.libraryID);
+							yield self.itemsView.selectItem(itemID, expand);
+						}
+					});
+				});
+			});
+		});
+		
+		// open Zotero pane
+		this.show();
 		
 		return true;
 	});
 	
 	
+	this.addEventListener = function (event, listener) {
+		if (event == 'itemsLoaded') {
+			if (this.itemsView) {
+				this.itemsView.addEventListener('load', listener);
+			}
+			else {
+				if (!this._listeners.itemsLoaded) {
+					this._listeners.itemsLoaded = [];
+				}
+				this._listeners.itemsLoaded.push(listener);
+			}
+		}
+	};
+	
+	
+	this._runListeners = Zotero.Promise.coroutine(function* (event) {
+		if (!this._listeners[event]) {
+			return;
+		}
+		
+		var listener;
+		while (listener = this._listeners[event].shift()) {
+			yield Zotero.Promise.resolve(listener());
+		}
+	});
+	
+	
 	this.getSelectedLibraryID = function () {
 		return this.collectionsView.getSelectedLibraryID();
 	}
diff --git a/components/zotero-protocol-handler.js b/components/zotero-protocol-handler.js
index 319fdadb4..9a78202cd 100644
--- a/components/zotero-protocol-handler.js
+++ b/components/zotero-protocol-handler.js
@@ -110,6 +110,7 @@ function ZoteroProtocolHandler() {
 				}
 				
 				var params = {
+					objectType: 'item',
 					format: 'html',
 					sort: 'title'
 				};
@@ -793,47 +794,68 @@ function ZoteroProtocolHandler() {
 	var SelectExtension = {
 		newChannel: function (uri) {
 			return new AsyncChannel(uri, function* () {
-				generateContent:try {
-					var mimeType, content = '';
-					
-					var [path, queryString] = uri.path.substr(1).split('?');
-					var [type, id] = path.split('/');
-					
-					// currently only able to select one item
-					var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
-						.getService(Components.interfaces.nsIWindowMediator);
-					var win = wm.getMostRecentWindow("navigator:browser");
-					
-					// restore window if it's in the dock
-					if(win.windowState == Components.interfaces.nsIDOMChromeWindow.STATE_MINIMIZED) {
-						win.restore();
-					}
-					
-					// open Zotero pane
-					win.ZoteroPane.show();
-					
-					if(!id) return;
-					
-					var lkh = Zotero.Items.parseLibraryKeyHash(id);
+				var path = uri.path;
+				if (!path) {
+					return 'Invalid URL';
+				}
+				// Strip leading '/'
+				path = path.substr(1);
+				var mimeType, content = '';
+				
+				var params = {
+					objectType: 'item'
+				};
+				var router = new Zotero.Router(params);
+				
+				// Item within a collection or search
+				router.add('library/:scopeObject/:scopeObjectKey/items/:objectKey', function () {
+					params.libraryID = 0;
+				});
+				router.add('groups/:groupID/:scopeObject/:scopeObjectKey/items/:objectKey');
+				
+				// All items
+				router.add('library/items/:objectKey', function () {
+					params.libraryID = 0;
+				});
+				router.add('groups/:groupID/items/:objectKey');
+				
+				// Old-style URLs
+				router.add('item/:id', function () {
+					var lkh = Zotero.Items.parseLibraryKeyHash(params.id);
 					if (lkh) {
-						var item = Zotero.Items.getByLibraryAndKey(lkh.libraryID, lkh.key);
+						params.libraryID = lkh.libraryID;
+						params.objectKey = lkh.key;
 					}
 					else {
-						var item = Zotero.Items.get(id);
+						params.objectID = params.id;
 					}
-					if (!item) {
-						var msg = "Item " + id + " not found in zotero://select";
-						Zotero.debug(msg, 2);
-						Components.utils.reportError(msg);
-						return;
-					}
-					
-					win.ZoteroPane.selectItem(item.id);
+					delete params.id;
+				});
+				router.run(path);
+				
+				try {
+					Zotero.API.parseParams(params);
+					var results = yield Zotero.API.getResultsFromParams(params);
 				}
-				catch (e){
-					Zotero.debug(e);
-					throw (e);
+				catch (e) {
+					Zotero.debug(e, 1);
+					return e.toString();
 				}
+				
+				
+				if (!results.length) {
+					var msg = "Selected items not found";
+					Zotero.debug(msg, 2);
+					Components.utils.reportError(msg);
+					return;
+				}
+				
+				var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+					.getService(Components.interfaces.nsIWindowMediator);
+				var win = wm.getMostRecentWindow("navigator:browser");
+				
+				// TODO: Currently only able to select one item
+				yield win.ZoteroPane.selectItem(results[0].id);
 			});
 		}
 	};
@@ -1226,13 +1248,18 @@ AsyncChannel.prototype = {
 				});
 				return promise;
 			}
+			else if (data === undefined) {
+				this.cancel(0x804b0002); // BINDING_ABORTED
+			}
 			else {
 				throw new Error("Invalid return type (" + typeof data + ") from generator passed to AsyncChannel");
 			}
 		}.bind(this))
 		.then(function () {
-			Zotero.debug("AsyncChannel request succeeded in " + (new Date - t) + " ms");
-			channel._isPending = false;
+			if (this._isPending) {
+				Zotero.debug("AsyncChannel request succeeded in " + (new Date - t) + " ms");
+				channel._isPending = false;
+			}
 		})
 		.catch(function (e) {
 			Zotero.debug(e, 1);