diff --git a/chrome/content/zotero-platform/mac/overlay.css b/chrome/content/zotero-platform/mac/overlay.css
index 6ed923252..a3fa35a42 100644
--- a/chrome/content/zotero-platform/mac/overlay.css
+++ b/chrome/content/zotero-platform/mac/overlay.css
@@ -111,7 +111,7 @@
 	background-color: #ffffff;
 }
 
-#zotero-view-selected-label {
+#zotero-item-pane-message {
 	color: #7f7f7f;
 }
 
diff --git a/chrome/content/zotero/bindings/itembox.xml b/chrome/content/zotero/bindings/itembox.xml
index 927d76448..cdf4c8060 100644
--- a/chrome/content/zotero/bindings/itembox.xml
+++ b/chrome/content/zotero/bindings/itembox.xml
@@ -79,7 +79,6 @@
 							break;
 						
 						case 'merge':
-							//this.hideEmptyFields = true;
 							this.clickByItem = true;
 							break;
 						
@@ -92,6 +91,11 @@
 							this.blurHandler = this.hideEditor;
 							break;
 						
+						case 'fieldmerge':
+							this.hideEmptyFields = true;
+							this._fieldAlternatives = {};
+							break;
+						
 						default:
 							throw ("Invalid mode '" + val + "' in itembox.xml");
 					}
@@ -103,15 +107,22 @@
 			</property>
 			
 			<field name="_item"/>
-			<property name="item"
-				onget="return this._item;"
-				onset="this._item = val; this.refresh();">
+			<property name="item" onget="return this._item;">
+				<setter>
+				<![CDATA[
+					if (!(val instanceof Zotero.Item)) {
+						throw ("<zoteroitembox>.item must be a Zotero.Item");
+					}
+					this._item = val;
+					this.refresh();
+				]]>
+				</setter>
 			</property>
 			
 			<!-- .ref is an alias for .item -->
 			<property name="ref"
 				onget="return this._item;"
-				onset="this._item = val; this.refresh();">
+				onset="this.item = val; this.refresh();">
 			</property>
 			
 			
@@ -132,6 +143,22 @@
 				</setter>
 			</property>
 			
+			<!--
+				 An array of field names that should be hidden
+			-->
+			<field name="_hiddenFields">[]</field>
+			<property name="hiddenFields">
+				<setter>
+				<![CDATA[
+					if (val.constructor.name != 'Array') {
+						throw ('hiddenFields must be an array in <itembox>.visibleFields');
+					}
+					
+					this._hiddenFields = val;
+				]]>
+				</setter>
+			</property>
+			
 			<!--
 				 An array of field names that should be clickable
 				 even if this.clickable is false
@@ -166,6 +193,26 @@
 				</setter>
 			</property>
 			
+			<!--
+				 An object of alternative values for keyed fields
+				 
+			-->
+			<field name="_fieldAlternatives">{}</field>
+			<property name="fieldAlternatives">
+				<setter>
+				<![CDATA[
+					if (val.constructor.name != 'Object') {
+						throw ('fieldAlternatives must be an Object in <itembox>.fieldAlternatives');
+					}
+					
+					if (this.mode != 'fieldmerge') {
+						throw ('fieldAlternatives is valid only in fieldmerge mode in <itembox>.fieldAlternatives');
+					}
+					
+					this._fieldAlternatives = val;
+				]]>
+				</setter>
+			</property>
 			
 			<!--
 				 An array of field names in the order they should appear
@@ -209,7 +256,6 @@
 				onget="return '(' + Zotero.getString('pane.item.defaultLastName') + ')'"/>
 			<property name="_defaultFullName"
 				onget="return '(' + Zotero.getString('pane.item.defaultFullName') + ')'"/>
-			
 			<method name="refresh">
 				<body>
 				<![CDATA[
@@ -285,6 +331,10 @@
 						}
 						
 						if (fieldName) {
+							if (this._hiddenFields.indexOf(fieldName) != -1) {
+								continue;
+							}
+							
 							// createValueElement() adds the itemTypeID as an attribute
 							// and converts it to a localized string for display
 							if (fieldName == 'itemType') {
@@ -294,13 +344,14 @@
 								val = this.item.getField(fieldName);
 							}
 							
-							var fieldIsClickable = this._fieldIsClickable(fieldName);
-							
 							if (!val && this.hideEmptyFields
-									&& this._visibleFields.indexOf(fieldName) == -1) {
+									&& this._visibleFields.indexOf(fieldName) == -1
+									&& (this.mode != 'fieldmerge' || typeof this._fieldAlternatives[fieldName] == 'undefined')) {
 								continue;
 							}
 							
+							var fieldIsClickable = this._fieldIsClickable(fieldName);
+							
 							// Start tabindex at 1001 after creators
 							var tabindex = fieldIsClickable
 								? (i>0 ? this._tabIndexMinFields + i : 1) : 0;
@@ -365,11 +416,39 @@
 								"if (this.nextSibling.inputField) { this.nextSibling.inputField.blur(); }");
 						}
 						
-						this.addDynamicRow(label, valueElement);
+						var row = this.addDynamicRow(label, valueElement);
 						
 						if (fieldName && this._selectField == fieldName) {
 							this.showEditor(valueElement);
 						}
+						
+						// In field merge mode, add a button to switch field versions
+						else if (this.mode == 'fieldmerge' && typeof this._fieldAlternatives[fieldName] != 'undefined') {
+							var button = document.createElement("toolbarbutton");
+							button.className = 'zotero-field-version-button';
+							button.setAttribute('image', 'chrome://zotero/skin/treesource-duplicates.png');
+							button.setAttribute('type', 'menu');
+							
+							var popup = button.appendChild(document.createElement("menupopup"));
+							
+							for each(var v in this._fieldAlternatives[fieldName]) {
+								var menuitem = document.createElement("menuitem");
+								menuitem.setAttribute('label', Zotero.Utilities.ellipsize(v, 40));
+								menuitem.setAttribute('fieldName', fieldName);
+								menuitem.setAttribute('originalValue', v);
+								menuitem.setAttribute(
+									'oncommand',
+									"var binding = document.getBindingParent(this); "
+									+ "var item = binding.item; "
+									+ "item.setField(this.getAttribute('fieldName'), this.getAttribute('originalValue')); "
+									+ "var row = Zotero.getAncestorByTagName(this, 'row'); "
+									+ "binding.refresh();"
+								);
+								popup.appendChild(menuitem);
+							}
+							
+							row.appendChild(button);
+						}
 					}
 					this._selectField = false;
 					
@@ -446,13 +525,11 @@
 						this.addCreatorRow(false, false, true, true);
 					}
 					
-					
 					// Move to next or previous field if (shift-)tab was pressed
 					if (this._lastTabIndex && this._tabDirection)
 					{
 						this._focusNextField('info', this._dynamicFields, this._lastTabIndex, this._tabDirection == -1);
 					}
-
 				]]>
 				</body>
 			</method>
@@ -534,6 +611,8 @@
 					else {
 						this._dynamicFields.appendChild(row);
 					}
+					
+					return row;
 				]]>
 				</body>
 			</method>
@@ -1140,10 +1219,10 @@
 						}
 						
 						// Display a context menu for certain fields
-						if (fieldName == 'seriesTitle' || fieldName == 'shortTitle' ||
+						if (this.editable && (fieldName == 'seriesTitle' || fieldName == 'shortTitle' ||
 								Zotero.ItemFields.isFieldOfBase(fieldID, 'title') ||
-								Zotero.ItemFields.isFieldOfBase(fieldID, 'publicationTitle')) {
-							valueElement.setAttribute('contextmenu', 'field-menu');
+								Zotero.ItemFields.isFieldOfBase(fieldID, 'publicationTitle'))) {
+							valueElement.setAttribute('contextmenu', 'zotero-field-transform-menu');
 						}
 					}
 					
@@ -2261,7 +2340,7 @@
 							);
 							typeBox.setAttribute('typeid', typeID);
 							document.getBindingParent(this).modifyCreator(index, fields);"/>
-					<menupopup id="field-menu">
+					<menupopup id="zotero-field-transform-menu">
 						<menu label="&zotero.item.textTransform;">
 							<menupopup>
 								<menuitem label="&zotero.item.textTransform.titlecase;" class="menuitem-non-iconic"
diff --git a/chrome/content/zotero/bindings/tagselector.xml b/chrome/content/zotero/bindings/tagselector.xml
index ef251c871..654603c16 100644
--- a/chrome/content/zotero/bindings/tagselector.xml
+++ b/chrome/content/zotero/bindings/tagselector.xml
@@ -467,6 +467,20 @@
 				<parameter name="ids"/>
 				<body>
 				<![CDATA[
+					var itemGroup = ZoteroPane_Local.getItemGroup();
+					
+					// Ignore anything other than deletes in duplicates view
+					if (itemGroup.isDuplicates()) {
+						switch (event) {
+							case 'delete':
+							case 'trash':
+								break;
+							
+							default:
+								return;
+						}
+					}
+					
 					// If a selected tag no longer exists, deselect it
 					if (event == 'delete') {
 						this._tags = Zotero.Tags.getAll(this._types, this.libraryID);
diff --git a/chrome/content/zotero/duplicatesMerge.js b/chrome/content/zotero/duplicatesMerge.js
new file mode 100644
index 000000000..8dc7868f5
--- /dev/null
+++ b/chrome/content/zotero/duplicatesMerge.js
@@ -0,0 +1,156 @@
+/*
+    ***** BEGIN LICENSE BLOCK *****
+    
+    Copyright © 2009 Center for History and New Media
+                     George Mason University, Fairfax, Virginia, USA
+                     http://zotero.org
+    
+    This file is part of Zotero.
+    
+    Zotero is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+    
+    Zotero is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+    
+    You should have received a copy of the GNU Affero General Public License
+    along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
+    
+    ***** END LICENSE BLOCK *****
+*/
+
+var Zotero_Duplicates_Pane = new function () {
+	_items = [];
+	_otherItems = [];
+	_ignoreFields = ['dateAdded', 'dateModified', 'accessDate'];
+	
+	this.setItems = function (items, displayNumItemsOnTypeError) {
+		var itemTypeID, oldestItem, otherItems = [];
+		for each(var item in items) {
+			// Find the oldest item
+			if (!oldestItem) {
+				oldestItem = item;
+			}
+			else if (item.dateAdded < oldestItem.dateAdded) {
+				otherItems.push(oldestItem);
+				oldestItem = item;
+			}
+			else {
+				otherItems.push(item);
+			}
+			
+			if (!item.isRegularItem() || [1,14].indexOf(item.itemTypeID) != -1) {
+				// TODO: localize
+				var msg = "Only top-level full items can be merged.";
+				ZoteroPane_Local.setItemPaneMessage(msg);
+				return false;
+			}
+			
+			// Make sure all items are of the same type
+			if (itemTypeID) {
+				if (itemTypeID != item.itemTypeID) {
+					if (displayNumItemsOnTypeError) {
+						var msg = Zotero.getString('pane.item.selected.multiple', items.length);
+					}
+					else {
+						// TODO: localize
+						var msg = "Merged items must all be of the same item type.";
+					}
+					ZoteroPane_Local.setItemPaneMessage(msg);
+					return false;
+				}
+			}
+			else {
+				itemTypeID = item.itemTypeID;
+			}
+		}
+		
+		_items = items;
+		
+		_items.sort(function (a, b) {
+			return a.dateAdded > b.dateAdded ? 1 : a.dateAdded == b.dateAdded ? 0 : -1;
+		});
+		
+		//
+		// Update the UI
+		//
+		
+		var diff = oldestItem.multiDiff(otherItems, _ignoreFields);
+		
+		var button = document.getElementById('zotero-duplicates-merge-button');
+		var versionSelect = document.getElementById('zotero-duplicates-merge-version-select');
+		var itembox = document.getElementById('zotero-duplicates-merge-item-box');
+		var fieldSelect = document.getElementById('zotero-duplicates-merge-field-select');
+		
+		versionSelect.hidden = !diff;
+		if (diff) {
+			// Populate menulist with Date Added values from all items
+			var dateList = document.getElementById('zotero-duplicates-merge-original-date');
+			
+			while (dateList.itemCount) {
+				dateList.removeItemAt(0);
+			}
+			
+			var numRows = 0;
+			for each(var item in _items) {
+				var date = Zotero.Date.sqlToDate(item.dateAdded, true);
+				dateList.appendItem(date.toLocaleString());
+				numRows++;
+			}
+			
+			dateList.setAttribute('rows', numRows);
+			
+			// If we set this inline, the selection doesn't take on the first
+			// selection after unhiding versionSelect (when clicking
+			// from a set with no differences) -- tested in Fx5.0.1
+			setTimeout(function () {
+				dateList.selectedIndex = 0;
+			}, 0);
+		}
+		
+		button.label = "Merge " + (otherItems.length + 1) + " items";
+		itembox.hiddenFields = diff ? [] : ['dateAdded', 'dateModified'];
+		fieldSelect.hidden = !diff;
+		
+		this.setMaster(0);
+		
+		return true;
+	}
+	
+	
+	this.setMaster = function (pos) {
+		var itembox = document.getElementById('zotero-duplicates-merge-item-box');
+		itembox.mode = 'fieldmerge';
+		
+		_otherItems = _items.concat();
+		var item = _otherItems.splice(pos, 1)[0];
+		
+		// Add master item's values to the beginning of each set of
+		// alternative values so that they're still available if the item box
+		// modifies the item
+		var diff = item.multiDiff(_otherItems, _ignoreFields);
+		if (diff) {
+			var itemValues = item.serialize()
+			for (var i in diff) {
+				diff[i].unshift(itemValues.fields[i]);
+			}
+			itembox.fieldAlternatives = diff;
+		}
+		
+		itembox.item = item.clone(true);
+	}
+	
+	
+	this.merge = function () {
+		var itembox = document.getElementById('zotero-duplicates-merge-item-box');
+		// Work around item.clone() weirdness -- the cloned item can't safely be
+		// used after it's saved, because it's not the version in memory and
+		// doesn't get reloaded properly in item.save()
+		var item = Zotero.Items.get(itembox.item.id);
+		Zotero.Items.merge(item, _otherItems);
+	}
+}
diff --git a/chrome/content/zotero/itemPane.xul b/chrome/content/zotero/itemPane.xul
index a22919af7..bec6deca7 100644
--- a/chrome/content/zotero/itemPane.xul
+++ b/chrome/content/zotero/itemPane.xul
@@ -28,39 +28,96 @@
 
 <!DOCTYPE window SYSTEM "chrome://zotero/locale/zotero.dtd">
 
-<overlay 
-	xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
-	
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 	<script src="include.js"/>
 	<script src="itemPane.js"/>
 	
-	<tabpanels id="zotero-view-item" flex="1">
-		<tabpanel>
-			<zoteroitembox id="zotero-editpane-item-box" flex="1"/>
-		</tabpanel>
+	<vbox id="zotero-item-pane" zotero-persist="width">
+		<!-- Trash -->
+		<!-- TODO: localize -->
+		<!-- TODO: Make look less awful -->
+		<button id="zotero-item-restore-button" label="Restore to Library"
+			oncommand="ZoteroPane_Local.restoreSelectedItems()" hidden="true"/>
 		
-		<tabpanel flex="1" orient="vertical">
-			<vbox flex="1">
-				<hbox align="center">
-					<label id="zotero-editpane-notes-label"/>
-					<button id="zotero-editpane-notes-add" label="&zotero.item.add;" oncommand="ZoteroItemPane.addNote(event.shiftKey);"/>
-				</hbox>
-				<grid flex="1">
-					<columns>
-						<column flex="1"/>
-						<column/>
-					</columns>
-					<rows id="zotero-editpane-dynamic-notes" flex="1"/>
-				</grid>
+		<!-- Commons -->
+		<button id="zotero-item-show-original" label="Show Original"
+			oncommand="ZoteroPane_Local.showOriginalItem()" hidden="true"/>
+		
+		<deck id="zotero-item-pane-content" selectedIndex="0" flex="1">
+			<!-- Center label (for zero or multiple item selection) -->
+			<vbox pack="center" align="center">
+				<label id="zotero-item-pane-message"/>
 			</vbox>
-		</tabpanel>
-		
-		<tabpanel>
-			<tagsbox id="zotero-editpane-tags" flex="1"/>
-		</tabpanel>
-		
-		<tabpanel>
-			<seealsobox id="zotero-editpane-related" flex="1"/>
-		</tabpanel>
-	</tabpanels>
+			
+			<!-- Regular item -->
+			<tabbox id="zotero-view-tabbox" flex="1" onselect="if (!ZoteroPane_Local.collectionsView.selection || event.originalTarget.localName != 'tabpanels') { return; }; ZoteroItemPane.viewItem(ZoteroPane_Local.getSelectedItems()[0], ZoteroPane_Local.collectionsView.editable ? 'edit' : 'view', this.selectedIndex)">
+				<tabs>
+					<tab label="&zotero.tabs.info.label;"/>
+					<tab label="&zotero.tabs.notes.label;"/>
+					<tab label="&zotero.tabs.tags.label;"/>
+					<tab label="&zotero.tabs.related.label;"/>
+				</tabs>
+					<tabpanels id="zotero-view-item" flex="1">
+						<tabpanel>
+							<zoteroitembox id="zotero-editpane-item-box" flex="1"/>
+						</tabpanel>
+						
+						<tabpanel flex="1" orient="vertical">
+							<vbox flex="1">
+								<hbox align="center">
+									<label id="zotero-editpane-notes-label"/>
+									<button id="zotero-editpane-notes-add" label="&zotero.item.add;" oncommand="ZoteroItemPane.addNote(event.shiftKey);"/>
+								</hbox>
+								<grid flex="1">
+									<columns>
+										<column flex="1"/>
+										<column/>
+									</columns>
+									<rows id="zotero-editpane-dynamic-notes" flex="1"/>
+								</grid>
+							</vbox>
+						</tabpanel>
+						
+						<tabpanel>
+							<tagsbox id="zotero-editpane-tags" flex="1"/>
+						</tabpanel>
+						
+						<tabpanel>
+							<seealsobox id="zotero-editpane-related" flex="1"/>
+						</tabpanel>
+					</tabpanels>
+			</tabbox>
+			
+			<!-- Note item -->
+			<groupbox id="zotero-view-note" flex="1">
+				<zoteronoteeditor id="zotero-note-editor" flex="1" notitle="1"/>
+				<button id="zotero-view-note-button" label="&zotero.notes.separate;" oncommand="ZoteroPane_Local.openNoteWindow(this.getAttribute('noteID')); if(this.hasAttribute('sourceID')) ZoteroPane_Local.selectItem(this.getAttribute('sourceID'));"/>
+			</groupbox>
+			
+			<!-- Attachment item -->
+			<zoteroattachmentbox id="zotero-attachment-box" flex="1"/>
+			
+			<!-- Duplicate merging -->
+			<!-- TODO: localize -->
+			<vbox id="zotero-duplicates-merge-pane" flex="1">
+				<groupbox>
+					<button id="zotero-duplicates-merge-button" oncommand="Zotero_Duplicates_Pane.merge()"/>
+				</groupbox>
+				
+				<groupbox id="zotero-duplicates-merge-version-select">
+					<description>Choose the version of the item to use as the master item:</description>
+					<hbox>
+						<listbox id="zotero-duplicates-merge-original-date" onselect="Zotero_Duplicates_Pane.setMaster(this.selectedIndex)"/>
+					</hbox>
+				</groupbox>
+				
+				<groupbox flex="1">
+					<description id="zotero-duplicates-merge-field-select">
+						Select fields to keep from other versions of the item:
+					</description>
+					<zoteroitembox id="zotero-duplicates-merge-item-box" flex="1"/>
+				</groupbox>
+			</vbox>
+		</deck>
+	</vbox>
 </overlay>
\ No newline at end of file
diff --git a/chrome/content/zotero/selectItemsDialog.js b/chrome/content/zotero/selectItemsDialog.js
index ec9750bb4..02ba2370c 100644
--- a/chrome/content/zotero/selectItemsDialog.js
+++ b/chrome/content/zotero/selectItemsDialog.js
@@ -45,7 +45,7 @@ function doLoad()
 	
 	collectionsView = new Zotero.CollectionTreeView();
 	// Don't show Commons when citing
-	collectionsView.showCommons = false;
+	collectionsView.hideSources = ['duplicates', 'trash', 'commons'];
 	document.getElementById('zotero-collections-tree').view = collectionsView;
 	if(io.select) itemsView.selectItem(io.select);
 	
diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js
index b340d857d..690d85847 100644
--- a/chrome/content/zotero/xpcom/collectionTreeView.js
+++ b/chrome/content/zotero/xpcom/collectionTreeView.js
@@ -40,8 +40,7 @@ Zotero.CollectionTreeView = function()
 	this.itemToSelect = null;
 	this._highlightedRows = {};
 	this._unregisterID = Zotero.Notifier.registerObserver(this, ['collection', 'search', 'share', 'group', 'bucket']);
-	this.showDuplicates = false;
-	this.showCommons = true;
+	this.hideSources = [];
 }
 
 /*
@@ -78,6 +77,12 @@ Zotero.CollectionTreeView.prototype.setTree = function(treebox)
 	
 	var row = this.getLastViewedRow();
 	this.selection.select(row);
+	
+	// TODO: make better
+	var tb = this._treebox;
+	setTimeout(function () {
+		tb.ensureRowIsVisible(row);
+	}, 1);
 }
 
 
@@ -102,6 +107,17 @@ Zotero.CollectionTreeView.prototype.refresh = function()
 	this._dataItems = [];
 	this.rowCount = 0;
 	
+	if (this.hideSources.indexOf('duplicates') == -1) {
+		try {
+			var duplicateLibraries = Zotero.Prefs.get('duplicateLibraries').split(',');
+		}
+		catch (e) {
+			// Add to personal library by default
+			Zotero.Prefs.set('duplicateLibraries', '0');
+			duplicateLibraries = ['0'];
+		}
+	}
+	
 	try {
 		var unfiledLibraries = Zotero.Prefs.get('unfiledLibraries').split(',');
 	}
@@ -136,24 +152,31 @@ Zotero.CollectionTreeView.prototype.refresh = function()
 				}
 			}
 			
-			// Unfiled items
-			if (unfiledLibraries.indexOf('0') != -1) {
-				var s = new Zotero.Search;
-				// Give virtual search an id so it can be reselected automatically
-				s.id = 86345330000; // 'UNFILED' + '000' + libraryID
-				s.name = Zotero.getString('pane.collections.unfiled');
-				s.addCondition('libraryID', 'is', null);
-				s.addCondition('unfiled', 'true');
-				self._showItem(new Zotero.ItemGroup('search', s), 1, newRows+1);
+			// Duplicate items
+			if (self.hideSources.indexOf('duplicates') == -1 && duplicateLibraries.indexOf('0') != -1) {
+				var d = new Zotero.Duplicates(0);
+				self._showItem(new Zotero.ItemGroup('duplicates', d), 1, newRows+1);
 				newRows++;
 			}
 			
-			var deletedItems = Zotero.Items.getDeleted();
-			if (deletedItems || Zotero.Prefs.get("showTrashWhenEmpty")) {
-				self._showItem(new Zotero.ItemGroup('trash', false), 1, newRows+1);
+			// Unfiled items
+			if (unfiledLibraries.indexOf('0') != -1) {
+				var s = new Zotero.Search;
+				s.name = Zotero.getString('pane.collections.unfiled');
+				s.addCondition('libraryID', 'is', null);
+				s.addCondition('unfiled', 'true');
+				self._showItem(new Zotero.ItemGroup('unfiled', s), 1, newRows+1);
 				newRows++;
 			}
-			self.trashNotEmpty = !!deletedItems;
+			
+			if (self.hideSources.indexOf('trash') == -1) {
+				var deletedItems = Zotero.Items.getDeleted();
+				if (deletedItems || Zotero.Prefs.get("showTrashWhenEmpty")) {
+					self._showItem(new Zotero.ItemGroup('trash', false), 1, newRows+1);
+					newRows++;
+				}
+				self.trashNotEmpty = !!deletedItems;
+			}
 			
 			return newRows;
 		}
@@ -195,15 +218,22 @@ Zotero.CollectionTreeView.prototype.refresh = function()
 						}
 					}
 					
+					// Duplicate items
+					if (self.hideSources.indexOf('duplicates') == -1
+							&& duplicateLibraries.indexOf(groups[i].libraryID + '') != -1) {
+						var d = new Zotero.Duplicates(groups[i].libraryID);
+						self._showItem(new Zotero.ItemGroup('duplicates', d), 2);
+						newRows++;
+					}
+					
 					// Unfiled items
 					if (unfiledLibraries.indexOf(groups[i].libraryID + '') != -1) {
 						var s = new Zotero.Search;
-						s.id = parseInt('8634533000' + groups[i].libraryID); // 'UNFILED' + '000' + libraryID
 						s.libraryID = groups[i].libraryID;
 						s.name = Zotero.getString('pane.collections.unfiled');
 						s.addCondition('libraryID', 'is', groups[i].libraryID);
 						s.addCondition('unfiled', 'true');
-						self._showItem(new Zotero.ItemGroup('search', s), 2);
+						self._showItem(new Zotero.ItemGroup('unfiled', s), 2);
 						newRows++;
 					}
 				}
@@ -221,7 +251,7 @@ Zotero.CollectionTreeView.prototype.refresh = function()
 		}
 	}
 	
-	if (this.showCommons && Zotero.Commons.enabled) {
+	if (this.hideSources.indexOf('commons') == -1 && Zotero.Commons.enabled) {
 		this._showItem(new Zotero.ItemGroup('separator', false));
 		var header = {
 			id: "commons-header",
@@ -246,7 +276,14 @@ Zotero.CollectionTreeView.prototype.refresh = function()
 		}
 	}
 	
-	this._refreshHashMap();
+	try {
+		this._refreshHashMap();
+	}
+	catch (e) {
+		Components.utils.reportError(e);
+		Zotero.debug(e);
+		throw (e);
+	}
 	
 	// Update the treebox's row count
 	var diff = this.rowCount - oldCount;
@@ -274,7 +311,7 @@ Zotero.CollectionTreeView.prototype.reload = function()
 	for(var i = 0; i < openCollections.length; i++)
 	{
 		var row = this._collectionRowMap[openCollections[i]];
-		if (row != null) {
+		if (typeof row != 'undefined') {
 			this.toggleOpenState(row);
 		}
 	}
@@ -313,22 +350,20 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids)
 			switch (type)
 			{
 				case 'collection':
-					if(this._collectionRowMap[ids[i]] != null)
-					{
-						rows.push(this._collectionRowMap[ids[i]]);
+					if (typeof this._rowMap['C' + ids[i]] != 'undefined') {
+						rows.push(this._rowMap['C' + ids[i]]);
 					}
 					break;
 				
 				case 'search':
-					if(this._searchRowMap[ids[i]] != null)
-					{
-						rows.push(this._searchRowMap[ids[i]]);
+					if (typeof this._rowMap['S' + ids[i]] != 'undefined') {
+						rows.push(this._rowMap['S' + ids[i]]);
 					}
 					break;
 				
 				case 'group':
-					//if (this._groupRowMap[ids[i]] != null) {
-					//	rows.push(this._groupRowMap[ids[i]]);
+					//if (this._rowMap['G' + ids[i]] != null) {
+					//	rows.push(this._rowMap['G' + ids[i]]);
 					//}
 					
 					// For now, just reload if a group is removed, since otherwise
@@ -410,7 +445,7 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids)
 					this.rememberSelection(savedSelection);
 					break;
 				}
-				this.selection.select(this._searchRowMap[ids]);
+				this.selection.select(this._rowMap['S' + ids]);
 				break;
 			
 			case 'group':
@@ -454,21 +489,6 @@ Zotero.CollectionTreeView.prototype.unregister = function()
 	Zotero.Notifier.unregisterObserver(this._unregisterID);
 }
 
-Zotero.CollectionTreeView.prototype.isLibrary = function(row)
-{
-	return this._getItemAtRow(row).isLibrary();
-}
-
-Zotero.CollectionTreeView.prototype.isCollection = function(row)
-{
-	return this._getItemAtRow(row).isCollection();
-}
-
-Zotero.CollectionTreeView.prototype.isSearch = function(row)
-{
-	return this._getItemAtRow(row).isSearch();
-}
-
 
 ////////////////////////////////////////////////////////////////////////////////
 ///
@@ -498,17 +518,6 @@ Zotero.CollectionTreeView.prototype.getImageSrc = function(row, col)
 			}
 			break;
 		
-		case 'collection':
-			// TODO: group collection
-			return "chrome://zotero-platform/content/treesource-collection.png";
- 		
-		case 'search':
-			if ((source.ref.id + "").match(/^8634533000/)) { // 'UNFILED000'
-				collectionType = "search-virtual";
-				break;
-			}
-			return "chrome://zotero-platform/content/treesource-search.png";
-		
 		case 'header':
 			if (source.ref.id == 'group-libraries-header') {
 				collectionType = 'groups';
@@ -521,7 +530,12 @@ Zotero.CollectionTreeView.prototype.getImageSrc = function(row, col)
 		case 'group':
 			collectionType = 'library';
 			break;
+		
+		case 'collection':
+		case 'search':
+			return "chrome://zotero-platform/content/treesource-" + collectionType + ".png";
 	}
+	
 	return "chrome://zotero/skin/treesource-" + collectionType + ".png";
 }
 
@@ -752,12 +766,13 @@ Zotero.CollectionTreeView.prototype.selectLibrary = function (libraryID) {
 	return false;
 }
 
+
 /**
  * Select the last-viewed source
  */
 Zotero.CollectionTreeView.prototype.getLastViewedRow = function () {
 	var lastViewedFolder = Zotero.Prefs.get('lastViewedFolder');
-	var matches = lastViewedFolder.match(/^(?:(C|S|G)([0-9]+)|L)$/);
+	var matches = lastViewedFolder.match(/^([A-Z])([0-9]+)?$/);
 	var select = 0;
 	if (matches) {
 		if (matches[1] == 'C') {
@@ -813,11 +828,11 @@ Zotero.CollectionTreeView.prototype.getLastViewedRow = function () {
 				}
 			}
 		}
-		else if (matches[1] == 'S' && this._searchRowMap[matches[2]]) {
-			select = this._searchRowMap[matches[2]];
-		}
-		else if (matches[1] == 'G' && this._groupRowMap[matches[2]]) {
-			select = this._groupRowMap[matches[2]];
+		else {
+			var id = matches[1] + (matches[2] ? matches[2] : "");
+			if (this._rowMap[id]) {
+				select = this._rowMap[id];
+			}
 		}
 	}
 	
@@ -929,20 +944,12 @@ Zotero.CollectionTreeView.prototype.saveSelection = function()
 	for (var i=0, len=this.rowCount; i<len; i++) {
 		if (this.selection.isSelected(i)) {
 			var itemGroup = this._getItemAtRow(i);
-			if (itemGroup.isLibrary()) {
-				return 'L';
+			var id = itemGroup.id;
+			if (id) {
+				return id;
 			}
-			else if (itemGroup.isCollection()) {
-				return 'C' + itemGroup.ref.id;
-			}
-			else if (itemGroup.isSearch()) {
-				return 'S' + itemGroup.ref.id;
-			}
-			else if (itemGroup.isTrash()) {
-				return 'T';
-			}
-			else if (itemGroup.isGroup()) {
-				return 'G' + itemGroup.ref.id;
+			else {
+				break;
 			}
 		}
 	}
@@ -954,49 +961,8 @@ Zotero.CollectionTreeView.prototype.saveSelection = function()
  */
 Zotero.CollectionTreeView.prototype.rememberSelection = function(selection)
 {
-	if (!selection) {
-		return;
-	}
-	
-	var id = selection.substr(1);
-	switch (selection.substr(0, 1)) {
-		// Library
-		case 'L':
-			this.selection.select(0);
-			break;
-		
-		// Collection
-		case 'C':
-			// This only selects the collection if it's still visible,
-			// so we open the parent in notify()
-			if (this._collectionRowMap[id] != undefined) {
-				this.selection.select(this._collectionRowMap[id]);
-			}
-			break;
-		
-		// Saved search
-		case 'S':
-			if (this._searchRowMap[id] != undefined) {
-				this.selection.select(this._searchRowMap[id]);
-			}
-			break;
-		
-		// Trash
-		case 'T':
-			if (this._getItemAtRow(this.rowCount-1).isTrash()){
-				this.selection.select(this.rowCount-1);
-			}
-			else {
-				this.selection.select(0);
-			}
-			break;
-		
-		// Group
-		case 'G':
-			if (this._groupRowMap[id] != undefined) {
-				this.selection.select(this._groupRowMap[i]);
-			}
-			break;
+	if (selection && this._rowMap[selection] != 'undefined') {
+		this.selection.select(this._rowMap[selection]);
 	}
 }
 	
@@ -1015,26 +981,22 @@ Zotero.CollectionTreeView.prototype.getSelectedCollection = function(asID) {
 
 
 
-/*
- * Creates hash map of collection and search ids to row indexes
- * e.g., var rowForID = this._collectionRowMap[]
+/**
+ * Creates mapping of item group ids to tree rows
  */
 Zotero.CollectionTreeView.prototype._refreshHashMap = function()
 {	
 	this._collectionRowMap = [];
-	this._searchRowMap = [];
-	this._groupRowMap = [];
-	for(var i=0; i < this.rowCount; i++){
+	this._rowMap = [];
+	for(var i = 0, len = this.rowCount; i < len; i++) {
 		var itemGroup = this._getItemAtRow(i);
-		if (itemGroup.isCollection(i)) {
+		
+		// Collections get special treatment for now
+		if (itemGroup.isCollection()) {
 			this._collectionRowMap[itemGroup.ref.id] = i;
 		}
-		else if (itemGroup.isSearch(i)) {
-			this._searchRowMap[itemGroup.ref.id] = i;
-		}
-		else if (itemGroup.isGroup(i)) {
-			this._groupRowMap[itemGroup.ref.id] = i;
-		}
+		
+		this._rowMap[itemGroup.id] = i;
 	}
 }
 
@@ -1663,7 +1625,36 @@ Zotero.ItemGroup = function(type, ref)
 	this.ref = ref;
 }
 
-Zotero.ItemGroup.prototype.isLibrary = function(includeGlobal)
+
+Zotero.ItemGroup.prototype.__defineGetter__('id', function () {
+	switch (this.type) {
+		case 'library':
+			return 'L';
+		
+		case 'collection':
+			return 'C' + this.ref.id;
+		
+		case 'search':
+			return 'S' + this.ref.id;
+		
+		case 'duplicates':
+			return 'D' + (this.ref.libraryID ? this.ref.libraryID : 0);
+		
+		case 'unfiled':
+			return 'U' + (this.ref.libraryID ? this.ref.libraryID : 0);
+		
+		case 'trash':
+			return 'T';
+		
+		case 'group':
+			return 'G' + this.ref.id;
+		
+		default:
+			return '';
+	}
+});
+
+Zotero.ItemGroup.prototype.isLibrary = function (includeGlobal)
 {
 	if (includeGlobal) {
 		return this.type == 'library' || this.type == 'group';
@@ -1681,14 +1672,12 @@ Zotero.ItemGroup.prototype.isSearch = function()
 	return this.type == 'search';
 }
 
-Zotero.ItemGroup.prototype.isShare = function()
-{
-	return this.type == 'share';
+Zotero.ItemGroup.prototype.isDuplicates = function () {
+	return this.type == 'duplicates';
 }
 
-Zotero.ItemGroup.prototype.isBucket = function()
-{
-	return this.type == 'bucket';
+Zotero.ItemGroup.prototype.isUnfiled = function () {
+	return this.type == 'unfiled';
 }
 
 Zotero.ItemGroup.prototype.isTrash = function()
@@ -1696,18 +1685,29 @@ Zotero.ItemGroup.prototype.isTrash = function()
 	return this.type == 'trash';
 }
 
-Zotero.ItemGroup.prototype.isGroup = function() {
-	return this.type == 'group';
-}
-
 Zotero.ItemGroup.prototype.isHeader = function () {
 	return this.type == 'header';
 }
 
+Zotero.ItemGroup.prototype.isGroup = function() {
+	return this.type == 'group';
+}
+
 Zotero.ItemGroup.prototype.isSeparator = function () {
 	return this.type == 'separator';
 }
 
+Zotero.ItemGroup.prototype.isBucket = function()
+{
+	return this.type == 'bucket';
+}
+
+Zotero.ItemGroup.prototype.isShare = function()
+{
+	return this.type == 'share';
+}
+
+
 
 // Special
 Zotero.ItemGroup.prototype.isWithinGroup = function () {
@@ -1725,7 +1725,7 @@ Zotero.ItemGroup.prototype.__defineGetter__('editable', function () {
 	if (this.isGroup()) {
 		return this.ref.editable;
 	}
-	if (this.isCollection() || this.isSearch()) {
+	if (this.isCollection() || this.isSearch() || this.isDuplicates() || this.isUnfiled()) {
 		var type = Zotero.Libraries.getType(libraryID);
 		if (type == 'group') {
 			var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
@@ -1763,42 +1763,30 @@ Zotero.ItemGroup.prototype.__defineGetter__('filesEditable', function () {
 Zotero.ItemGroup.prototype.getName = function()
 {
 	switch (this.type) {
-		case 'collection':
-			return this.ref.name;
-		
 		case 'library':
 			return Zotero.getString('pane.collections.library');
 		
-		case 'search':
-			return this.ref.name;
-		
-		case 'share':
-			return this.ref.name;
-
-		case 'bucket':
-			return this.ref.name;
-		
 		case 'trash':
 			return Zotero.getString('pane.collections.trash');
 		
-		case 'group':
-			return this.ref.name;
-		
 		case 'header':
 			return this.ref.label;
 		
-		default:
+		case 'separator':
 			return "";
+		
+		default:
+			return this.ref.name;
 	}
 }
 
-Zotero.ItemGroup.prototype.getChildItems = function()
+Zotero.ItemGroup.prototype.getItems = function()
 {
 	switch (this.type) {
 		// Fake results if this is a shared library
 		case 'share':
 			return this.ref.getAll();
-
+		
 		case 'bucket':
 			return this.ref.getItems();
 		
@@ -1823,16 +1811,7 @@ Zotero.ItemGroup.prototype.getChildItems = function()
 	}
 	
 	try {
-		var ids;
-		if (this.showDuplicates) {
-			var duplicates = new Zotero.Duplicate;
-			var tmpTable = s.search(true);
-			ids = duplicates.getIDs(tmpTable);
-			Zotero.DB.query("DROP TABLE " + tmpTable);
-		}
-		else {
-			ids = s.search();
-		}
+		var ids = s.search();
 	}
 	catch (e) {
 		Zotero.DB.rollbackAllTransactions();
@@ -1853,33 +1832,38 @@ Zotero.ItemGroup.prototype.getSearchObject = function() {
 	var includeScopeChildren = false;
 	
 	// Create/load the inner search
-	var s = new Zotero.Search();
-	if (this.isLibrary()) {
-		s.addCondition('libraryID', 'is', null);
-		s.addCondition('noChildren', 'true');
-		includeScopeChildren = true;
-	}
-	else if (this.isGroup()) {
-		s.addCondition('libraryID', 'is', this.ref.libraryID);
-		s.addCondition('noChildren', 'true');
-		includeScopeChildren = true;
-	}
-	else if (this.isCollection()) {
-		s.addCondition('noChildren', 'true');
-		s.addCondition('collectionID', 'is', this.ref.id);
-		if (Zotero.Prefs.get('recursiveCollections')) {
-			s.addCondition('recursive', 'true');
-		}
-		includeScopeChildren = true;
-	}
-	else if (this.isTrash()) {
-		s.addCondition('deleted', 'true');
-	}
-	else if (this.isSearch()) {
+	if (this.ref instanceof Zotero.Search) {
 		var s = this.ref;
 	}
+	else if (this.isDuplicates()) {
+		var s = this.ref.getSearchObject();
+	}
 	else {
-		throw ('Invalid search mode in Zotero.ItemGroup.getSearchObject()');
+		var s = new Zotero.Search();
+		if (this.isLibrary()) {
+			s.addCondition('libraryID', 'is', null);
+			s.addCondition('noChildren', 'true');
+			includeScopeChildren = true;
+		}
+		else if (this.isGroup()) {
+			s.addCondition('libraryID', 'is', this.ref.libraryID);
+			s.addCondition('noChildren', 'true');
+			includeScopeChildren = true;
+		}
+		else if (this.isCollection()) {
+			s.addCondition('noChildren', 'true');
+			s.addCondition('collectionID', 'is', this.ref.id);
+			if (Zotero.Prefs.get('recursiveCollections')) {
+				s.addCondition('recursive', 'true');
+			}
+			includeScopeChildren = true;
+		}
+		else if (this.isTrash()) {
+			s.addCondition('deleted', 'true');
+		}
+		else {
+			throw ('Invalid search mode in Zotero.ItemGroup.getSearchObject()');
+		}
 	}
 	
 	// Create the outer (filter) search
@@ -1914,14 +1898,13 @@ Zotero.ItemGroup.prototype.getChildTags = function() {
 		// TODO: implement?
 		case 'share':
 			return false;
-
+		
 		case 'bucket':
 			return false;
 		
 		case 'header':
 			return false;
 	}
-
 	
 	var s = this.getSearchObject();
 	return Zotero.Tags.getAllWithinSearch(s);
diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js
index 66eb19198..07ec7778f 100644
--- a/chrome/content/zotero/xpcom/data/item.js
+++ b/chrome/content/zotero/xpcom/data/item.js
@@ -106,10 +106,10 @@ Zotero.Item.prototype.__defineGetter__('dateAdded', function () { return this.ge
 Zotero.Item.prototype.__defineGetter__('dateModified', function () { return this.getField('dateModified'); });
 Zotero.Item.prototype.__defineGetter__('firstCreator', function () { return this.getField('firstCreator'); });
 
-Zotero.Item.prototype.__defineGetter__('relatedItems', function () { var ids = this._getRelatedItems(true); return ids ? ids : []; });
+Zotero.Item.prototype.__defineGetter__('relatedItems', function () { var ids = this._getRelatedItems(true); return ids; });
 Zotero.Item.prototype.__defineSetter__('relatedItems', function (arr) { this._setRelatedItems(arr); });
-Zotero.Item.prototype.__defineGetter__('relatedItemsReverse', function () { var ids = this._getRelatedItemsReverse(); return ids ? ids : []; });
-Zotero.Item.prototype.__defineGetter__('relatedItemsBidirectional', function () { var ids = this._getRelatedItemsBidirectional(); return ids ? ids : []; });
+Zotero.Item.prototype.__defineGetter__('relatedItemsReverse', function () { var ids = this._getRelatedItemsReverse(); return ids; });
+Zotero.Item.prototype.__defineGetter__('relatedItemsBidirectional', function () { var ids = this._getRelatedItemsBidirectional(); return ids; });
 
 
 Zotero.Item.prototype.getID = function() {
@@ -608,8 +608,10 @@ Zotero.Item.prototype.getFieldsNotInType = function (itemTypeID, allowBaseConver
 * Return an array of collectionIDs for all collections the item belongs to
 **/
 Zotero.Item.prototype.getCollections = function() {
-	return Zotero.DB.columnQuery("SELECT collectionID FROM collectionItems "
-		+ "WHERE itemID=" + this.id);
+	var ids = Zotero.DB.columnQuery(
+		"SELECT collectionID FROM collectionItems WHERE itemID=?", this.id
+	);
+	return ids ? ids : [];
 }
 
 
@@ -1105,7 +1107,7 @@ Zotero.Item.prototype.addRelatedItem = function (itemID) {
 	}
 	
 	var current = this._getRelatedItems(true);
-	if (current && current.indexOf(itemID) != -1) {
+	if (current.indexOf(itemID) != -1) {
 		Zotero.debug("Item " + this.id + " already related to item "
 			+ itemID + " in Zotero.Item.addItem()");
 		return false;
@@ -1139,11 +1141,9 @@ Zotero.Item.prototype.removeRelatedItem = function (itemID) {
 	itemID = parsedInt;
 	
 	var current = this._getRelatedItems(true);
-	if (current) {
-		var index = current.indexOf(itemID);
-	}
+	var index = current.indexOf(itemID);
 	
-	if (!current || index == -1) {
+	if (index == -1) {
 		Zotero.debug("Item " + this.id + " isn't related to item "
 			+ itemID + " in Zotero.Item.removeRelatedItem()");
 		return false;
@@ -1487,9 +1487,6 @@ Zotero.Item.prototype.save = function() {
 				var removed = [];
 				var newids = [];
 				var currentIDs = this._getRelatedItems(true);
-				if (!currentIDs) {
-					currentIDs = [];
-				}
 				
 				if (this._previousData && this._previousData.related) {
 					for each(var id in this._previousData.related) {
@@ -1763,6 +1760,16 @@ Zotero.Item.prototype.save = function() {
 					sql = "REPLACE INTO deletedItems (itemID) VALUES (?)";
 				}
 				else {
+					// If undeleting, remove any merge-tracking relations
+					var relations = Zotero.Relations.getByURIs(
+						Zotero.URI.getItemURI(this),
+						Zotero.Relations.deletedItemPredicate,
+						false
+					);
+					for each(var relation in relations) {
+						relation.erase();
+					}
+					
 					sql = "DELETE FROM deletedItems WHERE itemID=?";
 				}
 				Zotero.DB.query(sql, this.id);
@@ -1952,9 +1959,6 @@ Zotero.Item.prototype.save = function() {
 				var removed = [];
 				var newids = [];
 				var currentIDs = this._getRelatedItems(true);
-				if (!currentIDs) {
-					currentIDs = [];
-				}
 				
 				if (this._previousData && this._previousData.related) {
 					for each(var id in this._previousData.related) {
@@ -2418,7 +2422,7 @@ Zotero.Item.prototype.setNote = function(text) {
  * Returns child notes of this item
  *
  * @param	{Boolean}	includeTrashed		Include trashed child items
- * @return	{Integer[]}						Array of itemIDs, or FALSE if none
+ * @return	{Integer[]}						Array of itemIDs
  */
 Zotero.Item.prototype.getNotes = function(includeTrashed) {
 	if (this.isNote()) {
@@ -2442,7 +2446,7 @@ Zotero.Item.prototype.getNotes = function(includeTrashed) {
 	
 	var notes = Zotero.DB.query(sql, this.id);
 	if (!notes) {
-		return false;
+		return [];
 	}
 	
 	// Sort by title
@@ -3204,7 +3208,7 @@ Zotero.Item.prototype.__defineGetter__('attachmentText', function () {
  * Returns child attachments of this item
  *
  * @param	{Boolean}	includeTrashed		Include trashed child items
- * @return	{Integer[]}						Array of itemIDs, or FALSE if none
+ * @return	{Integer[]}						Array of itemIDs
  */
 Zotero.Item.prototype.getAttachments = function(includeTrashed) {
 	if (this.isAttachment()) {
@@ -3232,7 +3236,7 @@ Zotero.Item.prototype.getAttachments = function(includeTrashed) {
 	
 	var attachments = Zotero.DB.query(sql, this.id);
 	if (!attachments) {
-		return false;
+		return [];
 	}
 	
 	// Sort by title
@@ -3322,7 +3326,7 @@ Zotero.Item.prototype.addTag = function(name, type) {
 	
 	var matchingTags = Zotero.Tags.getIDs(name, this.libraryID);
 	var itemTags = this.getTags();
-	if (matchingTags && itemTags) {
+	if (matchingTags && itemTags.length) {
 		for each(var id in matchingTags) {
 			if (itemTags.indexOf(id) != -1) {
 				var tag = Zotero.Tags.get(id);
@@ -3422,17 +3426,17 @@ Zotero.Item.prototype.hasTags = function(tagIDs) {
 /**
  * Returns all tags assigned to an item
  *
- * @return	array			Array of Zotero.Tag objects
+ * @return	Array			Array of Zotero.Tag objects
  */
 Zotero.Item.prototype.getTags = function() {
 	if (!this.id) {
-		return false;
+		return [];
 	}
 	var sql = "SELECT tagID, name FROM tags WHERE tagID IN "
 		+ "(SELECT tagID FROM itemTags WHERE itemID=?)";
 	var tags = Zotero.DB.query(sql, this.id);
 	if (!tags) {
-		return false;
+		return [];
 	}
 	
 	var collation = Zotero.getLocaleCollation();
@@ -3755,6 +3759,50 @@ Zotero.Item.prototype.diff = function (item, includeMatches, ignoreFields) {
 }
 
 
+/**
+ * Compare multiple items against this item and return fields that differ
+ *
+ * Currently compares only item data, not primary fields
+ */
+Zotero.Item.prototype.multiDiff = function (otherItems, ignoreFields) {
+	var thisData = this.serialize();
+	
+	var alternatives = {};
+	var hasDiffs = false;
+	
+	for each(var otherItem in otherItems) {
+		var diff = [];
+		var otherData = otherItem.serialize();
+		var numDiffs = Zotero.Items.diff(thisData, otherData, diff);
+		
+		if (numDiffs) {
+			for (var field in diff[1].fields) {
+				if (ignoreFields && ignoreFields.indexOf(field) != -1) {
+					continue;
+				}
+				
+				var value = diff[1].fields[field];
+				
+				if (!alternatives[field]) {
+					hasDiffs = true;
+					alternatives[field] = [value];
+				}
+				else if (alternatives[field].indexOf(value) == -1) {
+					hasDiffs = true;
+					alternatives[field].push(value);
+				}
+			}
+		}
+	}
+	
+	if (!hasDiffs) {
+		return false;
+	}
+	
+	return alternatives;
+}
+
+
 /**
  * Returns an unsaved copy of the item
  *
@@ -3781,13 +3829,14 @@ Zotero.Item.prototype.clone = function(includePrimary, newItem, unsaved) {
 		var sameLibrary = newItem.libraryID == this.libraryID;
 	}
 	else {
-		var newItem = new Zotero.Item(itemTypeID);
+		var newItem = new Zotero.Item;
 		var sameLibrary = true;
 		
 		if (includePrimary) {
 			newItem.id = this.id;
 			newItem.libraryID = this.libraryID;
 			newItem.key = this.key;
+			newItem.itemTypeID = itemTypeID;
 			for (var field in obj.primary) {
 				switch (field) {
 					case 'itemID':
@@ -3799,6 +3848,9 @@ Zotero.Item.prototype.clone = function(includePrimary, newItem, unsaved) {
 				newItem.setField(field, obj.primary[field]);
 			}
 		}
+		else {
+			newItem.setType(itemTypeID);
+		}
 	}
 	
 	var changedFields = {};
@@ -4026,13 +4078,11 @@ Zotero.Item.prototype.erase = function() {
 	
 	// Flag related items for notification
 	var relateds = this._getRelatedItemsBidirectional();
-	if (relateds) {
-		for each(var id in relateds) {
-			var relatedItem = Zotero.Items.get(id);
-			if (changedItems.indexOf(id) != -1) {
-				changedItemsNotifierData[id] = { old: relatedItem.serialize() };
-				changedItems.push(id);
-			}
+	for each(var id in relateds) {
+		var relatedItem = Zotero.Items.get(id);
+		if (changedItems.indexOf(id) != -1) {
+			changedItemsNotifierData[id] = { old: relatedItem.serialize() };
+			changedItems.push(id);
 		}
 	}
 	
@@ -4042,9 +4092,9 @@ Zotero.Item.prototype.erase = function() {
 		//Zotero.Fulltext.clearItemContent(this.id);
 	}
 	
-	// Remove relations
-	var relation = Zotero.URI.getItemURI(this);
-	Zotero.Relations.eraseByURIPrefix(relation);
+	// Remove relations (except for merge tracker)
+	var uri = Zotero.URI.getItemURI(this);
+	Zotero.Relations.eraseByURIPrefix(uri, [Zotero.Relations.deletedItemPredicate]);
 	
 	Zotero.DB.query('DELETE FROM annotations WHERE itemID=?', this.id);
 	Zotero.DB.query('DELETE FROM highlights WHERE itemID=?', this.id);
@@ -4061,7 +4111,7 @@ Zotero.Item.prototype.erase = function() {
 	Zotero.DB.query('DELETE FROM itemSeeAlso WHERE linkedItemID=?', this.id);
 	
 	var tags = this.getTags();
-	if (tags) {
+	if (tags.length) {
 		var hasTags = true;
 		Zotero.DB.query('DELETE FROM itemTags WHERE itemID=?', this.id);
 		// DEBUG: Hack to reload linked items -- replace with something better
@@ -4236,19 +4286,14 @@ Zotero.Item.prototype.toArray = function (mode) {
 	
 	arr.tags = [];
 	var tags = this.getTags();
-	if (tags) {
-		for (var i=0; i<tags.length; i++) {
-			var tag = tags[i].serialize();
-			tag.tag = tag.fields.name;
-			tag.type = tag.fields.type;
-			arr.tags.push(tag);
-		}
+	for (var i=0, len=tags.length; i<len; i++) {
+		var tag = tags[i].serialize();
+		tag.tag = tag.fields.name;
+		tag.type = tag.fields.type;
+		arr.tags.push(tag);
 	}
 	
 	arr.related = this._getRelatedItemsBidirectional();
-	if (!arr.related) {
-		arr.related = [];
-	}
 	
 	return arr;
 }
@@ -4376,16 +4421,12 @@ Zotero.Item.prototype.serialize = function(mode) {
 	
 	arr.tags = [];
 	var tags = this.getTags();
-	if (tags) {
-		for (var i=0; i<tags.length; i++) {
-			arr.tags.push(tags[i].serialize());
-		}
+	for (var i=0, len=tags.length; i<len; i++) {
+		arr.tags.push(tags[i].serialize());
 	}
 	
-	var related = this._getRelatedItems(true);
-	var reverse = this._getRelatedItemsReverse();
-	arr.related = related ? related : [];
-	arr.relatedReverse = reverse ? reverse : [];
+	arr.related = this._getRelatedItems(true);
+	arr.relatedReverse = this._getRelatedItemsReverse();
 	
 	return arr;
 }
@@ -4502,7 +4543,7 @@ Zotero.Item.prototype._loadRelatedItems = function() {
  * Returns related items this item point to
  *
  * @param	bool		asIDs		Return as itemIDs
- * @return	array					Array of itemIDs, or FALSE if none
+ * @return	array					Array of itemIDs
  */
 Zotero.Item.prototype._getRelatedItems = function (asIDs) {
 	if (!this._relatedItemsLoaded) {
@@ -4510,7 +4551,7 @@ Zotero.Item.prototype._getRelatedItems = function (asIDs) {
 	}
 	
 	if (this._relatedItems.length == 0) {
-		return false;
+		return [];
 	}
 	
 	// Return itemIDs
@@ -4534,28 +4575,33 @@ Zotero.Item.prototype._getRelatedItems = function (asIDs) {
 /**
  * Returns related items that point to this item
  *
- * @return	array						Array of itemIDs, or FALSE if none
+ * @return	array						Array of itemIDs
  */
 Zotero.Item.prototype._getRelatedItemsReverse = function () {
 	if (!this.id) {
-		return false;
+		return [];
 	}
 	
 	var sql = "SELECT itemID FROM itemSeeAlso WHERE linkedItemID=?";
-	return Zotero.DB.columnQuery(sql, this.id);
+	var ids = Zotero.DB.columnQuery(sql, this.id);
+	if (!ids) {
+		return [];
+	}
+	return ids;
 }
 
 
 /**
  * Returns related items this item points to and that point to this item
  *
- * @return array|bool		Array of itemIDs, or false if none
+ * @return array		Array of itemIDs
  */
 Zotero.Item.prototype._getRelatedItemsBidirectional = function () {
 	var related = this._getRelatedItems(true);
 	var reverse = this._getRelatedItemsReverse();
-	if (reverse) {
-		if (!related) {
+	
+	if (reverse.length) {
+		if (!related.length) {
 			return reverse;
 		}
 		
@@ -4566,7 +4612,7 @@ Zotero.Item.prototype._getRelatedItemsBidirectional = function () {
 		}
 	}
 	else if (!related) {
-		return false;
+		return [];
 	}
 	return related;
 }
@@ -4582,9 +4628,6 @@ Zotero.Item.prototype._setRelatedItems = function (itemIDs) {
 	}
 	
 	var currentIDs = this._getRelatedItems(true);
-	if (!currentIDs) {
-		currentIDs = [];
-	}
 	var oldIDs = []; // children being kept
 	var newIDs = []; // new children
 	
diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js
index 6e099622d..241dc4f95 100644
--- a/chrome/content/zotero/xpcom/data/items.js
+++ b/chrome/content/zotero/xpcom/data/items.js
@@ -374,6 +374,68 @@ Zotero.Items = new function() {
 	}
 	
 	
+	this.merge = function (item, otherItems) {
+		Zotero.DB.beginTransaction();
+		
+		var otherItemIDs = [];  
+		var itemURI = Zotero.URI.getItemURI(item);
+		
+		for each(var otherItem in otherItems) {
+			// Move child items to master
+			var ids = otherItem.getAttachments(true).concat(otherItem.getNotes(true));
+			for each(var id in ids) {
+				var attachment = Zotero.Items.get(id);
+				
+				// TODO: Skip identical children?
+				
+				attachment.setSource(item.id);
+				attachment.save();
+			}
+			
+			// All other operations are additive only and do not affect the,
+			// old item, which will be put in the trash
+			
+			// Add collections to master
+			var collectionIDs = otherItem.getCollections();
+			for each(var collectionID in collectionIDs) {
+				var collection = Zotero.Collections.get(collectionID);
+				collection.addItem(item.id);
+			}
+			
+			// Add tags to master
+			var tags = otherItem.getTags();
+			for each(var tag in tags) {
+				item.addTagByID(tag.id);
+			}
+			
+			// Related items
+			var relatedItems = otherItem.relatedItemsBidirectional;
+			Zotero.debug(item._getRelatedItems(true));
+			for each(var relatedItemID in relatedItems) {
+				item.addRelatedItem(relatedItemID);
+			}
+			item.save();
+			
+			// Relations
+			Zotero.Relations.copyURIs(
+				item.libraryID,
+				Zotero.URI.getItemURI(item),
+				Zotero.URI.getItemURI(otherItem)
+			);
+			
+			// Add relation to track merge
+			var otherItemURI = Zotero.URI.getItemURI(otherItem);
+			Zotero.Relations.add(item.libraryID, otherItemURI, Zotero.Relations.deletedItemPredicate, itemURI);
+			
+			// Trash other item
+			otherItem.deleted = true;
+			otherItem.save();
+		}
+		
+		Zotero.DB.commitTransaction();
+	}
+	
+	
 	this.trash = function (ids) {
 		ids = Zotero.flattenArguments(ids);
 		
diff --git a/chrome/content/zotero/xpcom/data/relation.js b/chrome/content/zotero/xpcom/data/relation.js
index a2d764846..99a643929 100644
--- a/chrome/content/zotero/xpcom/data/relation.js
+++ b/chrome/content/zotero/xpcom/data/relation.js
@@ -176,6 +176,27 @@ Zotero.Relation.prototype.save = function () {
 }
 
 
+Zotero.Relation.prototype.erase = function () {
+	if (!this.id) {
+		throw ("ID not set in Zotero.Relation.erase()");
+	}
+	
+	Zotero.DB.beginTransaction();
+	
+	var deleteData = {};
+	deleteData[this.id] = {
+		old: this.serialize()
+	}
+	
+	var sql = "DELETE FROM relations WHERE ROWID=?";
+	Zotero.DB.query(sql, [this.id]);
+	
+	Zotero.DB.commitTransaction();
+	
+	Zotero.Notifier.trigger('delete', 'relation', [this.id], deleteData);
+}
+
+
 Zotero.Relation.prototype.toXML = function () {
 	var xml = <relation/>;
 	xml.subject = this.subject;
@@ -183,3 +204,22 @@ Zotero.Relation.prototype.toXML = function () {
 	xml.object = this.object;
 	return xml;
 }
+
+
+Zotero.Relation.prototype.serialize = function () {
+	// Use a hash of the parts as the object key
+	var key = Zotero.Utilities.Internal.md5(this.subject + "_" + this.predicate + "_" + this.object);
+	
+	var obj = {
+		primary: {
+			libraryID: this.libraryID,
+			key: key,
+		},
+		fields: {
+			subject: this.subject,
+			predicate: this.predicate,
+			object: this.object
+		}
+	};
+	return obj;
+}
diff --git a/chrome/content/zotero/xpcom/data/relations.js b/chrome/content/zotero/xpcom/data/relations.js
index b4ddc066d..3254cbfab 100644
--- a/chrome/content/zotero/xpcom/data/relations.js
+++ b/chrome/content/zotero/xpcom/data/relations.js
@@ -27,7 +27,10 @@ Zotero.Relations = new function () {
 	Zotero.DataObjects.apply(this, ['relation']);
 	this.constructor.prototype = new Zotero.DataObjects();
 	
+	this.__defineGetter__('deletedItemPredicate', function () 'dc:isReplacedBy');
+	
 	var _namespaces = {
+		dc: 'http://purl.org/dc/elements/1.1/',
 		owl: 'http://www.w3.org/2002/07/owl#'
 	};
 	
@@ -46,7 +49,10 @@ Zotero.Relations = new function () {
 	 * @return	{Object[]}
 	 */
 	this.getByURIs = function (subject, predicate, object) {
-		predicate = _getPrefixAndValue(predicate).join(':');
+		if (predicate) {
+			predicate = _getPrefixAndValue(predicate).join(':');
+		}
+		
 		if (!subject && !predicate && !object) {
 			throw ("No values provided in Zotero.Relations.get()");
 		}
@@ -151,34 +157,66 @@ Zotero.Relations = new function () {
 	}
 	
 	
-	this.erase = function (id) {
+	/**
+	 * Copy relations from one object to another within the same library
+	 */
+	this.copyURIs = function (libraryID, fromURI, toURI) {
+		var rels = this.getByURIs(fromURI);
+		for each(var rel in rels) {
+			this.add(libraryID, toURI, rel.predicate, rel.object);
+		}
+		
+		var rels = this.getByURIs(false, false, fromURI);
+		for each(var rel in rels) {
+			this.add(libraryID, rel.subject, rel.predicate, toURI);
+		}
+	}
+	
+	
+	/**
+	 * @param {String} prefix
+	 * @param {String[]} ignorePredicates
+	 */
+	this.eraseByURIPrefix = function (prefix, ignorePredicates) {
 		Zotero.DB.beginTransaction();
 		
-		var sql = "DELETE FROM relations WHERE ROWID=?";
-		Zotero.DB.query(sql, [id]);
+		prefix = prefix + '%';
+		var sql = "SELECT ROWID FROM relations WHERE (subject LIKE ? OR object LIKE ?)";
+		var params = [prefix, prefix];
+		if (ignorePredicates) {
+			sql += " AND predicate != ?";
+			params = params.concat(ignorePredicates);
+		}
+		var ids = Zotero.DB.columnQuery(sql, params);
 		
-		// TODO: log to syncDeleteLog
+		for each(var id in ids) {
+			var relation = this.get(id);
+			relation.erase();
+		}
 		
 		Zotero.DB.commitTransaction();
 	}
 	
 	
-	this.eraseByURIPrefix = function (prefix) {
-		prefix = prefix + '%';
-		var sql = "DELETE FROM relations WHERE subject LIKE ? OR object LIKE ?";
-		Zotero.DB.query(sql, [prefix, prefix]);
-	}
-	
-	
 	this.eraseByURI = function (uri) {
-		var sql = "DELETE FROM relations WHERE subject=? OR object=?";
-		Zotero.DB.query(sql, [uri, uri]);
+		Zotero.DB.beginTransaction();
+		
+		var sql = "SELECT ROWID FROM relations WHERE subject=? OR object=?";
+		var ids = Zotero.DB.columnQuery(sql, [uri, uri]);
+		
+		for each(var id in ids) {
+			var relation = this.get(id);
+			relation.erase();
+		}
+		
+		Zotero.DB.commitTransaction();
 	}
 	
 	
 	this.purge = function () {
-		var sql = "SELECT subject FROM relations UNION SELECT object FROM relations";
-		var uris = Zotero.DB.columnQuery(sql);
+		var sql = "SELECT subject FROM relations WHERE predicate != ? "
+				+ "UNION SELECT object FROM relations WHERE predicate != ?";
+		var uris = Zotero.DB.columnQuery(sql, [this.deletedItemPredicate, this.deletedItemPredicate]);
 		if (uris) {
 			var prefix = Zotero.URI.defaultPrefix;
 			Zotero.DB.beginTransaction();
diff --git a/chrome/content/zotero/xpcom/duplicate.js b/chrome/content/zotero/xpcom/duplicate.js
deleted file mode 100644
index 6f8b2aba1..000000000
--- a/chrome/content/zotero/xpcom/duplicate.js
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
-    ***** BEGIN LICENSE BLOCK *****
-    
-    Copyright © 2009 Center for History and New Media
-                     George Mason University, Fairfax, Virginia, USA
-                     http://zotero.org
-    
-    This file is part of Zotero.
-    
-    Zotero is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-    
-    Zotero is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-    
-    You should have received a copy of the GNU Affero General Public License
-    along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
-    
-    ***** END LICENSE BLOCK *****
-*/
-
-
-Zotero.Duplicate = function(duplicateID) {
-	this._id = duplicateID ? duplicateID : null;
-	this._itemIDs = [];
-}
-
-Zotero.Duplicate.prototype.__defineGetter__('id', function () { return this._id; });
-
-Zotero.Duplicate.prototype.getIDs = function(idsTable) {
-	if (!idsTable)  {
-		return;
-	}
-	
-	var minLen = 5, percentLen = 1./3, checkLen, i, j;
-	
-	var sql = "SELECT itemID, value AS val "
-				+ "FROM " + idsTable + " NATURAL JOIN itemData "
-				+ "NATURAL JOIN itemDataValues "
-				+ "WHERE fieldID BETWEEN 110 AND 113 AND "
-				+ "itemID NOT IN (SELECT itemID FROM itemAttachments) "
-				+ "ORDER BY val";
-	
-	var results = Zotero.DB.query(sql);
-	
-	var resultsLen = results.length;
-	this._itemIDs = [];
-	
-	for (i = 0; i < resultsLen; i++) {
-		results[i].len = results[i].val.length;
-	}
-	
-	for (i = 0; i < resultsLen; i++) {
-		// title must be at least minLen long to be a duplicate
-		if (results[i].len < minLen) {
-			continue;
-		}
-		
-		for (j = i + 1; j < resultsLen; j++) {
-			// duplicates must match the first checkLen characters
-			// checkLen = percentLen * the length of the longer title
-			checkLen = (results[i].len >= results[j].len) ? 
-				parseInt(percentLen * results[i].len) : parseInt(percentLen * results[j].len);
-			checkLen = (checkLen > results[i].len) ? results[i].len : checkLen;
-			checkLen = (checkLen > results[j].len) ? results[j].len : checkLen;
-			checkLen = (checkLen < minLen) ? minLen : checkLen;
-
-			if (results[i].val.substr(0, checkLen) == results[j].val.substr(0, checkLen)) {
-				// include results[i] when a duplicate is first found
-				if (j == i + 1) {
-					this._itemIDs.push(results[i].itemID);
-				}
-				this._itemIDs.push(results[j].itemID);
-			}
-			else {
-				break;
-			}
-		}
-		i = j - 1;
-	}
-	
-	return this._itemIDs;
-}
diff --git a/chrome/content/zotero/xpcom/duplicates.js b/chrome/content/zotero/xpcom/duplicates.js
new file mode 100644
index 000000000..fccc30c6d
--- /dev/null
+++ b/chrome/content/zotero/xpcom/duplicates.js
@@ -0,0 +1,286 @@
+/*
+    ***** BEGIN LICENSE BLOCK *****
+    
+    Copyright © 2009 Center for History and New Media
+                     George Mason University, Fairfax, Virginia, USA
+                     http://zotero.org
+    
+    This file is part of Zotero.
+    
+    Zotero is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+    
+    Zotero is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+    
+    You should have received a copy of the GNU Affero General Public License
+    along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
+    
+    ***** END LICENSE BLOCK *****
+*/
+
+Zotero.Duplicates = function (libraryID) {
+	if (typeof libraryID == 'undefined') {
+		throw ("libraryID not provided in Zotero.Duplicates constructor");
+	}
+	
+	if (!libraryID) {
+		libraryID = null;
+	}
+	
+	this._libraryID = libraryID;
+}
+
+
+Zotero.Duplicates.prototype.__defineGetter__('name', function () "Duplicate Items"); // TODO: localize
+Zotero.Duplicates.prototype.__defineGetter__('libraryID', function () this._libraryID);
+
+
+/**
+ * Get duplicates, populate a temporary table, and return a search based
+ * on that table
+ *
+ * @return {Zotero.Search}
+ */
+Zotero.Duplicates.prototype.getSearchObject = function () {
+	Zotero.DB.beginTransaction();
+	
+	var sql = "DROP TABLE IF EXISTS tmpDuplicates";
+	Zotero.DB.query(sql);
+	
+	var sql = "CREATE TEMPORARY TABLE tmpDuplicates "
+				+ "(id INTEGER PRIMARY KEY)";
+	Zotero.DB.query(sql);
+	
+	this._findDuplicates();
+	var ids = this._sets.findAll(true);
+	
+	sql = "INSERT INTO tmpDuplicates VALUES (?)";
+	var insertStatement = Zotero.DB.getStatement(sql);
+	
+	for each(var id in ids) {
+		insertStatement.bindInt32Parameter(0, id);
+		
+		try {
+			insertStatement.execute();
+		}
+		catch(e) {
+			throw (e + ' [ERROR: ' + Zotero.DB.getLastErrorString() + ']');
+		}
+	}
+	
+	Zotero.DB.commitTransaction();
+	
+	var s = new Zotero.Search;
+	s.libraryID = this._libraryID;
+	s.addCondition('tempTable', 'is', 'tmpDuplicates');
+	return s;
+}
+
+
+/**
+ * Finds all items in the same set as a given item
+ *
+ * @param {Integer} itemID
+ * @return {Integer[]}  Array of itemIDs
+ */
+Zotero.Duplicates.prototype.getSetItemsByItemID = function (itemID) {
+	return this._sets.findAllInSet(this._getObjectFromID(itemID), true);
+}
+
+
+Zotero.Duplicates.prototype._getObjectFromID = function (id) {
+	return {
+		get id() { return id; }
+	}
+}
+
+
+Zotero.Duplicates.prototype._findDuplicates = function () {
+	var self = this;
+	
+	this._sets = new Zotero.DisjointSetForest;
+	var sets = this._sets;
+	
+	function normalizeString(str) {
+		// Make sure we have a string and not an integer
+		str = str + "";
+		
+		str = Zotero.Utilities.removeDiacritics(str)
+			.replace(/[^!-~]/g, ' ') // Convert punctuation to spaces
+			.replace(/ +/, ' ') // Normalize spaces
+			.toLowerCase();
+		
+		return str;
+	}
+	
+	/**
+	 * @param {Function} compareRows  Comparison function, if not exact match
+	 */
+	function processRows(compareRows) {
+		if (!rows) {
+			return;
+		}
+		
+		for (var i = 0, len = rows.length; i < len; i++) {
+			var j = i + 1, lastMatch = false, added = false;
+			while (j < len) {
+				if (compareRows) {
+					var match = compareRows(rows[i], rows[j]);
+					// Not a match, and don't try any more with this i value
+					if (match == -1) {
+						break;
+					}
+					// Not a match, but keep looking
+					if (match == 0) {
+						j++;
+						continue;
+					}
+				}
+				// If no comparison function, check for exact match
+				else {
+					if (rows[i].value != rows[j].value) {
+						break;
+					}
+				}
+				
+				sets.union(
+					self._getObjectFromID(rows[i].itemID),
+					self._getObjectFromID(rows[j].itemID)
+				);
+				
+				lastMatch = j;
+				j++;
+			}
+			if (lastMatch) {
+				i = lastMatch;
+			}
+		}
+	}
+	
+	// Match on normalized title
+	var sql = "SELECT itemID, value FROM items JOIN itemData USING (itemID) "
+				+ "JOIN itemDataValues USING (valueID) "
+				+ "WHERE libraryID=? AND fieldID BETWEEN 110 AND 113 "
+				+ "AND itemTypeID NOT IN (1, 14) "
+				+ "AND itemID NOT IN (SELECT itemID FROM deletedItems) "
+				+ "ORDER BY value COLLATE locale";
+	var rows = Zotero.DB.query(sql, [this._libraryID]);
+	processRows(function (a, b) {
+		a = normalizeString(a.value);
+		b = normalizeString(b.value);
+		// If we stripped one of the strings completely, we can't compare them
+		if (a.length == 0 || b.length == 0) {
+			return -1;
+		}
+		return a == b ? 1 : -1;
+	});
+	
+	// Match on exact fields
+	var fields = ['DOI', 'ISBN'];
+	for each(var field in fields) {
+		var sql = "SELECT itemID, value FROM items JOIN itemData USING (itemID) "
+					+ "JOIN itemDataValues USING (valueID) "
+					+ "WHERE libraryID=? AND fieldID=? "
+					+ "AND itemID NOT IN (SELECT itemID FROM deletedItems) "
+					+ "ORDER BY value";
+		var rows = Zotero.DB.query(sql, [this._libraryID, Zotero.ItemFields.getID(field)]);
+		processRows();
+	}
+}
+
+
+
+/**
+ * Implements the Disjoint Set data structure
+ *
+ *  Based on pseudo-code from http://en.wikipedia.org/wiki/Disjoint-set_data_structure
+ *
+ * Objects passed should have .id properties that uniquely identify them
+ */
+
+Zotero.DisjointSetForest = function () {
+	this._objects = {};
+}
+
+Zotero.DisjointSetForest.prototype.find = function (x) {
+	var id = x.id;
+	
+	// If we've seen this object before, use the existing copy,
+	// which will have .parent and .rank properties
+	if (this._objects[id]) {
+		var obj = this._objects[id];
+	}
+	// Otherwise initialize it as a new set
+	else {
+		this._makeSet(x);
+		this._objects[id] = x;
+		var obj = x;
+	}
+	
+	if (obj.parent.id == obj.id) {
+		return obj;
+	}
+	else {
+		obj.parent = this.find(obj.parent);
+		return obj.parent;
+	}
+}
+
+
+Zotero.DisjointSetForest.prototype.union = function (x, y) {
+	var xRoot = this.find(x);
+	var yRoot = this.find(y);
+	
+	// Already in same set
+	if (xRoot.id == yRoot.id) {
+		return;
+	}
+	
+	if (xRoot.rank < yRoot.rank) {
+		xRoot.parent = yRoot;
+	}
+	else if (xRoot.rank > yRoot.rank) {
+		yRoot.parent = xRoot;
+	}
+	else {
+		yRoot.parent = xRoot;
+		xRoot.rank = xRoot.rank + 1;
+	}
+}
+
+
+Zotero.DisjointSetForest.prototype.sameSet = function (x, y) {
+    return this.find(x) == this.find(y);
+}
+
+
+Zotero.DisjointSetForest.prototype.findAll = function (asIDs) {
+	var objects = [];
+	for each(var obj in this._objects) {
+		objects.push(asIDs ? obj.id : obj);
+	}
+	return objects;
+}
+
+
+Zotero.DisjointSetForest.prototype.findAllInSet = function (x, asIDs) {
+	var xRoot = this.find(x);
+	var objects = [];
+	for each(var obj in this._objects) {
+		if (this.find(obj) == xRoot) {
+			objects.push(asIDs ? obj.id : obj);
+		}
+	}
+	return objects;
+}
+
+
+Zotero.DisjointSetForest.prototype._makeSet = function (x) {
+	x.parent = x;
+	x.rank = 0;
+}
diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js
index f43654990..c75ea060d 100644
--- a/chrome/content/zotero/xpcom/integration.js
+++ b/chrome/content/zotero/xpcom/integration.js
@@ -1203,7 +1203,40 @@ Zotero.Integration.Session.prototype.addCitation = function(index, noteIndex, ar
 		var zoteroItem = false;
 		if(citationItem.uri) {
 			[zoteroItem, needUpdate] = this.uriMap.getZoteroItemForURIs(citationItem.uri);
-			if(needUpdate) this.updateIndices[index] = true;
+			if(zoteroItem) {
+				if(needUpdate) this.updateIndices[index] = true;
+			} else {
+				// Try merged item mappings
+				for each(var uri in citationItem.uri) {
+					var seen = [];
+					
+					// Follow merged item relations until we find an item
+					// or hit a dead end
+					while (!zoteroItem) {
+						var relations = Zotero.Relations.getByURIs(uri, Zotero.Relations.deletedItemPredicate);
+						// No merged items found
+						if(!relations.length) {
+							break;
+						}
+						
+						uri = relations[0].object;
+						
+						// Keep track of mapped URIs in case there's a circular relation
+						if(seen.indexOf(uri) != -1) {
+							var msg = "Circular relation for '" + uri + "' in merged item mapping resolution";
+							Zotero.debug(msg, 2);
+							Components.utils.reportError(msg);
+							break;
+						}
+						seen.push(uri);
+						
+						[zoteroItem, needUpdate] = this.uriMap.getZoteroItemForURIs([uri]);
+					}
+				}
+				
+				if(zoteroItem && needUpdate) this.updateIndices[index] = true;
+			}
+			
 		} else {
 			if(citationItem.key) {
 				zoteroItem = Zotero.Items.getByKey(citationItem.key);
@@ -2044,8 +2077,13 @@ Zotero.Integration.URIMap.prototype.getZoteroItemForURIs = function(uris) {
 	
 	for(var i in uris) {
 		try {
-			zoteroItem = Zotero.URI.getURIItem(uris[i]);	
-			if(zoteroItem) break;
+			zoteroItem = Zotero.URI.getURIItem(uris[i]);
+			if(zoteroItem) {
+				// Ignore items in the trash
+				if(zoteroItem.deleted) {
+					zoteroItem = false;
+				}
+			}
 		} catch(e) {}
 	}
 	
diff --git a/chrome/content/zotero/xpcom/itemTreeView.js b/chrome/content/zotero/xpcom/itemTreeView.js
index ed9feebc4..095c19b75 100644
--- a/chrome/content/zotero/xpcom/itemTreeView.js
+++ b/chrome/content/zotero/xpcom/itemTreeView.js
@@ -122,19 +122,37 @@ Zotero.ItemTreeView.prototype.setTree = function(treebox)
 		obj.refresh();
 		
 		// Add a keypress listener for expand/collapse
-		var expandAllRows = obj.expandAllRows;
-		var collapseAllRows = obj.collapseAllRows;
 		var tree = obj._treebox.treeBody.parentNode;
 		var listener = function(event) {
-			var key = String.fromCharCode(event.which);
-			
-			if (key == '+' && !(event.ctrlKey || event.altKey || event.metaKey)) {
-				obj.expandAllRows(treebox);
+			// Handle arrow keys specially on multiple selection, since
+			// otherwise the tree just applies it to the last-selected row
+			if (event.keyCode == 39 || event.keyCode == 37) {
+				if (obj._treebox.view.selection.count > 1) {
+					switch (event.keyCode) {
+						case 39:
+							obj.expandSelectedRows();
+							break;
+							
+						case 37:
+							obj.collapseSelectedRows();
+							break;
+					}
+					
+					event.preventDefault();
+				}
+				
 				return;
 			}
-			else if (key == '-' && !(event.shiftKey || event.ctrlKey ||
-					event.altKey || event.metaKey)) {
-				obj.collapseAllRows(treebox);
+			
+			var key = String.fromCharCode(event.which);
+			if (key == '+' && !(event.ctrlKey || event.altKey || event.metaKey)) {
+				obj.expandAllRows();
+				event.preventDefault();
+				return;
+			}
+			else if (key == '-' && !(event.shiftKey || event.ctrlKey || event.altKey || event.metaKey)) {
+				obj.collapseAllRows();
+				event.preventDefault();
 				return;
 			}
 		};
@@ -206,7 +224,8 @@ Zotero.ItemTreeView.prototype.refresh = function()
 	Zotero.DB.beginTransaction();
 	Zotero.Items.cacheFields(cacheFields);
 	
-	var newRows = this._itemGroup.getChildItems();
+	var newRows = this._itemGroup.getItems();
+	
 	var added = 0;
 	
 	for (var i=0, len=newRows.length; i < len; i++) {
@@ -276,6 +295,7 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
 	var sort = false;
 	
 	var savedSelection = this.saveSelection();
+	var previousRow = false;
 	
 	// Redraw the tree (for tag color changes)
 	if (action == 'redraw') {
@@ -303,26 +323,27 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
 		return;
 	}
 	
-	if (this._itemGroup.isShare()) {
+	if (itemGroup.isShare()) {
 		return;
 	}
 	
-	this.selection.selectEventsSuppressed = true;
-	
 	// See if we're in the active window
 	var zp = Zotero.getActiveZoteroPane();
 	var activeWindow = zp && zp.itemsView == this;
 	
 	var quicksearch = this._ownerDocument.getElementById('zotero-tb-search');
 	
-	
 	// 'collection-item' ids are in the form collectionID-itemID
 	if (type == 'collection-item') {
+		if (!itemGroup.isCollection()) {
+			return;
+		}
+		
 		var splitIDs = [];
 		for each(var id in ids) {
 			var split = id.split('-');
-			// Skip if not collection or not an item in this collection
-			if (!itemGroup.isCollection() || split[0] != this._itemGroup.ref.id) {
+			// Skip if not an item in this collection
+			if (split[0] != itemGroup.ref.id) {
 				continue;
 			}
 			splitIDs.push(split[1]);
@@ -336,41 +357,51 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
 		}
 	}
 	
+	this.selection.selectEventsSuppressed = true;
+	
 	if ((action == 'remove' && !itemGroup.isLibrary(true))
 			|| action == 'delete' || action == 'trash') {
 		
-		// Since a remove involves shifting of rows, we have to do it in order,
-		// so sort the ids by row
-		var rows = [];
-		for(var i=0, len=ids.length; i<len; i++)
-		{
-			if (action == 'delete' || action == 'trash' ||
-					!itemGroup.ref.hasItem(ids[i])) {
-				// Row might already be gone (e.g. if this is a child and
-				// 'modify' was sent to parent)
-				if (this._itemRowMap[ids[i]] != undefined) {
-					rows.push(this._itemRowMap[ids[i]]);
-				}
-			}
-		}
-		
-		if(rows.length > 0)
-		{
-			rows.sort(function(a,b) { return a-b });
-			
-			for(var i=0, len=rows.length; i<len; i++)
-			{
-				var row = rows[i];
-				if(row != null)
-				{
-					this._hideItem(row-i);
-					this._treebox.rowCountChanged(row-i,-1);
-				}
-			}
-			
+		// On a delete in duplicates mode, just refresh rather than figuring
+		// out what to remove
+		if (itemGroup.isDuplicates()) {
+			previousRow = this._itemRowMap[ids[0]];
+			this.refresh();
 			madeChanges = true;
 			sort = true;
 		}
+		else {
+			// Since a remove involves shifting of rows, we have to do it in order,
+			// so sort the ids by row
+			var rows = [];
+			for (var i=0, len=ids.length; i<len; i++) {
+				if (action == 'delete' || action == 'trash' ||
+						!itemGroup.ref.hasItem(ids[i])) {
+					// Row might already be gone (e.g. if this is a child and
+					// 'modify' was sent to parent)
+					if (this._itemRowMap[ids[i]] != undefined) {
+						rows.push(this._itemRowMap[ids[i]]);
+					}
+				}
+			}
+			
+			if (rows.length > 0) {
+				rows.sort(function(a,b) { return a-b });
+				
+				for(var i=0, len=rows.length; i<len; i++)
+				{
+					var row = rows[i];
+					if(row != null)
+					{
+						this._hideItem(row-i);
+						this._treebox.rowCountChanged(row-i,-1);
+					}
+				}
+				
+				madeChanges = true;
+				sort = true;
+			}
+		}
 	}
 	else if (action == 'modify')
 	{
@@ -435,6 +466,8 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
 					if (item.deleted) {
 						continue;
 					}
+					
+					// Otherwise the item has to be added
 					if(item.isRegularItem() || !item.getSource())
 					{
 						//most likely, the note or attachment's parent was removed.
@@ -580,7 +613,9 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
 		}
 		else
 		{
-			var previousRow = this._itemRowMap[ids[0]];
+			if (previousRow === false) {
+				previousRow = this._itemRowMap[ids[0]];
+			}
 			
 			if (sort) {
 				this.sort(typeof sort == 'number' ? sort : false);
@@ -589,14 +624,25 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
 				this._refreshHashMap();
 			}
 			
-			// On delete, select item at previous position
-			if (action == 'delete' || action == 'remove') {
-				if (this._dataItems[previousRow]) {
-					this.selection.select(previousRow);
+			// On removal of a row, select item at previous position
+			if (action == 'remove' || action == 'trash' || action == 'delete') {
+				// In duplicates view, select the next set on delete
+				if (itemGroup.isDuplicates()) {
+					if (this._dataItems[previousRow]) {
+						// Mirror ZoteroPane.onTreeMouseDown behavior
+						var itemID = this._dataItems[previousRow].ref.id;
+						var setItemIDs = itemGroup.ref.getSetItemsByItemID(itemID);
+						this.selectItems(setItemIDs);
+					}
 				}
-				// If no item at previous position, select last item in list
-				else if (this._dataItems[this._dataItems.length - 1]) {
-					this.selection.select(this._dataItems.length - 1);
+				else {
+					if (this._dataItems[previousRow]) {
+						this.selection.select(previousRow);
+					}
+					// If no item at previous position, select last item in list
+					else if (this._dataItems[this._dataItems.length - 1]) {
+						this.selection.select(this._dataItems.length - 1);
+					}
 				}
 			}
 			else {
@@ -631,7 +677,6 @@ Zotero.ItemTreeView.prototype.unregister = function()
 ////////////////////////////////////////////////////////////////////////////////
 ///
 ///  nsITreeView functions
-///  http://www.xulplanet.com/references/xpcomref/ifaces/nsITreeView.html
 ///
 ////////////////////////////////////////////////////////////////////////////////
 
@@ -1378,6 +1423,41 @@ Zotero.ItemTreeView.prototype.selectItem = function(id, expand, noRecurse)
 	return true;
 }
 
+
+/**
+ * Select multiple top-level items
+ *
+ * @param {Integer[]} ids	An array of itemIDs
+ */
+Zotero.ItemTreeView.prototype.selectItems = function(ids) {
+	if (ids.length == 0) {
+		return;
+	}
+	
+	var rows = [];
+	for each(var id in ids) {
+		rows.push(this._itemRowMap[id]);
+	}
+	rows.sort(function (a, b) {
+		return a - b;
+	});
+	
+	this.selection.clearSelection();
+	
+	this.selection.selectEventsSuppressed = true;
+	
+	var lastStart = 0;
+	for (var i = 0, len = rows.length; i < len; i++) {
+		if (i == len - 1 || rows[i + 1] != rows[i] + 1) {
+			this.selection.rangedSelect(rows[lastStart], rows[i], true);
+			lastStart = i + 1;
+		}
+	}
+	
+	this.selection.selectEventsSuppressed = false;
+}
+
+
 /*
  * Return an array of Item objects for selected items
  *
@@ -1702,7 +1782,7 @@ Zotero.ItemTreeView.prototype.rememberFirstRow = function(firstRow) {
 }
 
 
-Zotero.ItemTreeView.prototype.expandAllRows = function(treebox) {
+Zotero.ItemTreeView.prototype.expandAllRows = function() {
 	this._treebox.beginUpdateBatch();
 	for (var i=0; i<this.rowCount; i++) {
 		if (this.isContainer(i) && !this.isContainerOpen(i)) {
@@ -1714,7 +1794,7 @@ Zotero.ItemTreeView.prototype.expandAllRows = function(treebox) {
 }
 
 
-Zotero.ItemTreeView.prototype.collapseAllRows = function(treebox) {
+Zotero.ItemTreeView.prototype.collapseAllRows = function() {
 	this._treebox.beginUpdateBatch();
 	for (var i=0; i<this.rowCount; i++) {
 		if (this.isContainer(i) && this.isContainerOpen(i)) {
@@ -1726,6 +1806,38 @@ Zotero.ItemTreeView.prototype.collapseAllRows = function(treebox) {
 }
 
 
+Zotero.ItemTreeView.prototype.expandSelectedRows = function() {
+	var start = {}, end = {};
+	this._treebox.beginUpdateBatch();
+	for (var i = 0, len = this.selection.getRangeCount(); i<len; i++) {
+		this.selection.getRangeAt(i, start, end);
+		for (var j = start.value; j <= end.value; j++) {
+			if (this.isContainer(j) && !this.isContainerOpen(j)) {
+				this.toggleOpenState(j, true);
+			}
+		}
+	}
+	this._refreshHashMap();
+	this._treebox.endUpdateBatch();
+}
+
+
+Zotero.ItemTreeView.prototype.collapseSelectedRows = function() {
+	var start = {}, end = {};
+	this._treebox.beginUpdateBatch();
+	for (var i = 0, len = this.selection.getRangeCount(); i<len; i++) {
+		this.selection.getRangeAt(i, start, end);
+		for (var j = start.value; j <= end.value; j++) {
+			if (this.isContainer(j) && this.isContainerOpen(j)) {
+				this.toggleOpenState(j, true);
+			}
+		}
+	}
+	this._refreshHashMap();
+	this._treebox.endUpdateBatch();
+}
+
+
 Zotero.ItemTreeView.prototype.getVisibleFields = function() {
 	var columns = [];
 	for (var i=0, len=this._treebox.columns.count; i<len; i++) {
diff --git a/chrome/content/zotero/xpcom/notifier.js b/chrome/content/zotero/xpcom/notifier.js
index deb96bf78..14fab27df 100644
--- a/chrome/content/zotero/xpcom/notifier.js
+++ b/chrome/content/zotero/xpcom/notifier.js
@@ -28,7 +28,7 @@ Zotero.Notifier = new function(){
 	var _disabled = false;
 	var _types = [
 		'collection', 'creator', 'search', 'share', 'share-items', 'item',
-		'collection-item', 'item-tag', 'tag', 'group', 'bucket'
+		'collection-item', 'item-tag', 'tag', 'group', 'bucket', 'relation'
 	];
 	var _inTransaction;
 	var _locked = false;
@@ -90,7 +90,7 @@ Zotero.Notifier = new function(){
 	*
 	* 	event: 'add', 'modify', 'delete', 'move' ('c', for changing parent),
 	*		'remove' (ci, it), 'refresh', 'redraw', 'trash'
-	* 	type - 'collection', 'search', 'item', 'collection-item', 'item-tag', 'tag', 'group'
+	* 	type - 'collection', 'search', 'item', 'collection-item', 'item-tag', 'tag', 'group', 'relation'
 	* 	ids - single id or array of ids
 	*
 	* Notes:
@@ -152,7 +152,7 @@ Zotero.Notifier = new function(){
 		}
 		
 		for (var i in _observers.items){
-			Zotero.debug("Calling notify() on observer with hash '" + i + "'", 4);
+			Zotero.debug("Calling notify('" + event + "') on observer with hash '" + i + "'", 4);
 			// Find observers that handle notifications for this type (or all types)
 			if (!_observers.get(i).types || _observers.get(i).types.indexOf(type)!=-1){
 				// Catch exceptions so all observers get notified even if
diff --git a/chrome/content/zotero/xpcom/search.js b/chrome/content/zotero/xpcom/search.js
index 57d453724..88c3f2f38 100644
--- a/chrome/content/zotero/xpcom/search.js
+++ b/chrome/content/zotero/xpcom/search.js
@@ -984,7 +984,7 @@ Zotero.Search.prototype._buildQuery = function(){
 		var data = Zotero.SearchConditions.get(this._conditions[i]['condition']);
 		
 		// Has a table (or 'savedSearch', which doesn't have a table but isn't special)
-		if (data.table || data.name == 'savedSearch') {
+		if (data.table || data.name == 'savedSearch' || data.name == 'tempTable') {
 			conditions.push({
 				name: data['name'],
 				alias: data['name']!=this._conditions[i]['condition']
@@ -1283,6 +1283,14 @@ Zotero.Search.prototype._buildQuery = function(){
 						openParens++;
 						break;
 					
+					case 'tempTable':
+						if (!condition.value.match(/^[a-zA-Z0-9]+$/)) {
+							throw ("Invalid temp table '" + condition.value + "'");
+						}
+						condSQL += "itemID IN (SELECT id FROM " + condition.value + ")";
+						skipOperators = true;
+						break;
+						
 					// For quicksearch blocks
 					case 'blockStart':
 					case 'blockEnd':
@@ -2142,6 +2150,13 @@ Zotero.SearchConditions = new function(){
 					doesNotContain: true
 				},
 				special: false
+			},
+			
+			{
+				name: 'tempTable',
+				operators: {
+					is: true
+				}
 			}
 		];
 		
diff --git a/chrome/content/zotero/xpcom/sync.js b/chrome/content/zotero/xpcom/sync.js
index 53e1813da..f6e30101a 100644
--- a/chrome/content/zotero/xpcom/sync.js
+++ b/chrome/content/zotero/xpcom/sync.js
@@ -223,6 +223,10 @@ Zotero.Sync = new function() {
 	
 	
 	function _loadObjectTypes() {
+		// TEMP: Take this out once system.sql > 31
+		var sql = "UPDATE syncObjectTypes SET name='relation' WHERE syncObjectTypeID=6 AND name='relations'";
+		Zotero.DB.query(sql);
+		
 		var sql = "SELECT * FROM syncObjectTypes";
 		var types = Zotero.DB.query(sql);
 		for each(var type in types) {
diff --git a/chrome/content/zotero/xpcom/utilities.js b/chrome/content/zotero/xpcom/utilities.js
index 4e2920b5e..ceff75210 100644
--- a/chrome/content/zotero/xpcom/utilities.js
+++ b/chrome/content/zotero/xpcom/utilities.js
@@ -558,7 +558,125 @@ Zotero.Utilities = {
 		
 		return newString;
 	},
-
+	
+	/**
+	 * Replaces accented characters in a string with ASCII equivalents
+	 *
+	 * @param {String} str
+	 * @param {Boolean} [lowercaseOnly]  Limit conversions to lowercase characters
+	 *                                   (for improved performance on lowercase input)
+	 * @return {String}
+	 *
+	 * From http://lehelk.com/2011/05/06/script-to-remove-diacritics/
+	 */
+	"removeDiacritics": function (str, lowercaseOnly) {
+		var map = this._diacriticsRemovalMap.lowercase;
+		for (var i=0, len=map.length; i<len; i++) {
+			str = str.replace(map[i].letters, map[i].base);
+		}
+		
+		if (!lowercaseOnly) {
+			var map = this._diacriticsRemovalMap.uppercase;
+			for (var i=0, len=map.length; i<len; i++) {
+				str = str.replace(map[i].letters, map[i].base);
+			}
+		}
+		
+		return str;
+	},
+	
+	"_diacriticsRemovalMap": {
+		uppercase: [
+			{'base':'A', 'letters':/[\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F]/g},
+			{'base':'AA','letters':/[\uA732]/g},
+			{'base':'AE','letters':/[\u00C6\u01FC\u01E2]/g},
+			{'base':'AO','letters':/[\uA734]/g},
+			{'base':'AU','letters':/[\uA736]/g},
+			{'base':'AV','letters':/[\uA738\uA73A]/g},
+			{'base':'AY','letters':/[\uA73C]/g},
+			{'base':'B', 'letters':/[\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181]/g},
+			{'base':'C', 'letters':/[\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E]/g},
+			{'base':'D', 'letters':/[\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779]/g},
+			{'base':'DZ','letters':/[\u01F1\u01C4]/g},
+			{'base':'Dz','letters':/[\u01F2\u01C5]/g},
+			{'base':'E', 'letters':/[\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E]/g},
+			{'base':'F', 'letters':/[\u0046\u24BB\uFF26\u1E1E\u0191\uA77B]/g},
+			{'base':'G', 'letters':/[\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E]/g},
+			{'base':'H', 'letters':/[\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D]/g},
+			{'base':'I', 'letters':/[\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197]/g},
+			{'base':'J', 'letters':/[\u004A\u24BF\uFF2A\u0134\u0248]/g},
+			{'base':'K', 'letters':/[\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2]/g},
+			{'base':'L', 'letters':/[\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780]/g},
+			{'base':'LJ','letters':/[\u01C7]/g},
+			{'base':'Lj','letters':/[\u01C8]/g},
+			{'base':'M', 'letters':/[\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C]/g},
+			{'base':'N', 'letters':/[\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4]/g},
+			{'base':'NJ','letters':/[\u01CA]/g},
+			{'base':'Nj','letters':/[\u01CB]/g},
+			{'base':'O', 'letters':/[\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C]/g},
+			{'base':'OI','letters':/[\u01A2]/g},
+			{'base':'OO','letters':/[\uA74E]/g},
+			{'base':'OU','letters':/[\u0222]/g},
+			{'base':'P', 'letters':/[\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754]/g},
+			{'base':'Q', 'letters':/[\u0051\u24C6\uFF31\uA756\uA758\u024A]/g},
+			{'base':'R', 'letters':/[\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782]/g},
+			{'base':'S', 'letters':/[\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784]/g},
+			{'base':'T', 'letters':/[\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786]/g},
+			{'base':'TZ','letters':/[\uA728]/g},
+			{'base':'U', 'letters':/[\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244]/g},
+			{'base':'V', 'letters':/[\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245]/g},
+			{'base':'VY','letters':/[\uA760]/g},
+			{'base':'W', 'letters':/[\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72]/g},
+			{'base':'X', 'letters':/[\u0058\u24CD\uFF38\u1E8A\u1E8C]/g},
+			{'base':'Y', 'letters':/[\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE]/g},
+			{'base':'Z', 'letters':/[\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762]/g},
+		],
+		
+		lowercase: [
+			{'base':'a', 'letters':/[\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250]/g},
+			{'base':'aa','letters':/[\uA733]/g},
+			{'base':'ae','letters':/[\u00E6\u01FD\u01E3]/g},
+			{'base':'ao','letters':/[\uA735]/g},
+			{'base':'au','letters':/[\uA737]/g},
+			{'base':'av','letters':/[\uA739\uA73B]/g},
+			{'base':'ay','letters':/[\uA73D]/g},
+			{'base':'b', 'letters':/[\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253]/g},
+			{'base':'c', 'letters':/[\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184]/g},
+			{'base':'d', 'letters':/[\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A]/g},
+			{'base':'dz','letters':/[\u01F3\u01C6]/g},
+			{'base':'e', 'letters':/[\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD]/g},
+			{'base':'f', 'letters':/[\u0066\u24D5\uFF46\u1E1F\u0192\uA77C]/g},
+			{'base':'g', 'letters':/[\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F]/g},
+			{'base':'h', 'letters':/[\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265]/g},
+			{'base':'hv','letters':/[\u0195]/g},
+			{'base':'i', 'letters':/[\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131]/g},
+			{'base':'j', 'letters':/[\u006A\u24D9\uFF4A\u0135\u01F0\u0249]/g},
+			{'base':'k', 'letters':/[\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3]/g},
+			{'base':'l', 'letters':/[\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747]/g},
+			{'base':'lj','letters':/[\u01C9]/g},
+			{'base':'m', 'letters':/[\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F]/g},
+			{'base':'n', 'letters':/[\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5]/g},
+			{'base':'nj','letters':/[\u01CC]/g},
+			{'base':'o', 'letters':/[\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275]/g},
+			{'base':'oi','letters':/[\u01A3]/g},
+			{'base':'ou','letters':/[\u0223]/g},
+			{'base':'oo','letters':/[\uA74F]/g},
+			{'base':'p','letters':/[\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755]/g},
+			{'base':'q','letters':/[\u0071\u24E0\uFF51\u024B\uA757\uA759]/g},
+			{'base':'r','letters':/[\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783]/g},
+			{'base':'s','letters':/[\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B]/g},
+			{'base':'t','letters':/[\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787]/g},
+			{'base':'tz','letters':/[\uA729]/g},
+			{'base':'u','letters':/[\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289]/g},
+			{'base':'v','letters':/[\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C]/g},
+			{'base':'vy','letters':/[\uA761]/g},
+			{'base':'w','letters':/[\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73]/g},
+			{'base':'x','letters':/[\u0078\u24E7\uFF58\u1E8B\u1E8D]/g},
+			{'base':'y','letters':/[\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF]/g},
+			{'base':'z','letters':/[\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763]/g}
+		]
+	},
+	
 	/**
 	 * Run sets of data through multiple asynchronous callbacks
 	 *
diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js
index 31d6ca817..a052a5a12 100644
--- a/chrome/content/zotero/zoteroPane.js
+++ b/chrome/content/zotero/zoteroPane.js
@@ -53,7 +53,6 @@ var ZoteroPane = new function()
 	this.clearTagSelection = clearTagSelection;
 	this.updateTagFilter = updateTagFilter;
 	this.onCollectionSelected = onCollectionSelected;
-	this.itemSelected = itemSelected;
 	this.reindexItem = reindexItem;
 	this.duplicateSelectedItem = duplicateSelectedItem;
 	this.editSelectedCollection = editSelectedCollection;
@@ -93,9 +92,6 @@ var ZoteroPane = new function()
 	var titlebarcolorState, titleState, observerService;
 	var _reloadFunctions = [];
 	
-	// Also needs to be changed in collectionTreeView.js
-	var _lastViewedFolderRE = /^(?:(C|S|G)([0-9]+)|L)$/;
-	
 	/**
 	 * Called when the window containing Zotero pane is open
 	 */
@@ -167,6 +163,7 @@ var ZoteroPane = new function()
 		
 		var itemsTree = document.getElementById('zotero-items-tree');
 		itemsTree.controllers.appendController(new Zotero.ItemTreeCommandController(itemsTree));
+		itemsTree.addEventListener("mousedown", ZoteroPane_Local.onTreeMouseDown, true);
 		itemsTree.addEventListener("click", ZoteroPane_Local.onTreeClick, true);
 		
 		var menu = document.getElementById("contentAreaContextMenu");
@@ -249,10 +246,6 @@ var ZoteroPane = new function()
 			sep.nextSibling.nextSibling.hidden = false;
 			sep.nextSibling.nextSibling.nextSibling.hidden = false;
 		}
-		
-		if (Zotero.Prefs.get('debugShowDuplicates')) {
-			document.getElementById('zotero-tb-actions-showDuplicates').hidden = false;
-		}
 	}
 	
 	
@@ -793,9 +786,25 @@ var ZoteroPane = new function()
 		window.openDialog('chrome://zotero/content/searchDialog.xul','','chrome,modal',io);
 	}
 	
-	this.setUnfiled = function (libraryID, show) {
+	
+	this.setVirtual = function (libraryID, mode, show) {
+		switch (mode) {
+			case 'duplicates':
+				var prefKey = 'duplicateLibraries';
+				var lastViewedFolderID = 'D' + (libraryID ? libraryID : 0);
+				break;
+			
+			case 'unfiled':
+				var prefKey = 'unfiledLibraries';
+				var lastViewedFolderID = 'U' + (libraryID ? libraryID : 0);
+				break;
+			
+			default:
+				throw ("Invalid virtual mode '" + mode + "' in ZoteroPane.setVirtual()");
+		}
+		
 		try {
-			var ids = Zotero.Prefs.get('unfiledLibraries').split(',');
+			var ids = Zotero.Prefs.get(prefKey).split(',');
 		}
 		catch (e) {
 			var ids = [];
@@ -829,11 +838,10 @@ var ZoteroPane = new function()
 		
 		newids.sort();
 		
-		Zotero.Prefs.set('unfiledLibraries', newids.join());
+		Zotero.Prefs.set(prefKey, newids.join());
 		
 		if (show) {
-			// 'UNFILED' + '000' + libraryID
-			Zotero.Prefs.set('lastViewedFolder', 'S' + '8634533000' + libraryID);
+			Zotero.Prefs.set('lastViewedFolder', lastViewedFolderID);
 		}
 		
 		this.collectionsView.refresh();
@@ -843,6 +851,7 @@ var ZoteroPane = new function()
 		this.collectionsView.selection.select(row);
 	}
 	
+	
 	this.openLookupWindow = function () {
 		if (!Zotero.stateCheck()) {
 			this.displayErrorMessage(true);
@@ -1039,7 +1048,6 @@ var ZoteroPane = new function()
 		
 		itemgroup.setSearch('');
 		itemgroup.setTags(getTagSelection());
-		itemgroup.showDuplicates = false;
 		
 		try {
 			Zotero.UnresponsiveScriptIndicator.disable();
@@ -1052,50 +1060,28 @@ var ZoteroPane = new function()
 			Zotero.UnresponsiveScriptIndicator.enable();
 		}
 		
-		if (itemgroup.isLibrary()) {
-			Zotero.Prefs.set('lastViewedFolder', 'L');
-		}
-		if (itemgroup.isCollection()) {
-			Zotero.Prefs.set('lastViewedFolder', 'C' + itemgroup.ref.id);
-		}
-		else if (itemgroup.isSearch()) {
-			Zotero.Prefs.set('lastViewedFolder', 'S' + itemgroup.ref.id);
-		}
-		else if (itemgroup.isGroup()) {
-			Zotero.Prefs.set('lastViewedFolder', 'G' + itemgroup.ref.id);
-		}
+		Zotero.Prefs.set('lastViewedFolder', itemgroup.id);
 	}
 	
 	
-	this.showDuplicates = function () {
-		if (this.collectionsView.selection.count == 1 && this.collectionsView.selection.currentIndex != -1) {
-			var itemGroup = this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex);
-			itemGroup.showDuplicates = true;
-			
-			try {
-				Zotero.UnresponsiveScriptIndicator.disable();
-				this.itemsView.refresh();
-			}
-			finally {
-				Zotero.UnresponsiveScriptIndicator.enable();
-			}
-		}
-	}
-	
 	this.getItemGroup = function () {
 		return this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex);
 	}
 	
-
-	function itemSelected()
-	{
+	
+	this.itemSelected = function (event) {
 		if (!Zotero.stateCheck()) {
 			this.displayErrorMessage();
 			return;
 		}
 		
+		// DEBUG: Is this actually possible?
+		if (!this.itemsView) {
+			Components.utils.reportError("this.itemsView is not defined in ZoteroPane.itemSelected()");
+		}
+		
 		// Display restore button if items selected in Trash
-		if (this.itemsView && this.itemsView.selection.count) {
+		if (this.itemsView.selection.count) {
 			document.getElementById('zotero-item-restore-button').hidden
 				= !this.itemsView._itemGroup.isTrash()
 					|| _nonDeletedItemsSelected(this.itemsView);
@@ -1111,32 +1097,34 @@ var ZoteroPane = new function()
 			document.getElementById('zotero-note-editor').save();
 		}
 		
+		var itemGroup = this.getItemGroup();
+		
 		// Single item selected
-		if (this.itemsView && this.itemsView.selection.count == 1 && this.itemsView.selection.currentIndex != -1)
+		if (this.itemsView.selection.count == 1 && this.itemsView.selection.currentIndex != -1)
 		{
-			var item = this.itemsView._getItemAtRow(this.itemsView.selection.currentIndex);
+			var item = this.itemsView.getSelectedItems()[0];
 			
-			if(item.ref.isNote()) {
+			if (item.isNote()) {
 				var noteEditor = document.getElementById('zotero-note-editor');
 				noteEditor.mode = this.collectionsView.editable ? 'edit' : 'view';
 				
 				// If loading new or different note, disable undo while we repopulate the text field
 				// so Undo doesn't end up clearing the field. This also ensures that Undo doesn't
 				// undo content from another note into the current one.
-				if (!noteEditor.item || noteEditor.item.id != item.ref.id) {
+				if (!noteEditor.item || noteEditor.item.id != item.id) {
 					noteEditor.disableUndo();
 				}
 				noteEditor.parent = null;
-				noteEditor.item = item.ref;
+				noteEditor.item = item;
 				
 				noteEditor.enableUndo();
 				
 				var viewButton = document.getElementById('zotero-view-note-button');
 				if (this.collectionsView.editable) {
 					viewButton.hidden = false;
-					viewButton.setAttribute('noteID', item.ref.id);
-					if (item.ref.getSource()) {
-						viewButton.setAttribute('sourceID', item.ref.getSource());
+					viewButton.setAttribute('noteID', item.id);
+					if (item.getSource()) {
+						viewButton.setAttribute('sourceID', item.getSource());
 					}
 					else {
 						viewButton.removeAttribute('sourceID');
@@ -1149,17 +1137,17 @@ var ZoteroPane = new function()
 				document.getElementById('zotero-item-pane-content').selectedIndex = 2;
 			}
 			
-			else if(item.ref.isAttachment()) {
+			else if (item.isAttachment()) {
 				var attachmentBox = document.getElementById('zotero-attachment-box');
 				attachmentBox.mode = this.collectionsView.editable ? 'edit' : 'view';
-				attachmentBox.item = item.ref;
+				attachmentBox.item = item;
 				
 				document.getElementById('zotero-item-pane-content').selectedIndex = 3;
 			}
 			
 			// Regular item
 			else {
-				var isCommons = this.getItemGroup().isBucket();
+				var isCommons = itemGroup.isBucket();
 				
 				document.getElementById('zotero-item-pane-content').selectedIndex = 1;
 				var tabBox = document.getElementById('zotero-view-tabbox');
@@ -1176,26 +1164,65 @@ var ZoteroPane = new function()
 				}
 				
 				if (this.collectionsView.editable) {
-					ZoteroItemPane.viewItem(item.ref, null, pane);
+					ZoteroItemPane.viewItem(item, null, pane);
 					tabs.selectedIndex = document.getElementById('zotero-view-item').selectedIndex;
 				}
 				else {
-					ZoteroItemPane.viewItem(item.ref, 'view', pane);
+					ZoteroItemPane.viewItem(item, 'view', pane);
 					tabs.selectedIndex = document.getElementById('zotero-view-item').selectedIndex;
 				}
 			}
 		}
 		// Zero or multiple items selected
 		else {
-			document.getElementById('zotero-item-pane-content').selectedIndex = 0;
+			var count = this.itemsView.selection.count;
 			
-			var label = document.getElementById('zotero-view-selected-label');
-			
-			if (this.itemsView && this.itemsView.selection.count) {
-				label.value = Zotero.getString('pane.item.selected.multiple', this.itemsView.selection.count);
+			// Display duplicates merge interface in item pane
+			if (itemGroup.isDuplicates()) {
+				if (!itemGroup.editable) {
+					if (count) {
+						// TODO: localize
+						var msg = "Library write access is required to merge items.";
+					}
+					else {
+						var msg = Zotero.getString('pane.item.selected.zero');
+					}
+					this.setItemPaneMessage(msg);
+				}
+				else if (count) {
+					document.getElementById('zotero-item-pane-content').selectedIndex = 4;
+					
+					// Load duplicates UI code
+					if (typeof Zotero_Duplicates_Pane == 'undefined') {
+						Zotero.debug("Loading duplicatesMerge.js");
+						Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
+							.getService(Components.interfaces.mozIJSSubScriptLoader)
+							.loadSubScript("chrome://zotero/content/duplicatesMerge.js");
+					}
+					
+					// On a Select All of more than a few items, display a row
+					// count instead of the usual item type mismatch error
+					var displayNumItemsOnTypeError = count > 5 && count == this.itemsView.rowCount;
+					
+					// Initialize the merge pane with the selected items
+					Zotero_Duplicates_Pane.setItems(this.getSelectedItems(), displayNumItemsOnTypeError);
+				}
+				else {
+					// TODO: localize
+					var msg = "Select items to merge";
+					this.setItemPaneMessage(msg);
+				}
 			}
+			// Display label in the middle of the item pane
 			else {
-				label.value = Zotero.getString('pane.item.selected.zero');
+				if (count) {
+					var msg = Zotero.getString('pane.item.selected.multiple', count);
+				}
+				else {
+					var msg = Zotero.getString('pane.item.selected.zero');
+				}
+				
+				this.setItemPaneMessage(msg);
 			}
 		}
 	}
@@ -1400,23 +1427,12 @@ var ZoteroPane = new function()
 			// In collection, only prompt if trashing
 			var prompt = force ? (itemGroup.isWithinGroup() ? toDelete : toTrash) : false;
 		}
-		// This should be changed if/when groups get trash
-		else if (itemGroup.isGroup()) {
-			var prompt = toDelete;
-		}
-		else if (itemGroup.isSearch()) {
+		else if (itemGroup.isSearch() || itemGroup.isUnfiled() || itemGroup.isDuplicates()) {
 			if (!force) {
 				return;
 			}
 			var prompt = toTrash;
 		}
-		// Do nothing in share views
-		else if (itemGroup.isShare()) {
-			return;
-		}
-		else if (itemGroup.isBucket()) {
-			var prompt = toDelete;
-		}
 		// Do nothing in trash view if any non-deleted items are selected
 		else if (itemGroup.isTrash()) {
 			var start = {};
@@ -1431,6 +1447,17 @@ var ZoteroPane = new function()
 			}
 			var prompt = toDelete;
 		}
+		// This should be changed if/when groups get trash
+		else if (itemGroup.isGroup()) {
+			var prompt = toDelete;
+		}
+		else if (itemGroup.isBucket()) {
+			var prompt = toDelete;
+		}
+		// Do nothing in share views
+		else if (itemGroup.isShare()) {
+			return;
+		}
 		
 		var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
 										.getService(Components.interfaces.nsIPromptService);
@@ -1439,11 +1466,37 @@ var ZoteroPane = new function()
 		}
 	}
 	
+	
+	this.mergeSelectedItems = function () {
+		if (!this.canEdit()) {
+			this.displayCannotEditLibraryMessage();
+			return;
+		}
+		
+		document.getElementById('zotero-item-pane-content').selectedIndex = 4;
+		
+		if (typeof Zotero_Duplicates_Pane == 'undefined') {
+			Zotero.debug("Loading duplicatesMerge.js");
+			Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
+				.getService(Components.interfaces.mozIJSSubScriptLoader)
+				.loadSubScript("chrome://zotero/content/duplicatesMerge.js");
+		}
+		
+		// Initialize the merge pane with the selected items
+		Zotero_Duplicates_Pane.setItems(this.getSelectedItems());
+	}
+	
+	
 	this.deleteSelectedCollection = function () {
-		// Remove virtual Unfiled search
-		var row = this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex);
-		if (row.isSearch() && (row.ref.id + "").match(/^8634533000/)) { // 'UNFILED000'
-			this.setUnfiled(row.ref.libraryID, false);
+		var itemGroup = this.getItemGroup();
+		
+		// Remove virtual duplicates collection
+		if (itemGroup.isDuplicates()) {
+			this.setVirtual(itemGroup.ref.libraryID, 'duplicates', false);
+		}
+		// Remove virtual unfiled collection
+		else if (itemGroup.isUnfiled()) {
+			this.setVirtual(itemGroup.ref.libraryID, 'unfiled', false);
 			return;
 		}
 		
@@ -1453,14 +1506,14 @@ var ZoteroPane = new function()
 		}
 		
 		if (this.collectionsView.selection.count == 1) {
-			if (row.isCollection())
+			if (itemGroup.isCollection())
 			{
 				if (confirm(Zotero.getString('pane.collections.delete')))
 				{
 					this.collectionsView.deleteSelection();
 				}
 			}
-			else if (row.isSearch())
+			else if (itemGroup.isSearch())
 			{
 				if (confirm(Zotero.getString('pane.collections.deleteSearch')))
 				{
@@ -1939,30 +1992,39 @@ var ZoteroPane = new function()
 	
 	this.buildCollectionContextMenu = function buildCollectionContextMenu()
 	{
+		var options = [
+			"newCollection",
+			"newSavedSearch",
+			"newSubcollection",
+			"sep1",
+			"showDuplicates",
+			"showUnfiled",
+			"editSelectedCollection",
+			"removeCollection",
+			"sep2",
+			"exportCollection",
+			"createBibCollection",
+			"exportFile",
+			"loadReport",
+			"emptyTrash",
+			"createCommonsBucket",
+			"refreshCommonsBucket"
+		];
+		
+		var m = {};
+		var i = 0;
+		for each(var option in options) {
+			m[option] = i++;
+		}
+		
 		var menu = document.getElementById('zotero-collectionmenu');
-		var m = {
-			newCollection: 0,
-			newSavedSearch: 1,
-			newSubcollection: 2,
-			sep1: 3,
-			showUnfiled: 4,
-			editSelectedCollection: 5,
-			removeCollection: 6,
-			sep2: 7,
-			exportCollection: 8,
-			createBibCollection: 9,
-			exportFile: 10,
-			loadReport: 11,
-			emptyTrash: 12,
-			createCommonsBucket: 13,
-			refreshCommonsBucket: 14
-		};
 		
 		var itemGroup = this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex);
 		
-		var enable = [], disable = [], show = [];
+		// By default things are hidden and visible, so we only need to record
+		// when things are visible and when they're visible but disabled
+		var show = [], disable = [];
 		
-		// Collection
 		if (itemGroup.isCollection()) {
 			show = [
 				m.newSubcollection,
@@ -1975,15 +2037,13 @@ var ZoteroPane = new function()
 				m.loadReport
 			];
 			var s = [m.exportCollection, m.createBibCollection, m.loadReport];
-			if (this.itemsView.rowCount>0) {
-				enable = s;
-			}
-			else if (!this.collectionsView.isContainerEmpty(this.collectionsView.selection.currentIndex)) {
-				enable = [m.exportCollection];
-				disable = [m.createBibCollection, m.loadReport];
-			}
-			else {
-				disable = s;
+			if (!this.itemsView.rowCount) {
+				if (!this.collectionsView.isContainerEmpty(this.collectionsView.selection.currentIndex)) {
+					disable = [m.createBibCollection, m.loadReport];
+				}
+				else {
+					disable = s;
+				}
 			}
 			
 			// Adjust labels
@@ -1993,35 +2053,20 @@ var ZoteroPane = new function()
 			menu.childNodes[m.createBibCollection].setAttribute('label', Zotero.getString('pane.collections.menu.createBib.collection'));
 			menu.childNodes[m.loadReport].setAttribute('label', Zotero.getString('pane.collections.menu.generateReport.collection'));
 		}
-		// Saved Search
 		else if (itemGroup.isSearch()) {
-			// Unfiled items view
-			if ((itemGroup.ref.id + "").match(/^8634533000/)) { // 'UNFILED000'
-				show = [
-					m.removeCollection
-				];
-				
-				menu.childNodes[m.removeCollection].setAttribute('label', Zotero.getString('general.remove'));
-			}
-			// Normal search view
-			else {
-				show = [
-					m.editSelectedCollection,
-					m.removeCollection,
-					m.sep2,
-					m.exportCollection,
-					m.createBibCollection,
-					m.loadReport
-				];
-				
-				menu.childNodes[m.removeCollection].setAttribute('label', Zotero.getString('pane.collections.menu.remove.savedSearch'));
-			}
+			show = [
+				m.editSelectedCollection,
+				m.removeCollection,
+				m.sep2,
+				m.exportCollection,
+				m.createBibCollection,
+				m.loadReport
+			];
+			
+			menu.childNodes[m.removeCollection].setAttribute('label', Zotero.getString('pane.collections.menu.remove.savedSearch'));
 			
 			var s = [m.exportCollection, m.createBibCollection, m.loadReport];
-			if (this.itemsView.rowCount>0) {
-				enable = s;
-			}
-			else {
+			if (!this.itemsView.rowCount) {
 				disable = s;
 			}
 			
@@ -2031,10 +2076,19 @@ var ZoteroPane = new function()
 			menu.childNodes[m.createBibCollection].setAttribute('label', Zotero.getString('pane.collections.menu.createBib.savedSearch'));
 			menu.childNodes[m.loadReport].setAttribute('label', Zotero.getString('pane.collections.menu.generateReport.savedSearch'));
 		}
-		// Trash
 		else if (itemGroup.isTrash()) {
 			show = [m.emptyTrash];
 		}
+		else if (itemGroup.isGroup()) {
+			show = [m.newCollection, m.newSavedSearch, m.sep1, m.showDuplicates, m.showUnfiled];
+		}
+		else if (itemGroup.isDuplicates() || itemGroup.isUnfiled()) {
+			show = [
+				m.removeCollection
+			];
+			
+			menu.childNodes[m.removeCollection].setAttribute('label', Zotero.getString('general.remove'));
+		}
 		else if (itemGroup.isHeader()) {
 			if (itemGroup.ref.id == 'commons-header') {
 				show = [m.createCommonsBucket];
@@ -2043,67 +2097,63 @@ var ZoteroPane = new function()
 		else if (itemGroup.isBucket()) {
 			show = [m.refreshCommonsBucket];
 		}
-		// Group
-		else if (itemGroup.isGroup()) {
-			show = [m.newCollection, m.newSavedSearch, m.sep1, m.showUnfiled];
-		}
 		// Library
 		else
 		{
-			show = [m.newCollection, m.newSavedSearch, m.sep1, m.showUnfiled, m.sep2, m.exportFile];
+			show = [m.newCollection, m.newSavedSearch, m.showDuplicates, m.showUnfiled, m.sep2, m.exportFile];
 		}
 		
 		// Disable some actions if user doesn't have write access
 		var s = [m.editSelectedCollection, m.removeCollection, m.newCollection, m.newSavedSearch, m.newSubcollection];
-		if (itemGroup.isWithinGroup() && !itemGroup.editable) {
+		if (itemGroup.isWithinGroup() && !itemGroup.editable && !itemGroup.isDuplicates() && !itemGroup.isUnfiled()) {
 			disable = disable.concat(s);
 		}
-		else {
-			enable = enable.concat(s);
-		}
 		
-		for (var i in disable)
-		{
-			menu.childNodes[disable[i]].setAttribute('disabled', true);
-		}
-		
-		for (var i in enable)
-		{
-			menu.childNodes[enable[i]].setAttribute('disabled', false);
-		}
-		
-		// Hide all items by default
+		// Hide and enable all actions by default (so if they're shown they're enabled)
 		for each(var pos in m) {
 			menu.childNodes[pos].setAttribute('hidden', true);
+			menu.childNodes[pos].setAttribute('disabled', false);
 		}
 		
 		for (var i in show)
 		{
 			menu.childNodes[show[i]].setAttribute('hidden', false);
 		}
+		
+		for (var i in disable)
+		{
+			menu.childNodes[disable[i]].setAttribute('disabled', true);
+		}
 	}
 	
 	function buildItemContextMenu()
 	{
-		var m = {
-			showInLibrary: 0,
-			sep1: 1,
-			addNote: 2,
-			addAttachments: 3,
-			sep2: 4,
-			duplicateItem: 5,
-			deleteItem: 6,
-			deleteFromLibrary: 7,
-			sep3: 8,
-			exportItems: 9,
-			createBib: 10,
-			loadReport: 11,
-			sep4: 12,
-			createParent: 13,
-			recognizePDF: 14,
-			renameAttachments: 15,
-			reindexItem: 16
-		};
+		var options = [
+			'showInLibrary',
+			'sep1',
+			'addNote',
+			'addAttachments',
+			'sep2',
+			'duplicateItem',
+			'deleteItem',
+			'deleteFromLibrary',
+			'mergeItems',
+			'sep3',
+			'exportItems',
+			'createBib',
+			'loadReport',
+			'sep4',
+			'createParent',
+			'recognizePDF',
+			'renameAttachments',
+			'reindexItem'
+		];
+		
+		var m = {};
+		var i = 0;
+		for each(var option in options) {
+			m[option] = i++;
+		}
 		
 		var menu = document.getElementById('zotero-itemmenu');
 		
@@ -2112,59 +2162,62 @@ var ZoteroPane = new function()
 			menu.removeChild(menu.firstChild);
 		}
 		
-		var enable = [], disable = [], show = [], hide = [], multiple = '';
+		var disable = [], show = [], multiple = '';
 		
 		if (!this.itemsView) {
 			return;
 		}
 		
+		var itemGroup = this.getItemGroup();
+		
+		show.push(m.deleteItem, m.deleteFromLibrary, m.sep3, m.exportItems, m.createBib, m.loadReport);
+		
 		if (this.itemsView.selection.count > 0) {
-			var itemGroup = this.itemsView._itemGroup;
-			
-			enable.push(m.showInLibrary, m.addNote, m.addAttachments,
-				m.sep2, m.duplicateItem, m.deleteItem, m.deleteFromLibrary,
-				m.exportItems, m.createBib, m.loadReport);
-			
 			// Multiple items selected
 			if (this.itemsView.selection.count > 1) {
 				var multiple =  '.multiple';
-				hide.push(m.showInLibrary, m.sep1, m.addNote, m.addAttachments,
-					m.sep2, m.duplicateItem);
 				
-				// If all items can be reindexed, or all items can be recognized, show option
 				var items = this.getSelectedItems();
-				var canIndex = true;
-				var canRecognize = true;
+				var canMerge = true, canIndex = true, canRecognize = true, canRename = true;
+				
 				if (!Zotero.Fulltext.pdfConverterIsRegistered()) {
 					canIndex = false;
 				}
-				for (var i=0; i<items.length; i++) {
-					if (canIndex && !Zotero.Fulltext.canReindex(items[i].id)) {
+				
+				for each(var item in items) {
+					if (canMerge && !item.isRegularItem() || itemGroup.isDuplicates()) {
+						canMerge = false;
+					}
+					
+					if (canIndex && !Zotero.Fulltext.canReindex(item.id)) {
 						canIndex = false;
 					}
-					if (canRecognize && !Zotero_RecognizePDF.canRecognize(items[i])) {
+					
+					if (canRecognize && !Zotero_RecognizePDF.canRecognize(item)) {
 						canRecognize = false;
 					}
-					if (!canIndex && !canRecognize) {
-						break;
+					
+					// Show rename option only if all items are child attachments
+					if (canRename && (!item.isAttachment() || !item.getSource() || item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL)) {
+						canRename = false;
 					}
 				}
+				
+				if (canMerge) {
+					show.push(m.mergeItems);
+				}
+				
 				if (canIndex) {
 					show.push(m.reindexItem);
 				}
-				else {
-					hide.push(m.reindexItem);
-				}
+				
 				if (canRecognize) {
 					show.push(m.recognizePDF);
-					hide.push(m.createParent);
 				}
 				else {
-					hide.push(m.recognizePDF);
-					
 					var canCreateParent = true;
-					for (var i=0; i<items.length; i++) {
-						if (!items[i].isTopLevelItem() || items[i].isRegularItem() || Zotero_RecognizePDF.canRecognize(items[i])) {
+					for each(var item in items) {
+						if (!item.isTopLevelItem() || !item.isAttachment() || Zotero_RecognizePDF.canRecognize(item)) {
 							canCreateParent = false;
 							break;
 						}
@@ -2172,35 +2225,16 @@ var ZoteroPane = new function()
 					if (canCreateParent) {
 						show.push(m.createParent);
 					}
-					else {
-						hide.push(m.createParent);
-					}
 				}
 				
-				// If all items are child attachments, show rename option
-				var canRename = true;
-				for (var i=0; i<items.length; i++) {
-					var item = items[i];
-					// Same check as in rename function
-					if (!item.isAttachment() || !item.getSource() || item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
-						canRename = false;
-						break;
-					}
-				}
 				if (canRename) {
 					show.push(m.renameAttachments);
 				}
-				else {
-					hide.push(m.renameAttachments);
-				}
 				
 				// Add in attachment separator
 				if (canCreateParent || canRecognize || canRename || canIndex) {
 					show.push(m.sep4);
 				}
-				else {
-					hide.push(m.sep4);
-				}
 				
 				// Block certain actions on files if no access and at least one item
 				// is an imported attachment
@@ -2217,10 +2251,6 @@ var ZoteroPane = new function()
 						var d = [m.deleteFromLibrary, m.createParent, m.renameAttachments];
 						for each(var val in d) {
 							disable.push(val);
-							var index = enable.indexOf(val);
-							if (index != -1) {
-								enable.splice(index, 1);
-							}
 						}
 					}
 				}
@@ -2229,7 +2259,7 @@ var ZoteroPane = new function()
 			// Single item selected
 			else
 			{
-				var item = this.itemsView._getItemAtRow(this.itemsView.selection.currentIndex).ref;
+				var item = this.getSelectedItems()[0];
 				var itemID = item.id;
 				menu.setAttribute('itemID', itemID);
 				
@@ -2237,39 +2267,29 @@ var ZoteroPane = new function()
 				if (!itemGroup.isLibrary() && !itemGroup.isWithinGroup()) {
 					show.push(m.showInLibrary, m.sep1);
 				}
-				else {
-					hide.push(m.showInLibrary, m.sep1);
+				
+				// Disable actions in the trash
+				if (itemGroup.isTrash()) {
+					disable.push(m.deleteItem, m.deleteFromLibrary);
 				}
 				
-				if (item.isRegularItem())
-				{
+				if (item.isRegularItem()) {
 					show.push(m.addNote, m.addAttachments, m.sep2);
 				}
-				else
-				{
-					hide.push(m.addNote, m.addAttachments, m.sep2);
-				}
 				
 				if (item.isAttachment()) {
 					var showSep4 = false;
-					hide.push(m.duplicateItem);
 					
 					if (Zotero_RecognizePDF.canRecognize(item)) {
 						show.push(m.recognizePDF);
-						hide.push(m.createParent);
 						showSep4 = true;
 					}
 					else {
-						hide.push(m.recognizePDF);
-						
 						// If not a PDF, allow parent item creation
 						if (item.isTopLevelItem()) {
 							show.push(m.createParent);
 							showSep4 = true;
 						}
-						else {
-							hide.push(m.createParent);
-						}
 					}
 					
 					// Attachment rename option
@@ -2277,16 +2297,6 @@ var ZoteroPane = new function()
 						show.push(m.renameAttachments);
 						showSep4 = true;
 					}
-					else {
-						hide.push(m.renameAttachments);
-					}
-					
-					if (showSep4) {
-						show.push(m.sep4);
-					}
-					else {
-						hide.push(m.sep4);
-					}
 					
 					// If not linked URL, show reindex line
 					if (Zotero.Fulltext.pdfConverterIsRegistered()
@@ -2294,20 +2304,13 @@ var ZoteroPane = new function()
 						show.push(m.reindexItem);
 						showSep4 = true;
 					}
-					else {
-						hide.push(m.reindexItem);
+					
+					if (showSep4) {
+						show.push(m.sep4);
 					}
 				}
 				else {
-					if (item.isNote() && item.isTopLevelItem()) {
-						show.push(m.sep4, m.createParent);
-					}
-					else {
-						hide.push(m.sep4, m.createParent);
-					}
-					
 					show.push(m.duplicateItem);
-					hide.push(m.recognizePDF, m.renameAttachments, m.reindexItem);
 				}
 				
 				// Update attachment submenu
@@ -2319,10 +2322,6 @@ var ZoteroPane = new function()
 					var d = [m.deleteFromLibrary, m.createParent, m.renameAttachments];
 					for each(var val in d) {
 						disable.push(val);
-						var index = enable.indexOf(val);
-						if (index != -1) {
-							enable.splice(index, 1);
-						}
 					}
 				}
 			}
@@ -2334,14 +2333,9 @@ var ZoteroPane = new function()
 			if (!itemGroup.isLibrary()) {
 				show.push(m.showInLibrary, m.sep1);
 			}
-			else {
-				hide.push(m.showInLibrary, m.sep1);
-			}
 			
 			disable.push(m.showInLibrary, m.duplicateItem, m.deleteItem,
 				m.deleteFromLibrary, m.exportItems, m.createBib, m.loadReport);
-			hide.push(m.addNote, m.addAttachments, m.sep2, m.sep4, m.reindexItem,
-				m.createParent, m.recognizePDF, m.renameAttachments);
 		}
 		
 		// TODO: implement menu for remote items
@@ -2358,23 +2352,15 @@ var ZoteroPane = new function()
 					}
 				}
 				disable.push(m[i]);
-				var index = enable.indexOf(m[i]);
-				if (index != -1) {
-					enable.splice(index, 1);
-				}
 			}
 		}
 		
 		// Remove from collection
-		if (this.itemsView._itemGroup.isCollection() && !(item && item.getSource()))
+		if (itemGroup.isCollection() && !(item && item.getSource()))
 		{
 			menu.childNodes[m.deleteItem].setAttribute('label', Zotero.getString('pane.items.menu.remove' + multiple));
 			show.push(m.deleteItem);
 		}
-		else
-		{
-			hide.push(m.deleteItem);
-		}
 		
 		// Plural if necessary
 		menu.childNodes[m.deleteFromLibrary].setAttribute('label', Zotero.getString('pane.items.menu.erase' + multiple));
@@ -2386,21 +2372,17 @@ var ZoteroPane = new function()
 		menu.childNodes[m.renameAttachments].setAttribute('label', Zotero.getString('pane.items.menu.renameAttachments' + multiple));
 		menu.childNodes[m.reindexItem].setAttribute('label', Zotero.getString('pane.items.menu.reindexItem' + multiple));
 		
+		// Hide and enable all actions by default (so if they're shown they're enabled)
+		for each(var pos in m) {
+			menu.childNodes[pos].setAttribute('hidden', true);
+			menu.childNodes[pos].setAttribute('disabled', false);
+		}
+		
 		for (var i in disable)
 		{
 			menu.childNodes[disable[i]].setAttribute('disabled', true);
 		}
 		
-		for (var i in enable)
-		{
-			menu.childNodes[enable[i]].setAttribute('disabled', false);
-		}
-		
-		for (var i in hide)
-		{
-			menu.childNodes[hide[i]].setAttribute('hidden', true);
-		}
-		
 		for (var i in show)
 		{
 			menu.childNodes[show[i]].setAttribute('hidden', false);
@@ -2411,19 +2393,83 @@ var ZoteroPane = new function()
 	}
 	
 	
+	this.onTreeMouseDown = function (event) {
+		var itemGroup = ZoteroPane_Local.getItemGroup();
+		
+		// Automatically select all equivalent items when clicking on an item
+		// in duplicates view
+		if (itemGroup.isDuplicates()) {
+			// Trigger only on primary-button single clicks with modifiers
+			// (so that items can still be selected and deselected manually)
+			if (!event || event.detail != 1 || event.button != 0 || event.metaKey || event.shiftKey) {
+				return;
+			}
+			
+			var t = event.originalTarget;
+			
+			if (t.localName != 'treechildren') {
+				return;
+			}
+			
+			var tree = t.parentNode;
+			
+			var row = {}, col = {}, obj = {};
+			tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, obj);
+			
+			// obj.value == 'cell'/'text'/'image'
+			if (!obj.value) {
+				return;
+			}
+			
+			// Duplicated in itemTreeView.js::notify()
+			var itemID = ZoteroPane_Local.itemsView._getItemAtRow(row.value).ref.id;
+			var setItemIDs = itemGroup.ref.getSetItemsByItemID(itemID);
+			ZoteroPane_Local.itemsView.selectItems(setItemIDs);
+			
+			// Prevent the tree's select event from being called here,
+			// since it's triggered by the multi-select
+			event.stopPropagation();
+		}
+	}
+	
+	
 	// Adapted from: http://www.xulplanet.com/references/elemref/ref_tree.html#cmnote-9
 	this.onTreeClick = function (event) {
-		// We only care about primary button double and triple clicks
-		if (!event || (event.detail != 2 && event.detail != 3) || event.button != 0) {
-			return;
-		}
-		
 		var t = event.originalTarget;
 		
 		if (t.localName != 'treechildren') {
 			return;
 		}
 		
+		// We care only about primary-button double and triple clicks
+		if (!event || (event.detail != 2 && event.detail != 3) || event.button != 0) {
+			// The Mozilla tree binding fires select() in mousedown(),
+			// but if when it gets to click() the selection differs from
+			// what it expects (say, because multiple items had been
+			// selected during mousedown()), it fires select() again.
+			// We prevent that here.
+			var itemGroup = ZoteroPane_Local.getItemGroup();
+			
+			if (itemGroup.isDuplicates()) {
+				if (event.metaKey || event.shiftKey) {
+					return;
+				}
+				event.stopPropagation();
+				event.preventDefault();
+			}
+			
+			return;
+		}
+		
+		var itemGroup = ZoteroPane_Local.getItemGroup();
+		
+		// Ignore all double-clicks in duplicates view
+		if (itemGroup.isDuplicates()) {
+			event.stopPropagation();
+			event.preventDefault();
+			return;
+		}
+		
 		var tree = t.parentNode;
 		
 		var row = {}, col = {}, obj = {};
@@ -2440,7 +2486,6 @@ var ZoteroPane = new function()
 				return;
 			}
 			
-			var itemGroup = ZoteroPane_Local.collectionsView._getItemAtRow(tree.view.selection.currentIndex);
 			if (itemGroup.isLibrary()) {
 				var uri = Zotero.URI.getCurrentUserLibraryURI();
 				if (uri) {
@@ -2451,10 +2496,6 @@ var ZoteroPane = new function()
 			}
 			
 			if (itemGroup.isSearch()) {
-				// Don't do anything on double-click of Unfiled Items
-				if ((itemGroup.ref.id + "").match(/^8634533000/)) { // 'UNFILED000'
-					return;
-				}
 				ZoteroPane_Local.editSelectedCollection();
 				return;
 			}
@@ -2466,6 +2507,11 @@ var ZoteroPane = new function()
 				return;
 			}
 			
+			// Ignore double-clicks on Unfiled Items source row
+			if (itemGroup.isUnfiled()) {
+				return;
+			}
+			
 			if (itemGroup.isHeader()) {
 				if (itemGroup.ref.id == 'group-libraries-header') {
 					var uri = Zotero.URI.getGroupsURL();
@@ -2498,11 +2544,6 @@ var ZoteroPane = new function()
 				var item = ZoteroPane_Local.getSelectedItems()[0];
 				if (item) {
 					if (item.isRegularItem()) {
-						// Double-click on Commons item should load IA page
-						var itemGroup = ZoteroPane_Local.collectionsView._getItemAtRow(
-							ZoteroPane_Local.collectionsView.selection.currentIndex
-						);
-						
 						if (itemGroup.isBucket()) {
 							var uri = itemGroup.ref.getItemURI(item);
 							ZoteroPane_Local.loadURI(uri);
@@ -2669,6 +2710,14 @@ var ZoteroPane = new function()
 	}
 	
 	
+	this.setItemPaneMessage = function (msg) {
+		document.getElementById('zotero-item-pane-content').selectedIndex = 0;
+		
+		var label = document.getElementById('zotero-item-pane-message');
+		label.value = msg;
+	}
+	
+	
 	// Updates browser context menu options
 	function contextPopupShowing()
 	{
diff --git a/chrome/content/zotero/zoteroPane.xul b/chrome/content/zotero/zoteroPane.xul
index 0c28ac25d..d38a7eb22 100644
--- a/chrome/content/zotero/zoteroPane.xul
+++ b/chrome/content/zotero/zoteroPane.xul
@@ -110,8 +110,6 @@
 								label="Search for Shared Libraries" oncommand="Zotero.Zeroconf.findInstances()"/>
 							<menuseparator id="zotero-tb-actions-plugins-separator"/>
 							<menuitem id="zotero-tb-actions-timeline" label="&zotero.toolbar.timeline.label;" command="cmd_zotero_createTimeline"/>
-							<!-- TODO: localize <menuitem id="zotero-tb-actions-duplicate" label="&zotero.toolbar.duplicate.label;" oncommand="ZoteroPane_Local.showDuplicates()"/>-->
-							<menuitem id="zotero-tb-actions-showDuplicates" label="Show Duplicates" oncommand="ZoteroPane_Local.showDuplicates()" hidden="true"/>
 							<menuseparator hidden="true" id="zotero-tb-actions-sync-separator"/>
 							<menuitem hidden="true" label="WebDAV Sync Debugging" disabled="true"/>
 							<menuitem hidden="true" label="  Purge Deleted Storage Files" oncommand="Zotero.Sync.Storage.purgeDeletedStorageFiles('webdav', function(results) { Zotero.debug(results); })"/>
@@ -153,8 +151,9 @@
 							<menuitem class="menuitem-iconic zotero-menuitem-attachments-snapshot" label="&zotero.items.menu.attach.snapshot;" oncommand="var itemID = ZoteroPane_Local.getSelectedItems()[0].id; ZoteroPane_Local.addAttachmentFromPage(false, itemID)"/>
 							<menuitem class="menuitem-iconic zotero-menuitem-attachments-web-link" label="&zotero.items.menu.attach.link;" oncommand="var itemID = ZoteroPane_Local.getSelectedItems()[0].id; ZoteroPane_Local.addAttachmentFromPage(true, itemID)"/>
 							<menuitem class="menuitem-iconic zotero-menuitem-attachments-web-link" label="&zotero.items.menu.attach.link.uri;" oncommand="var itemID = ZoteroPane_Local.getSelectedItems()[0].id; ZoteroPane_Local.addAttachmentFromURI(true, itemID);"/>
-							<menuitem class="menuitem-iconic zotero-menuitem-attachments-file" label="Attach Stored Copy of File..." oncommand="var itemID = ZoteroPane_Local.getSelectedItems()[0].id; ZoteroPane_Local.addAttachmentFromDialog(false, itemID);"/>
-							<menuitem class="menuitem-iconic zotero-menuitem-attachments-link" label="Attach Link to File..." oncommand="var itemID = ZoteroPane_Local.getSelectedItems()[0].id; ZoteroPane_Local.addAttachmentFromDialog(true, itemID);"/>
+							<!-- TODO: localize -->
+							<menuitem class="menuitem-iconic zotero-menuitem-attachments-file" label="Attach Stored Copy of File…" oncommand="var itemID = ZoteroPane_Local.getSelectedItems()[0].id; ZoteroPane_Local.addAttachmentFromDialog(false, itemID);"/>
+							<menuitem class="menuitem-iconic zotero-menuitem-attachments-link" label="Attach Link to File…" oncommand="var itemID = ZoteroPane_Local.getSelectedItems()[0].id; ZoteroPane_Local.addAttachmentFromDialog(true, itemID);"/>
 						</menupopup>
 					</toolbarbutton>
 					<toolbarseparator/>
@@ -231,7 +230,8 @@
 					<menuitem label="&zotero.toolbar.newSavedSearch.label;" command="cmd_zotero_newSavedSearch"/>
 					<menuitem label="&zotero.toolbar.newSubcollection.label;" oncommand="ZoteroPane_Local.newCollection(ZoteroPane_Local.getSelectedCollection().id)"/>
 					<menuseparator/>
-					<menuitem label="&zotero.collections.showUnfiledItems;" oncommand="ZoteroPane_Local.setUnfiled(ZoteroPane_Local.getSelectedLibraryID(), true)"/>
+					<menuitem label="&zotero.toolbar.duplicate.label;" oncommand="ZoteroPane_Local.setVirtual(ZoteroPane_Local.getSelectedLibraryID(), 'duplicates', true)"/>
+					<menuitem label="&zotero.collections.showUnfiledItems;" oncommand="ZoteroPane_Local.setVirtual(ZoteroPane_Local.getSelectedLibraryID(), 'unfiled', true)"/>
 					<menuitem oncommand="ZoteroPane_Local.editSelectedCollection();"/>
 					<menuitem oncommand="ZoteroPane_Local.deleteSelectedCollection();"/>
 					<menuseparator/>
@@ -261,6 +261,8 @@
 					<menuitem label="&zotero.items.menu.duplicateItem;" oncommand="ZoteroPane_Local.duplicateSelectedItem();"/>
 					<menuitem oncommand="ZoteroPane_Local.deleteSelectedItems();"/>
 					<menuitem oncommand="ZoteroPane_Local.deleteSelectedItems(true);"/>
+					<!-- TODO: localize -->
+					<menuitem oncommand="ZoteroPane_Local.mergeSelectedItems();" label="Merge Items…"/>
 					<menuseparator/>
 					<menuitem oncommand="Zotero_File_Interface.exportItems();"/>
 					<menuitem oncommand="Zotero_File_Interface.bibliographyFromItems();"/>
@@ -316,7 +318,7 @@
 							enableColumnDrag="true"
 							onfocus="if (ZoteroPane_Local.itemsView.rowCount &amp;&amp; !ZoteroPane_Local.itemsView.selection.count) { ZoteroPane_Local.itemsView.selection.select(0); }"
 							onkeypress="ZoteroPane_Local.handleKeyPress(event, this.id)"
-							onselect="ZoteroPane_Local.itemSelected();"
+							onselect="ZoteroPane_Local.itemSelected(event)"
 							ondragstart="if (event.target.localName == 'treechildren') { ZoteroPane_Local.itemsView.onDragStart(event); }"
 							ondragenter="return ZoteroPane_Local.itemsView.onDragEnter(event)"
 							ondragover="return ZoteroPane_Local.itemsView.onDragOver(event)"
@@ -417,37 +419,8 @@
 					onmousemove="ZoteroPane_Local.updateToolbarPosition()"
 					oncommand="ZoteroPane_Local.updateToolbarPosition()"/>
 				
-				<vbox id="zotero-item-pane" zotero-persist="width">
-					<!-- TODO: localize -->
-					<button id="zotero-item-restore-button" label="Restore to Library"
-						oncommand="ZoteroPane_Local.restoreSelectedItems()" hidden="true"/>
-					<!-- TODO: localize -->
-					<button id="zotero-item-show-original" label="Show Original"
-						oncommand="ZoteroPane_Local.showOriginalItem()" hidden="true"/>
-					<deck id="zotero-item-pane-content" selectedIndex="0" flex="1">
-						<groupbox pack="center" align="center">
-							<label id="zotero-view-selected-label"/>
-						</groupbox>
-						<tabbox id="zotero-view-tabbox" flex="1" onselect="if (!ZoteroPane_Local.collectionsView.selection || event.originalTarget.localName != 'tabpanels') { return; }; ZoteroItemPane.viewItem(ZoteroPane_Local.getSelectedItems()[0], ZoteroPane_Local.collectionsView.editable ? 'edit' : 'view', this.selectedIndex)">
-							<tabs>
-								<tab label="&zotero.tabs.info.label;"/>
-								<tab label="&zotero.tabs.notes.label;"/>
-								<tab label="&zotero.tabs.tags.label;"/>
-								<tab label="&zotero.tabs.related.label;"/>
-							</tabs>
-							<tabpanels id="zotero-view-item" flex="1"/>
-						</tabbox>
-						<!-- Note info pane -->
-						<groupbox id="zotero-view-note" flex="1">
-							<zoteronoteeditor id="zotero-note-editor" flex="1" notitle="1"/>
-							<button id="zotero-view-note-button" label="&zotero.notes.separate;" oncommand="ZoteroPane_Local.openNoteWindow(this.getAttribute('noteID')); if(this.hasAttribute('sourceID')) ZoteroPane_Local.selectItem(this.getAttribute('sourceID'));"/>
-						</groupbox>
-						<!-- Attachment info pane -->
-						<groupbox flex="1">
-							<zoteroattachmentbox id="zotero-attachment-box" flex="1"/>
-						</groupbox>
-					</deck>
-				</vbox>
+				<!-- itemPane.xul -->
+				<vbox id="zotero-item-pane"/>
 			</hbox>
 		</vbox>
 		
diff --git a/chrome/locale/en-US/zotero/zotero.dtd b/chrome/locale/en-US/zotero/zotero.dtd
index fc366b0f5..8528c1fd5 100644
--- a/chrome/locale/en-US/zotero/zotero.dtd
+++ b/chrome/locale/en-US/zotero/zotero.dtd
@@ -37,6 +37,7 @@
 <!ENTITY zotero.tabs.related.label						"Related">
 <!ENTITY zotero.notes.separate							"Edit in a separate window">
 
+<!ENTITY zotero.toolbar.duplicate.label				"Show Duplicates">
 <!ENTITY zotero.collections.showUnfiledItems			"Show Unfiled Items">
 
 <!ENTITY zotero.items.itemType							"Item Type">
@@ -85,7 +86,6 @@
 <!ENTITY zotero.toolbar.export.label					"Export Library…">
 <!ENTITY zotero.toolbar.rtfScan.label					"RTF Scan…">
 <!ENTITY zotero.toolbar.timeline.label				"Create Timeline">
-<!ENTITY zotero.toolbar.duplicate.label				"Show Duplicates">
 <!ENTITY zotero.toolbar.preferences.label				"Preferences…">
 <!ENTITY zotero.toolbar.supportAndDocumentation		"Support and Documentation">
 <!ENTITY zotero.toolbar.about.label					"About Zotero">
diff --git a/chrome/skin/default/zotero/bindings/itembox.css b/chrome/skin/default/zotero/bindings/itembox.css
index e25d355cf..688b00824 100644
--- a/chrome/skin/default/zotero/bindings/itembox.css
+++ b/chrome/skin/default/zotero/bindings/itembox.css
@@ -148,4 +148,10 @@ hbox.zotero-date-field-status > label
 	background-position: center !important;
 	border-width: 0 !important;
 	-moz-border-radius: 4px !important;
+}
+
+/* Merge pane in duplicates view */
+.zotero-field-version-button {
+	margin: 0;
+	padding: 0;
 }
\ No newline at end of file
diff --git a/chrome/skin/default/zotero/itemPane.css b/chrome/skin/default/zotero/itemPane.css
index 87f4f391a..cd22c282d 100644
--- a/chrome/skin/default/zotero/itemPane.css
+++ b/chrome/skin/default/zotero/itemPane.css
@@ -1,4 +1,54 @@
+#zotero-item-pane-message {
+	padding: 0 2em;
+}
+
+#zotero-view-tabbox, #zotero-item-pane-content > groupbox, #zotero-item-pane-content > groupbox > .groupbox-body
+{
+	margin: 0 !important;
+	padding: 0 !important;
+}
+
+#zotero-view-tabbox tabs tab
+{
+	margin-top: 0 !important;
+	margin-bottom: 0 !important;
+}
+
+#zotero-view-tabbox tabs tab .tab-text
+{
+	margin-top: .2em !important;
+	margin-bottom: .25em !important;
+}
+
+#zotero-view-item
+{
+	padding: 1.5em .25em .25em;
+}
+
 #zotero-view-item > tabpanel > *
 {
 	overflow: auto;
 }
+
+#zotero-view-item > vbox
+{
+	overflow: auto;
+	margin-left: 5px;
+}
+
+
+/* Merge pane in duplicates view */
+#zotero-duplicates-merge-button
+{
+	font-size: 13px;
+}
+
+#zotero-duplicates-merge-pane > groupbox
+{
+	margin: 0;
+}
+
+#zotero-duplicates-merge-item-box row
+{
+	min-height: 20px;
+}
diff --git a/chrome/skin/default/zotero/overlay.css b/chrome/skin/default/zotero/overlay.css
index 9e748467c..6b0a3d7d9 100644
--- a/chrome/skin/default/zotero/overlay.css
+++ b/chrome/skin/default/zotero/overlay.css
@@ -429,35 +429,6 @@
 	cursor: default;
 }
 
-#zotero-view-tabbox, #zotero-item-pane-content > groupbox, #zotero-item-pane-content > groupbox > .groupbox-body
-{
-	margin: 0 !important;
-	padding: 0 !important;
-}
-
-#zotero-view-tabbox tabs tab
-{
-	margin-top: 0 !important;
-	margin-bottom: 0 !important;
-}
-
-#zotero-view-tabbox tabs tab .tab-text
-{
-	margin-top: .2em !important;
-	margin-bottom: .25em !important;
-}
-
-#zotero-view-item
-{
-	padding: 1.5em .25em .25em;
-}
-
-#zotero-view-item > vbox
-{
-	overflow: auto;
-	margin-left: 5px;
-}
-
 #zotero-splitter
 {
 	border-top: none;
diff --git a/chrome/skin/default/zotero/treesource-duplicates.png b/chrome/skin/default/zotero/treesource-duplicates.png
new file mode 100644
index 000000000..ca779f323
Binary files /dev/null and b/chrome/skin/default/zotero/treesource-duplicates.png differ
diff --git a/chrome/skin/default/zotero/treesource-search-virtual.png b/chrome/skin/default/zotero/treesource-search-virtual.png
deleted file mode 100644
index 44084add7..000000000
Binary files a/chrome/skin/default/zotero/treesource-search-virtual.png and /dev/null differ
diff --git a/components/zotero-service.js b/components/zotero-service.js
index d47b08629..1f8631ca6 100644
--- a/components/zotero-service.js
+++ b/components/zotero-service.js
@@ -76,7 +76,7 @@ const xpcomFilesLocal = [
 	'data/tags',
 	'date',
 	'db',
-	'duplicate',
+	'duplicates',
 	'enstyle',
 	'fulltext',
 	'id',
diff --git a/system.sql b/system.sql
index 623ffeff9..d7560b176 100644
--- a/system.sql
+++ b/system.sql
@@ -1359,4 +1359,4 @@ INSERT INTO "syncObjectTypes" VALUES(2, 'creator');
 INSERT INTO "syncObjectTypes" VALUES(3, 'item');
 INSERT INTO "syncObjectTypes" VALUES(4, 'search');
 INSERT INTO "syncObjectTypes" VALUES(5, 'tag');
-INSERT INTO "syncObjectTypes" VALUES(6, 'relations');
+INSERT INTO "syncObjectTypes" VALUES(6, 'relation');