diff --git a/include/class.format.php b/include/class.format.php
index ac9f94c3b42a9e5f1af87d196d0a2e24ac4fdacb..861f8796dd0768948eb14f321e810b9a69b0ae8f 100644
--- a/include/class.format.php
+++ b/include/class.format.php
@@ -353,6 +353,7 @@ class Format {
                             .'&auth='.$token;
                     // ALL link targets open in a new tab
                     $a['target'] = '_blank';
+                    $a['class'] = 'no-pjax';
                 }
                 // Images which are external are rewritten to <div
                 // data-src='url...'/>
diff --git a/include/class.osticket.php b/include/class.osticket.php
index abd563839d6be9910ae4446f4aaf71694b1dc96b..ce5d6354498dbb022ec66213f756eb49ffb1c69b 100644
--- a/include/class.osticket.php
+++ b/include/class.osticket.php
@@ -42,6 +42,7 @@ class osTicket {
 
     var $title; //Custom title. html > head > title.
     var $headers;
+    var $pjax_extra;
 
     var $config;
     var $session;
@@ -163,13 +164,17 @@ class osTicket {
         return $replacer->replaceVars($input);
     }
 
-    function addExtraHeader($header) {
+    function addExtraHeader($header, $pjax_script=false) {
         $this->headers[md5($header)] = $header;
+        $this->pjax_extra[md5($header)] = $pjax_script;
     }
 
     function getExtraHeaders() {
         return $this->headers;
     }
+    function getExtraPjax() {
+        return $this->pjax_extra;
+    }
 
     function setPageTitle($title) {
         $this->title = $title;
diff --git a/include/class.thread.php b/include/class.thread.php
index 5aed962fd07bf8f2197244825d719a9969b31ec4..05d8f67b45e17a05112193c3b42a5a918ad13579 100644
--- a/include/class.thread.php
+++ b/include/class.thread.php
@@ -602,7 +602,7 @@ Class ThreadEntry {
             if($attachment['size'])
                 $size=sprintf('<em>(%s)</em>', Format::file_size($attachment['size']));
 
-            $str.=sprintf('<a class="Icon file" href="%s?id=%d&h=%s" target="%s">%s</a>%s&nbsp;%s',
+            $str.=sprintf('<a class="Icon file no-pjax" href="%s?id=%d&h=%s" target="%s">%s</a>%s&nbsp;%s',
                     $file, $attachment['attach_id'], $hash, $target, Format::htmlchars($attachment['name']), $size, $separator);
         }
 
diff --git a/include/staff/footer.inc.php b/include/staff/footer.inc.php
index 5cb94ea3d817be39355871fb783369dedd009cb9..d112e157a52b98d0832a68f69f77e24751ae1f5f 100644
--- a/include/staff/footer.inc.php
+++ b/include/staff/footer.inc.php
@@ -1,3 +1,4 @@
+<?php if (!isset($_SERVER['HTTP_X_PJAX'])) { ?>
     </div>
     <div id="footer">
         Copyright &copy; 2006-<?php echo date('Y'); ?>&nbsp;<?php echo (string) $ost->company ?: 'osTicket.com'; ?>&nbsp;All Rights Reserved.
@@ -14,11 +15,21 @@ if(is_object($thisstaff) && $thisstaff->isStaff()) { ?>
 </div>
 <div id="overlay"></div>
 <div id="loading">
-    <h4>Please Wait!</h4>
-    <p>Please wait... it will take a second!</p>
+    <i class="icon-spinner icon-spin icon-3x pull-left icon-light"></i>
+    <h1>Loading ...</h1>
 </div>
 <div class="dialog" style="display:none;width:650px;" id="popup">
     <div class="body"></div>
 </div>
+<script type="text/javascript">
+if ($.support.pjax) {
+  $(document).on('click', 'a', function(event) {
+    if (!$(this).hasClass('no-pjax'))
+        $.pjax.click(event, {container: $('#content'), timeout: 2000})
+  })
+}
+</script>
 </body>
 </html>
+<?php } # endif X_PJAX ?>
+
diff --git a/include/staff/header.inc.php b/include/staff/header.inc.php
index d2a3e43b82277bb13eb38718e06bfd84340457f9..2b0870d27388842c36b1d4751ef7451747cc35c7 100644
--- a/include/staff/header.inc.php
+++ b/include/staff/header.inc.php
@@ -1,3 +1,4 @@
+<?php if (!isset($_SERVER['HTTP_X_PJAX'])) { ?>
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
 <html>
 <head>
@@ -13,6 +14,7 @@
     <![endif]-->
     <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-1.8.3.min.js"></script>
     <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-ui-1.10.3.custom.min.js"></script>
+    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery.pjax.js"></script>
     <script type="text/javascript" src="../js/jquery.multifile.js"></script>
     <script type="text/javascript" src="./js/tips.js"></script>
     <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor.min.js"></script>
@@ -53,61 +55,26 @@
         <p id="info">Welcome, <strong><?php echo $thisstaff->getFirstName(); ?></strong>
            <?php
             if($thisstaff->isAdmin() && !defined('ADMINPAGE')) { ?>
-            | <a href="admin.php">Admin Panel</a>
+            | <a href="admin.php" class="no-pjax">Admin Panel</a>
             <?php }else{ ?>
-            | <a href="index.php">Staff Panel</a>
+            | <a href="index.php" class="no-pjax">Staff Panel</a>
             <?php } ?>
             | <a href="profile.php">My Preferences</a>
-            | <a href="logout.php?auth=<?php echo $ost->getLinkToken(); ?>">Log Out</a>
+            | <a href="logout.php?auth=<?php echo $ost->getLinkToken(); ?>" class="no-pjax">Log Out</a>
         </p>
     </div>
     <ul id="nav">
-        <?php
-        if(($tabs=$nav->getTabs()) && is_array($tabs)){
-            foreach($tabs as $name =>$tab) {
-                echo sprintf('<li class="%s"><a href="%s">%s</a>',$tab['active']?'active':'inactive',$tab['href'],$tab['desc']);
-                if(!$tab['active'] && ($subnav=$nav->getSubMenu($name))){
-                    echo "<ul>\n";
-                    foreach($subnav as $k => $item) {
-                        if (!($id=$item['id']))
-                            $id="nav$k";
-
-                        echo sprintf('<li><a class="%s" href="%s" title="%s" id="%s">%s</a></li>',
-                                $item['iconclass'], $item['href'], $item['title'], $id, $item['desc']);
-                    }
-                    echo "\n</ul>\n";
-                }
-                echo "\n</li>\n";
-            }
-        } ?>
+<?php include STAFFINC_DIR . "templates/navigation.tmpl.php"; ?>
     </ul>
     <ul id="sub_nav">
-        <?php
-        if(($subnav=$nav->getSubMenu()) && is_array($subnav)){
-            $activeMenu=$nav->getActiveMenu();
-            if($activeMenu>0 && !isset($subnav[$activeMenu-1]))
-                $activeMenu=0;
-            foreach($subnav as $k=> $item) {
-                if($item['droponly']) continue;
-                $class=$item['iconclass'];
-                if ($activeMenu && $k+1==$activeMenu
-                        or (!$activeMenu
-                            && (strpos(strtoupper($item['href']),strtoupper(basename($_SERVER['SCRIPT_NAME']))) !== false
-                                or ($item['urls']
-                                    && in_array(basename($_SERVER['SCRIPT_NAME']),$item['urls'])
-                                    )
-                                )))
-                    $class="$class active";
-                if (!($id=$item['id']))
-                    $id="subnav$k";
-
-                echo sprintf('<li><a class="%s" href="%s" title="%s" id="%s">%s</a></li>',
-                        $class, $item['href'], $item['title'], $id, $item['desc']);
-            }
-        }
-        ?>
+<?php include STAFFINC_DIR . "templates/sub-navigation.tmpl.php"; ?>
     </ul>
     <div id="content">
+<?php } elseif ($pjax = $ost->getExtraPjax()) { # endif X_PJAX ?>
+    <script type="text/javascript">
+    <?php foreach (array_filter($pjax) as $s) echo $s.";"; ?>
+    </script>
+<?php } # endif X_PJAX ?>
         <?php if($errors['err']) { ?>
             <div id="msg_error"><?php echo $errors['err']; ?></div>
         <?php }elseif($msg) { ?>
diff --git a/include/staff/templates/navigation.tmpl.php b/include/staff/templates/navigation.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..66f5564c122abd62710171e84e38d9c3e86aa536
--- /dev/null
+++ b/include/staff/templates/navigation.tmpl.php
@@ -0,0 +1,22 @@
+<?php
+if(($tabs=$nav->getTabs()) && is_array($tabs)){
+    foreach($tabs as $name =>$tab) {
+        echo sprintf('<li class="%s"><a href="%s" class="no-pjax">%s</a>',$tab['active']?'active':'inactive',$tab['href'],$tab['desc']);
+        if(!$tab['active'] && ($subnav=$nav->getSubMenu($name))){
+            echo "<ul>\n";
+            foreach($subnav as $k => $item) {
+                if (!($id=$item['id']))
+                    $id="nav$k";
+
+                echo sprintf(
+                    '<li><a class="%s %s" href="%s" title="%s" id="%s">%s</a></li>',
+                    $item['iconclass'],
+                    $tab['active'] ? '' : 'no-pjax',
+                    $item['href'], $item['title'],
+                    $id, $item['desc']);
+            }
+            echo "\n</ul>\n";
+        }
+        echo "\n</li>\n";
+    }
+} ?>
diff --git a/include/staff/templates/sub-navigation.tmpl.php b/include/staff/templates/sub-navigation.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..ebc6fa20440fefaeb9705814ae50c1c74d311817
--- /dev/null
+++ b/include/staff/templates/sub-navigation.tmpl.php
@@ -0,0 +1,25 @@
+<?php
+if(($subnav=$nav->getSubMenu()) && is_array($subnav)){
+    $activeMenu=$nav->getActiveMenu();
+    if($activeMenu>0 && !isset($subnav[$activeMenu-1]))
+        $activeMenu=0;
+    foreach($subnav as $k=> $item) {
+        if($item['droponly']) continue;
+        $class=$item['iconclass'];
+        if ($activeMenu && $k+1==$activeMenu
+                or (!$activeMenu
+                    && (strpos(strtoupper($item['href']),strtoupper(basename($_SERVER['SCRIPT_NAME']))) !== false
+                        or ($item['urls']
+                            && in_array(basename($_SERVER['SCRIPT_NAME']),$item['urls'])
+                            )
+                        )))
+            $class="$class active";
+        if (!($id=$item['id']))
+            $id="subnav$k";
+
+        echo sprintf('<li><a class="%s" href="%s" title="%s" id="%s">%s</a></li>',
+                $class, $item['href'], $item['title'], $id, $item['desc']);
+    }
+}
+?>
+
diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php
index 621365fc4cd85b0c06cab4128c9f10ea1331affe..d155e040cfb7446f5c546cedc5a3cdee77b58e55 100644
--- a/include/staff/ticket-view.inc.php
+++ b/include/staff/ticket-view.inc.php
@@ -982,7 +982,6 @@ $tcount+= $ticket->getNumNotes();
     </form>
     <div class="clear"></div>
 </div>
-<script type="text/javascript" src="js/ticket.js"></script>
 <script type="text/javascript">
 $(function() {
     $(document).on('click', 'a.change-user', function(e) {
diff --git a/js/jquery.pjax.js b/js/jquery.pjax.js
new file mode 100644
index 0000000000000000000000000000000000000000..e2f958716ed658d8a1e6ee0563d08cf7a7820b8c
--- /dev/null
+++ b/js/jquery.pjax.js
@@ -0,0 +1,839 @@
+// jquery.pjax.js
+// copyright chris wanstrath
+// https://github.com/defunkt/jquery-pjax
+
+(function($){
+
+// When called on a container with a selector, fetches the href with
+// ajax into the container or with the data-pjax attribute on the link
+// itself.
+//
+// Tries to make sure the back button and ctrl+click work the way
+// you'd expect.
+//
+// Exported as $.fn.pjax
+//
+// Accepts a jQuery ajax options object that may include these
+// pjax specific options:
+//
+//
+// container - Where to stick the response body. Usually a String selector.
+//             $(container).html(xhr.responseBody)
+//             (default: current jquery context)
+//      push - Whether to pushState the URL. Defaults to true (of course).
+//   replace - Want to use replaceState instead? That's cool.
+//
+// For convenience the second parameter can be either the container or
+// the options object.
+//
+// Returns the jQuery object
+function fnPjax(selector, container, options) {
+  var context = this
+  return this.on('click.pjax', selector, function(event) {
+    var opts = $.extend({}, optionsFor(container, options))
+    if (!opts.container)
+      opts.container = $(this).attr('data-pjax') || context
+    handleClick(event, opts)
+  })
+}
+
+// Public: pjax on click handler
+//
+// Exported as $.pjax.click.
+//
+// event   - "click" jQuery.Event
+// options - pjax options
+//
+// Examples
+//
+//   $(document).on('click', 'a', $.pjax.click)
+//   // is the same as
+//   $(document).pjax('a')
+//
+//  $(document).on('click', 'a', function(event) {
+//    var container = $(this).closest('[data-pjax-container]')
+//    $.pjax.click(event, container)
+//  })
+//
+// Returns nothing.
+function handleClick(event, container, options) {
+  options = optionsFor(container, options)
+
+  var link = event.currentTarget
+
+  if (link.tagName.toUpperCase() !== 'A')
+    throw "$.fn.pjax or $.pjax.click requires an anchor element"
+
+  // Middle click, cmd click, and ctrl click should open
+  // links in a new tab as normal.
+  if ( event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey )
+    return
+
+  // Ignore cross origin links
+  if ( location.protocol !== link.protocol || location.hostname !== link.hostname )
+    return
+
+  // Ignore anchors on the same page
+  if (link.hash && link.href.replace(link.hash, '') ===
+       location.href.replace(location.hash, ''))
+    return
+
+  // Ignore empty anchor "foo.html#"
+  if (link.href === location.href + '#')
+    return
+
+  var defaults = {
+    url: link.href,
+    container: $(link).attr('data-pjax'),
+    target: link
+  }
+
+  var opts = $.extend({}, defaults, options)
+  var clickEvent = $.Event('pjax:click')
+  $(link).trigger(clickEvent, [opts])
+
+  if (!clickEvent.isDefaultPrevented()) {
+    pjax(opts)
+    event.preventDefault()
+    $(link).trigger('pjax:clicked', [opts])
+  }
+}
+
+// Public: pjax on form submit handler
+//
+// Exported as $.pjax.submit
+//
+// event   - "click" jQuery.Event
+// options - pjax options
+//
+// Examples
+//
+//  $(document).on('submit', 'form', function(event) {
+//    var container = $(this).closest('[data-pjax-container]')
+//    $.pjax.submit(event, container)
+//  })
+//
+// Returns nothing.
+function handleSubmit(event, container, options) {
+  options = optionsFor(container, options)
+
+  var form = event.currentTarget
+
+  if (form.tagName.toUpperCase() !== 'FORM')
+    throw "$.pjax.submit requires a form element"
+
+  var defaults = {
+    type: form.method.toUpperCase(),
+    url: form.action,
+    data: $(form).serializeArray(),
+    container: $(form).attr('data-pjax'),
+    target: form
+  }
+
+  pjax($.extend({}, defaults, options))
+
+  event.preventDefault()
+}
+
+// Loads a URL with ajax, puts the response body inside a container,
+// then pushState()'s the loaded URL.
+//
+// Works just like $.ajax in that it accepts a jQuery ajax
+// settings object (with keys like url, type, data, etc).
+//
+// Accepts these extra keys:
+//
+// container - Where to stick the response body.
+//             $(container).html(xhr.responseBody)
+//      push - Whether to pushState the URL. Defaults to true (of course).
+//   replace - Want to use replaceState instead? That's cool.
+//
+// Use it just like $.ajax:
+//
+//   var xhr = $.pjax({ url: this.href, container: '#main' })
+//   console.log( xhr.readyState )
+//
+// Returns whatever $.ajax returns.
+function pjax(options) {
+  options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options)
+
+  if ($.isFunction(options.url)) {
+    options.url = options.url()
+  }
+
+  var target = options.target
+
+  var hash = parseURL(options.url).hash
+
+  var context = options.context = findContainerFor(options.container)
+
+  // We want the browser to maintain two separate internal caches: one
+  // for pjax'd partial page loads and one for normal page loads.
+  // Without adding this secret parameter, some browsers will often
+  // confuse the two.
+  if (!options.data) options.data = {}
+  options.data._pjax = context.selector
+
+  function fire(type, args) {
+    var event = $.Event(type, { relatedTarget: target })
+    context.trigger(event, args)
+    return !event.isDefaultPrevented()
+  }
+
+  var timeoutTimer
+
+  options.beforeSend = function(xhr, settings) {
+    // No timeout for non-GET requests
+    // Its not safe to request the resource again with a fallback method.
+    if (settings.type !== 'GET') {
+      settings.timeout = 0
+    }
+
+    xhr.setRequestHeader('X-PJAX', 'true')
+    xhr.setRequestHeader('X-PJAX-Container', context.selector)
+
+    if (!fire('pjax:beforeSend', [xhr, settings]))
+      return false
+
+    if (settings.timeout > 0) {
+      timeoutTimer = setTimeout(function() {
+        if (fire('pjax:timeout', [xhr, options]))
+          xhr.abort('timeout')
+      }, settings.timeout)
+
+      // Clear timeout setting so jquerys internal timeout isn't invoked
+      settings.timeout = 0
+    }
+
+    options.requestUrl = parseURL(settings.url).href
+  }
+
+  options.complete = function(xhr, textStatus) {
+    if (timeoutTimer)
+      clearTimeout(timeoutTimer)
+
+    fire('pjax:complete', [xhr, textStatus, options])
+
+    fire('pjax:end', [xhr, options])
+  }
+
+  options.error = function(xhr, textStatus, errorThrown) {
+    var container = extractContainer("", xhr, options)
+
+    var allowed = fire('pjax:error', [xhr, textStatus, errorThrown, options])
+    if (options.type == 'GET' && textStatus !== 'abort' && allowed) {
+      locationReplace(container.url)
+    }
+  }
+
+  options.success = function(data, status, xhr) {
+    // If $.pjax.defaults.version is a function, invoke it first.
+    // Otherwise it can be a static string.
+    var currentVersion = (typeof $.pjax.defaults.version === 'function') ?
+      $.pjax.defaults.version() :
+      $.pjax.defaults.version
+
+    var latestVersion = xhr.getResponseHeader('X-PJAX-Version')
+
+    var container = extractContainer(data, xhr, options)
+
+    // If there is a layout version mismatch, hard load the new url
+    if (currentVersion && latestVersion && currentVersion !== latestVersion) {
+      locationReplace(container.url)
+      return
+    }
+
+    // If the new response is missing a body, hard load the page
+    if (!container.contents) {
+      locationReplace(container.url)
+      return
+    }
+
+    pjax.state = {
+      id: options.id || uniqueId(),
+      url: container.url,
+      title: container.title,
+      container: context.selector,
+      fragment: options.fragment,
+      timeout: options.timeout
+    }
+
+    if (options.push || options.replace) {
+      window.history.replaceState(pjax.state, container.title, container.url)
+    }
+
+    // Clear out any focused controls before inserting new page contents.
+    document.activeElement.blur()
+
+    if (container.title) document.title = container.title
+    context.html(container.contents)
+
+    // FF bug: Won't autofocus fields that are inserted via JS.
+    // This behavior is incorrect. So if theres no current focus, autofocus
+    // the last field.
+    //
+    // http://www.w3.org/html/wg/drafts/html/master/forms.html
+    var autofocusEl = context.find('input[autofocus], textarea[autofocus]').last()[0]
+    if (autofocusEl && document.activeElement !== autofocusEl) {
+      autofocusEl.focus();
+    }
+
+    executeScriptTags(container.scripts)
+
+    // Scroll to top by default
+    if (typeof options.scrollTo === 'number')
+      $(window).scrollTop(options.scrollTo)
+
+    // If the URL has a hash in it, make sure the browser
+    // knows to navigate to the hash.
+    if ( hash !== '' ) {
+      // Avoid using simple hash set here. Will add another history
+      // entry. Replace the url with replaceState and scroll to target
+      // by hand.
+      //
+      //   window.location.hash = hash
+      var url = parseURL(container.url)
+      url.hash = hash
+
+      pjax.state.url = url.href
+      window.history.replaceState(pjax.state, container.title, url.href)
+
+      var target = $(url.hash)
+      if (target.length) $(window).scrollTop(target.offset().top)
+    }
+
+    fire('pjax:success', [data, status, xhr, options])
+  }
+
+
+  // Initialize pjax.state for the initial page load. Assume we're
+  // using the container and options of the link we're loading for the
+  // back button to the initial page. This ensures good back button
+  // behavior.
+  if (!pjax.state) {
+    pjax.state = {
+      id: uniqueId(),
+      url: window.location.href,
+      title: document.title,
+      container: context.selector,
+      fragment: options.fragment,
+      timeout: options.timeout
+    }
+    window.history.replaceState(pjax.state, document.title)
+  }
+
+  // Cancel the current request if we're already pjaxing
+  var xhr = pjax.xhr
+  if ( xhr && xhr.readyState < 4) {
+    xhr.onreadystatechange = $.noop
+    xhr.abort()
+  }
+
+  pjax.options = options
+  var xhr = pjax.xhr = $.ajax(options)
+
+  if (xhr.readyState > 0) {
+    if (options.push && !options.replace) {
+      // Cache current container element before replacing it
+      cachePush(pjax.state.id, context.clone().contents())
+
+      window.history.pushState(null, "", stripPjaxParam(options.requestUrl))
+    }
+
+    fire('pjax:start', [xhr, options])
+    fire('pjax:send', [xhr, options])
+  }
+
+  return pjax.xhr
+}
+
+// Public: Reload current page with pjax.
+//
+// Returns whatever $.pjax returns.
+function pjaxReload(container, options) {
+  var defaults = {
+    url: window.location.href,
+    push: false,
+    replace: true,
+    scrollTo: false
+  }
+
+  return pjax($.extend(defaults, optionsFor(container, options)))
+}
+
+// Internal: Hard replace current state with url.
+//
+// Work for around WebKit
+//   https://bugs.webkit.org/show_bug.cgi?id=93506
+//
+// Returns nothing.
+function locationReplace(url) {
+  window.history.replaceState(null, "", "#")
+  window.location.replace(url)
+}
+
+
+var initialPop = true
+var initialURL = window.location.href
+var initialState = window.history.state
+
+// Initialize $.pjax.state if possible
+// Happens when reloading a page and coming forward from a different
+// session history.
+if (initialState && initialState.container) {
+  pjax.state = initialState
+}
+
+// Non-webkit browsers don't fire an initial popstate event
+if ('state' in window.history) {
+  initialPop = false
+}
+
+// popstate handler takes care of the back and forward buttons
+//
+// You probably shouldn't use pjax on pages with other pushState
+// stuff yet.
+function onPjaxPopstate(event) {
+  var state = event.state
+
+  if (state && state.container) {
+    // When coming forward from a separate history session, will get an
+    // initial pop with a state we are already at. Skip reloading the current
+    // page.
+    if (initialPop && initialURL == state.url) return
+
+    // If popping back to the same state, just skip.
+    // Could be clicking back from hashchange rather than a pushState.
+    if (pjax.state && pjax.state.id === state.id) return
+
+    var container = $(state.container)
+    if (container.length) {
+      var direction, contents = cacheMapping[state.id]
+
+      if (pjax.state) {
+        // Since state ids always increase, we can deduce the history
+        // direction from the previous state.
+        direction = pjax.state.id < state.id ? 'forward' : 'back'
+
+        // Cache current container before replacement and inform the
+        // cache which direction the history shifted.
+        cachePop(direction, pjax.state.id, container.clone().contents())
+      }
+
+      var popstateEvent = $.Event('pjax:popstate', {
+        state: state,
+        direction: direction
+      })
+      container.trigger(popstateEvent)
+
+      var options = {
+        id: state.id,
+        url: state.url,
+        container: container,
+        push: false,
+        fragment: state.fragment,
+        timeout: state.timeout,
+        scrollTo: false
+      }
+
+      if (contents) {
+        container.trigger('pjax:start', [null, options])
+
+        if (state.title) document.title = state.title
+        container.html(contents)
+        pjax.state = state
+
+        container.trigger('pjax:end', [null, options])
+      } else {
+        pjax(options)
+      }
+
+      // Force reflow/relayout before the browser tries to restore the
+      // scroll position.
+      container[0].offsetHeight
+    } else {
+      locationReplace(location.href)
+    }
+  }
+  initialPop = false
+}
+
+// Fallback version of main pjax function for browsers that don't
+// support pushState.
+//
+// Returns nothing since it retriggers a hard form submission.
+function fallbackPjax(options) {
+  var url = $.isFunction(options.url) ? options.url() : options.url,
+      method = options.type ? options.type.toUpperCase() : 'GET'
+
+  var form = $('<form>', {
+    method: method === 'GET' ? 'GET' : 'POST',
+    action: url,
+    style: 'display:none'
+  })
+
+  if (method !== 'GET' && method !== 'POST') {
+    form.append($('<input>', {
+      type: 'hidden',
+      name: '_method',
+      value: method.toLowerCase()
+    }))
+  }
+
+  var data = options.data
+  if (typeof data === 'string') {
+    $.each(data.split('&'), function(index, value) {
+      var pair = value.split('=')
+      form.append($('<input>', {type: 'hidden', name: pair[0], value: pair[1]}))
+    })
+  } else if (typeof data === 'object') {
+    for (key in data)
+      form.append($('<input>', {type: 'hidden', name: key, value: data[key]}))
+  }
+
+  $(document.body).append(form)
+  form.submit()
+}
+
+// Internal: Generate unique id for state object.
+//
+// Use a timestamp instead of a counter since ids should still be
+// unique across page loads.
+//
+// Returns Number.
+function uniqueId() {
+  return (new Date).getTime()
+}
+
+// Internal: Strips _pjax param from url
+//
+// url - String
+//
+// Returns String.
+function stripPjaxParam(url) {
+  return url
+    .replace(/\?_pjax=[^&]+&?/, '?')
+    .replace(/_pjax=[^&]+&?/, '')
+    .replace(/[\?&]$/, '')
+}
+
+// Internal: Parse URL components and returns a Locationish object.
+//
+// url - String URL
+//
+// Returns HTMLAnchorElement that acts like Location.
+function parseURL(url) {
+  var a = document.createElement('a')
+  a.href = url
+  return a
+}
+
+// Internal: Build options Object for arguments.
+//
+// For convenience the first parameter can be either the container or
+// the options object.
+//
+// Examples
+//
+//   optionsFor('#container')
+//   // => {container: '#container'}
+//
+//   optionsFor('#container', {push: true})
+//   // => {container: '#container', push: true}
+//
+//   optionsFor({container: '#container', push: true})
+//   // => {container: '#container', push: true}
+//
+// Returns options Object.
+function optionsFor(container, options) {
+  // Both container and options
+  if ( container && options )
+    options.container = container
+
+  // First argument is options Object
+  else if ( $.isPlainObject(container) )
+    options = container
+
+  // Only container
+  else
+    options = {container: container}
+
+  // Find and validate container
+  if (options.container)
+    options.container = findContainerFor(options.container)
+
+  return options
+}
+
+// Internal: Find container element for a variety of inputs.
+//
+// Because we can't persist elements using the history API, we must be
+// able to find a String selector that will consistently find the Element.
+//
+// container - A selector String, jQuery object, or DOM Element.
+//
+// Returns a jQuery object whose context is `document` and has a selector.
+function findContainerFor(container) {
+  container = $(container)
+
+  if ( !container.length ) {
+    throw "no pjax container for " + container.selector
+  } else if ( container.selector !== '' && container.context === document ) {
+    return container
+  } else if ( container.attr('id') ) {
+    return $('#' + container.attr('id'))
+  } else {
+    throw "cant get selector for pjax container!"
+  }
+}
+
+// Internal: Filter and find all elements matching the selector.
+//
+// Where $.fn.find only matches descendants, findAll will test all the
+// top level elements in the jQuery object as well.
+//
+// elems    - jQuery object of Elements
+// selector - String selector to match
+//
+// Returns a jQuery object.
+function findAll(elems, selector) {
+  return elems.filter(selector).add(elems.find(selector));
+}
+
+function parseHTML(html) {
+  return $.parseHTML(html, document, true)
+}
+
+// Internal: Extracts container and metadata from response.
+//
+// 1. Extracts X-PJAX-URL header if set
+// 2. Extracts inline <title> tags
+// 3. Builds response Element and extracts fragment if set
+//
+// data    - String response data
+// xhr     - XHR response
+// options - pjax options Object
+//
+// Returns an Object with url, title, and contents keys.
+function extractContainer(data, xhr, options) {
+  var obj = {}
+
+  // Prefer X-PJAX-URL header if it was set, otherwise fallback to
+  // using the original requested url.
+  obj.url = stripPjaxParam(xhr.getResponseHeader('X-PJAX-URL') || options.requestUrl)
+
+  // Attempt to parse response html into elements
+  if (/<html/i.test(data)) {
+    var $head = $(parseHTML(data.match(/<head[^>]*>([\s\S.]*)<\/head>/i)[0]))
+    var $body = $(parseHTML(data.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0]))
+  } else {
+    var $head = $body = $(parseHTML(data))
+  }
+
+  // If response data is empty, return fast
+  if ($body.length === 0)
+    return obj
+
+  // If there's a <title> tag in the header, use it as
+  // the page's title.
+  obj.title = findAll($head, 'title').last().text()
+
+  if (options.fragment) {
+    // If they specified a fragment, look for it in the response
+    // and pull it out.
+    if (options.fragment === 'body') {
+      var $fragment = $body
+    } else {
+      var $fragment = findAll($body, options.fragment).first()
+    }
+
+    if ($fragment.length) {
+      obj.contents = $fragment.contents()
+
+      // If there's no title, look for data-title and title attributes
+      // on the fragment
+      if (!obj.title)
+        obj.title = $fragment.attr('title') || $fragment.data('title')
+    }
+
+  } else if (!/<html/i.test(data)) {
+    obj.contents = $body
+  }
+
+  // Clean up any <title> tags
+  if (obj.contents) {
+    // Remove any parent title elements
+    obj.contents = obj.contents.not(function() { return $(this).is('title') })
+
+    // Then scrub any titles from their descendants
+    obj.contents.find('title').remove()
+
+    // Gather all script[src] elements
+    obj.scripts = findAll(obj.contents, 'script[src]').remove()
+    obj.contents = obj.contents.not(obj.scripts)
+  }
+
+  // Trim any whitespace off the title
+  if (obj.title) obj.title = $.trim(obj.title)
+
+  return obj
+}
+
+// Load an execute scripts using standard script request.
+//
+// Avoids jQuery's traditional $.getScript which does a XHR request and
+// globalEval.
+//
+// scripts - jQuery object of script Elements
+//
+// Returns nothing.
+function executeScriptTags(scripts) {
+  if (!scripts) return
+
+  var existingScripts = $('script[src]')
+
+  scripts.each(function() {
+    var src = this.src
+    var matchedScripts = existingScripts.filter(function() {
+      return this.src === src
+    })
+    if (matchedScripts.length) return
+
+    var script = document.createElement('script')
+    script.type = $(this).attr('type')
+    script.src = $(this).attr('src')
+    document.head.appendChild(script)
+  })
+}
+
+// Internal: History DOM caching class.
+var cacheMapping      = {}
+var cacheForwardStack = []
+var cacheBackStack    = []
+
+// Push previous state id and container contents into the history
+// cache. Should be called in conjunction with `pushState` to save the
+// previous container contents.
+//
+// id    - State ID Number
+// value - DOM Element to cache
+//
+// Returns nothing.
+function cachePush(id, value) {
+  cacheMapping[id] = value
+  cacheBackStack.push(id)
+
+  // Remove all entires in forward history stack after pushing
+  // a new page.
+  while (cacheForwardStack.length)
+    delete cacheMapping[cacheForwardStack.shift()]
+
+  // Trim back history stack to max cache length.
+  while (cacheBackStack.length > pjax.defaults.maxCacheLength)
+    delete cacheMapping[cacheBackStack.shift()]
+}
+
+// Shifts cache from directional history cache. Should be
+// called on `popstate` with the previous state id and container
+// contents.
+//
+// direction - "forward" or "back" String
+// id        - State ID Number
+// value     - DOM Element to cache
+//
+// Returns nothing.
+function cachePop(direction, id, value) {
+  var pushStack, popStack
+  cacheMapping[id] = value
+
+  if (direction === 'forward') {
+    pushStack = cacheBackStack
+    popStack  = cacheForwardStack
+  } else {
+    pushStack = cacheForwardStack
+    popStack  = cacheBackStack
+  }
+
+  pushStack.push(id)
+  if (id = popStack.pop())
+    delete cacheMapping[id]
+}
+
+// Public: Find version identifier for the initial page load.
+//
+// Returns String version or undefined.
+function findVersion() {
+  return $('meta').filter(function() {
+    var name = $(this).attr('http-equiv')
+    return name && name.toUpperCase() === 'X-PJAX-VERSION'
+  }).attr('content')
+}
+
+// Install pjax functions on $.pjax to enable pushState behavior.
+//
+// Does nothing if already enabled.
+//
+// Examples
+//
+//     $.pjax.enable()
+//
+// Returns nothing.
+function enable() {
+  $.fn.pjax = fnPjax
+  $.pjax = pjax
+  $.pjax.enable = $.noop
+  $.pjax.disable = disable
+  $.pjax.click = handleClick
+  $.pjax.submit = handleSubmit
+  $.pjax.reload = pjaxReload
+  $.pjax.defaults = {
+    timeout: 650,
+    push: true,
+    replace: false,
+    type: 'GET',
+    dataType: 'html',
+    scrollTo: 0,
+    maxCacheLength: 20,
+    version: findVersion
+  }
+  $(window).on('popstate.pjax', onPjaxPopstate)
+}
+
+// Disable pushState behavior.
+//
+// This is the case when a browser doesn't support pushState. It is
+// sometimes useful to disable pushState for debugging on a modern
+// browser.
+//
+// Examples
+//
+//     $.pjax.disable()
+//
+// Returns nothing.
+function disable() {
+  $.fn.pjax = function() { return this }
+  $.pjax = fallbackPjax
+  $.pjax.enable = enable
+  $.pjax.disable = $.noop
+  $.pjax.click = $.noop
+  $.pjax.submit = $.noop
+  $.pjax.reload = function() { window.location.reload() }
+
+  $(window).off('popstate.pjax', onPjaxPopstate)
+}
+
+
+// Add the state property to jQuery's event object so we can use it in
+// $(window).bind('popstate')
+if ( $.inArray('state', $.event.props) < 0 )
+  $.event.props.push('state')
+
+// Is pjax supported by this browser?
+$.support.pjax =
+  window.history && window.history.pushState && window.history.replaceState &&
+  // pushState isn't reliable on iOS until 5.
+  !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/)
+
+$.support.pjax ? enable() : disable()
+
+})(jQuery);
diff --git a/js/redactor-osticket.js b/js/redactor-osticket.js
index 4a919612bce3612a623f62a60085f04e883a5018..85bed3ac97ef07723a6e816b3a28b130faa69a56 100644
--- a/js/redactor-osticket.js
+++ b/js/redactor-osticket.js
@@ -280,4 +280,5 @@ $(function() {
     };
     findRichtextBoxes();
     $(document).ajaxStop(findRichtextBoxes);
+    $(document).on('pjax:success', findRichtextBoxes);
 });
diff --git a/scp/css/scp.css b/scp/css/scp.css
index c91bc26ce61f43324985d3b231aef0cc1e072259..70695648c42e76087886d80362f3c91cd6785107 100644
--- a/scp/css/scp.css
+++ b/scp/css/scp.css
@@ -1505,17 +1505,20 @@ ul.progress li.no small {color:red;}
 }
 
 #loading, #upgrading {
-    border:1px solid #2a67ac;
-    padding: 10px 10px 10px 60px;
+    border:3px solid #777;
+    border-radius: 10px;
+    padding: 10px;
     width: 300px;
-    height: 100px;
-    background: rgb( 255, 255, 255) url('../images/FhHRx-Spinner.gif') 10px 50% no-repeat;
+    background: #555 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAWUlEQVQYGY2PAQoAIAgDrQf6pf7kC2vCRlRUgrjqXFrMrI3McHdKiwhpiMrTDRL4ghL8geQIsc6EOwbMcsYXhIYyUlvTgXUeS1vzkXWG8OMRXCE0b+AJAtgB7jEc+2fs6e8AAAAASUVORK5CYII=) repeat;
     position: fixed;
     display: none;
     z-index: 3000;
+    box-shadow: 0 5px 20px #001;
+    vertical-align: middle;
 }
 
-#loading h4, #upgrading h4 { margin: 3px 0 0 0; padding: 0; color: #d80; }
+#loading h1, #upgrading h4 { margin: 3px 0 0 0; padding: 0; color: #d80; }
+#loading, #upgrading * { color: white; }
 
 .non-local-image:after {
     background: url(../images/ost-logo.png) center center no-repeat;
diff --git a/scp/forms.php b/scp/forms.php
index 43e66242aa85f55ea2baad290a9fa29369990055..18303449ea47b4be66e13baa7db94a79b3299a77 100644
--- a/scp/forms.php
+++ b/scp/forms.php
@@ -126,7 +126,8 @@ $page='dynamic-forms.inc.php';
 if($form || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add')))
     $page='dynamic-form.inc.php';
 
-$ost->addExtraHeader('<meta name="tip-namespace" content="forms" />');
+$ost->addExtraHeader('<meta name="tip-namespace" content="forms" />',
+    "$('#content').data('tipNamespace', 'forms');");
 $nav->setTabActive('manage');
 require(STAFFINC_DIR.'header.inc.php');
 require(STAFFINC_DIR.$page);
diff --git a/scp/js/scp.js b/scp/js/scp.js
index e2bb171c5a8a7a0e1beef6ce8a7bc1b068304d3f..60eef9e66bb15d02bf54681e57fa527da01b2a96 100644
--- a/scp/js/scp.js
+++ b/scp/js/scp.js
@@ -29,7 +29,7 @@ function checkbox_checker(formObj, min, max) {
 }
 
 
-$(document).ready(function(){
+var scp_prep = function() {
 
     $("input:not(.dp):visible:enabled:first").focus();
     $('table.list tbody tr:odd').addClass('odd');
@@ -241,42 +241,7 @@ $(document).ready(function(){
             })
             .done(function() { })
             .fail(function() { });
-     });
-
-
-
-
-    /************ global inits *****************/
-
-    //Add CSRF token to the ajax requests.
-    // Many thanks to https://docs.djangoproject.com/en/dev/ref/contrib/csrf/ + jared.
-    $(document).ajaxSend(function(event, xhr, settings) {
-
-        function sameOrigin(url) {
-            // url could be relative or scheme relative or absolute
-            var host = document.location.host; // host + port
-            var protocol = document.location.protocol;
-            var sr_origin = '//' + host;
-            var origin = protocol + sr_origin;
-            // Allow absolute or scheme relative URLs to same origin
-            return (url == origin || url.slice(0, origin.length + 1) == origin + '/') ||
-                (url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') ||
-                // or any other URL that isn't scheme relative or absolute i.e
-                // relative.
-                !(/^(\/\/|http:|https:).*/.test(url));
-        }
-
-        function safeMethod(method) {
-            return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
-        }
-        if (!safeMethod(settings.type) && sameOrigin(settings.url)) {
-            xhr.setRequestHeader("X-CSRFToken", $("meta[name=csrf_token]").attr("content"));
-        }
-
-       });
-
-    /* Get config settings from the backend */
-    jQuery.fn.exists = function() { return this.length>0; };
+    });
 
     /* Multifile uploads */
     var elems = $('.multifile');
@@ -292,23 +257,6 @@ $(document).ready(function(){
         });
     }
 
-    $.translate_format = function(str) {
-        var translation = {
-            'd':'dd',
-            'j':'d',
-            'z':'o',
-            'm':'mm',
-            'F':'MM',
-            'n':'m',
-            'Y':'yy'
-        };
-        // Change PHP formats to datepicker ones
-        $.each(translation, function(php, jqdp) {
-            str = str.replace(php, jqdp);
-        });
-        return str;
-    };
-
     /* Datepicker */
     getConfig().then(function(c) {
         $('.dp').datepicker({
@@ -371,6 +319,7 @@ $(document).ready(function(){
         },
         property: "email"
     });
+
     $('.staff-username.typeahead').typeahead({
         source: function (typeahead, query) {
             if(query.length > 2) {
@@ -418,18 +367,6 @@ $(document).ready(function(){
         return false;
     });
 
-    $(document).keydown(function(e) {
-
-        if (e.keyCode == 27 && !$('#overlay').is(':hidden')) {
-            $('div.dialog').hide();
-            $('#overlay').hide();
-
-            e.preventDefault();
-            e.stopPropagation();
-            return false;
-        }
-    });
-
     /* advanced search */
     $('.dialog#advanced-search').css({
         top  : ($(window).height() / 6),
@@ -449,66 +386,6 @@ $(document).ready(function(){
         $('#advanced-search').show();
     });
 
-    $.dialog = function (url, codes, cb, options) {
-        options = options||{};
-
-        if (codes && !$.isArray(codes))
-            codes = [codes];
-
-        $('.dialog#popup .body').load(url, function () {
-            $('#overlay').show();
-            $('.dialog#popup').show({
-                duration: 0,
-                complete: function() { if (options.onshow) options.onshow(); }
-            });
-            $(document).off('.dialog');
-            $(document).on('submit.dialog', '.dialog#popup form', function(e) {
-                e.preventDefault();
-                var $form = $(this);
-                var $dialog = $form.closest('.dialog');
-                $.ajax({
-                    type:  $form.attr('method'),
-                    url: 'ajax.php/'+$form.attr('action').substr(1),
-                    data: $form.serialize(),
-                    cache: false,
-                    success: function(resp, status, xhr) {
-                        if (xhr && xhr.status && codes
-                            && $.inArray(xhr.status, codes) != -1) {
-                            $('div.body', $dialog).empty();
-                            $dialog.hide();
-                            $('#overlay').hide();
-                            if(cb) cb(xhr);
-                        } else {
-                            $('div.body', $dialog).html(resp);
-                            $('#msg_notice, #msg_error', $dialog).delay(5000).slideUp();
-                        }
-                    }
-                })
-                .done(function() { })
-                .fail(function() { });
-                return false;
-            });
-         });
-        if (options.onload) { options.onload(); }
-     };
-
-    $.userLookup = function (url, cb) {
-        $.dialog(url, 201, function (xhr) {
-            var user = $.parseJSON(xhr.responseText);
-            if (cb) cb(user);
-        }, {
-            onshow: function() { $('#user-search').focus(); }
-        });
-    };
-
-    $.orgLookup = function (url, cb) {
-        $.dialog(url, 201, function (xhr) {
-            var org = $.parseJSON(xhr.responseText);
-            if (cb) cb(org);
-        }, {
-            onshow: function() { $('#org-search').focus(); }
-        });
-    };
 
     $('#advanced-search').delegate('#status', 'change', function() {
         switch($(this).val()) {
@@ -573,6 +450,7 @@ $(document).ready(function(){
       });
       return ui;
    };
+
    // Sortable tables for dynamic forms objects
    $('.sortable-rows').sortable({
        'helper': fixHelper,
@@ -584,34 +462,158 @@ $(document).ready(function(){
            });
        }
    });
+};
 
-   //Tabs
-   $(document).on('click.tab', 'ul.tabs li a', function(e) {
-        e.preventDefault();
-        if ($('.tab_content'+$(this).attr('href')).length) {
-            var ul = $(this).closest('ul');
-            $('ul.tabs li a', ul.parent()).removeClass('active');
-            $(this).addClass('active');
-            $('.tab_content', ul.parent()).hide();
-            $('.tab_content'+$(this).attr('href')).show();
-        }
+$(document).ready(scp_prep);
+$(document).on('pjax:complete', scp_prep);
+
+    /************ global inits *****************/
+
+//Add CSRF token to the ajax requests.
+// Many thanks to https://docs.djangoproject.com/en/dev/ref/contrib/csrf/ + jared.
+$(document).ajaxSend(function(event, xhr, settings) {
+
+    function sameOrigin(url) {
+        // url could be relative or scheme relative or absolute
+        var host = document.location.host; // host + port
+        var protocol = document.location.protocol;
+        var sr_origin = '//' + host;
+        var origin = protocol + sr_origin;
+        // Allow absolute or scheme relative URLs to same origin
+        return (url == origin || url.slice(0, origin.length + 1) == origin + '/') ||
+            (url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') ||
+            // or any other URL that isn't scheme relative or absolute i.e
+            // relative.
+            !(/^(\/\/|http:|https:).*/.test(url));
+    }
+
+    function safeMethod(method) {
+        return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
+    }
+    if (!safeMethod(settings.type) && sameOrigin(settings.url)) {
+        xhr.setRequestHeader("X-CSRFToken", $("meta[name=csrf_token]").attr("content"));
+    }
+
+});
+
+/* Get config settings from the backend */
+jQuery.fn.exists = function() { return this.length>0; };
+
+$.translate_format = function(str) {
+    var translation = {
+        'd':'dd',
+        'j':'d',
+        'z':'o',
+        'm':'mm',
+        'F':'MM',
+        'n':'m',
+        'Y':'yy'
+    };
+    // Change PHP formats to datepicker ones
+    $.each(translation, function(php, jqdp) {
+        str = str.replace(php, jqdp);
     });
+    return str;
+};
+$(document).keydown(function(e) {
+
+    if (e.keyCode == 27 && !$('#overlay').is(':hidden')) {
+        $('div.dialog').hide();
+        $('#overlay').hide();
 
-    //Collaborators
-    $(document).on('click', 'a.collaborator, a.collaborators', function(e) {
         e.preventDefault();
-        var url = 'ajax.php/'+$(this).attr('href').substr(1);
-        $.dialog(url, 201, function (xhr) {
-           $('input#emailcollab').show();
-           $('#recipients').text(xhr.responseText);
-           $('.tip_box').remove();
-        }, {
-            onshow: function() { $('#user-search').focus(); }
-        });
+        e.stopPropagation();
         return false;
+    }
+});
+
+$.dialog = function (url, codes, cb, options) {
+    options = options||{};
+
+    if (codes && !$.isArray(codes))
+        codes = [codes];
+
+    $('.dialog#popup .body').load(url, function () {
+        $('#overlay').show();
+        $('.dialog#popup').show({
+            duration: 0,
+            complete: function() { if (options.onshow) options.onshow(); }
+        });
+        $(document).off('.dialog');
+        $(document).on('submit.dialog', '.dialog#popup form', function(e) {
+            e.preventDefault();
+            var $form = $(this);
+            var $dialog = $form.closest('.dialog');
+            $.ajax({
+                type:  $form.attr('method'),
+                url: 'ajax.php/'+$form.attr('action').substr(1),
+                data: $form.serialize(),
+                cache: false,
+                success: function(resp, status, xhr) {
+                    if (xhr && xhr.status && codes
+                        && $.inArray(xhr.status, codes) != -1) {
+                        $('div.body', $dialog).empty();
+                        $dialog.hide();
+                        $('#overlay').hide();
+                        if(cb) cb(xhr);
+                    } else {
+                        $('div.body', $dialog).html(resp);
+                        $('#msg_notice, #msg_error', $dialog).delay(5000).slideUp();
+                    }
+                }
+            })
+            .done(function() { })
+            .fail(function() { });
+            return false;
+        });
      });
+    if (options.onload) { options.onload(); }
+ };
+
+$.userLookup = function (url, cb) {
+    $.dialog(url, 201, function (xhr) {
+        var user = $.parseJSON(xhr.responseText);
+        if (cb) cb(user);
+    }, {
+        onshow: function() { $('#user-search').focus(); }
+    });
+};
+
+$.orgLookup = function (url, cb) {
+    $.dialog(url, 201, function (xhr) {
+        var org = $.parseJSON(xhr.responseText);
+        if (cb) cb(org);
+    }, {
+        onshow: function() { $('#org-search').focus(); }
+    });
+};
+
+//Tabs
+$(document).on('click.tab', 'ul.tabs li a', function(e) {
+    e.preventDefault();
+    if ($('.tab_content'+$(this).attr('href')).length) {
+        var ul = $(this).closest('ul');
+        $('ul.tabs li a', ul.parent()).removeClass('active');
+        $(this).addClass('active');
+        $('.tab_content', ul.parent()).hide();
+        $('.tab_content'+$(this).attr('href')).show();
+    }
 });
 
+//Collaborators
+$(document).on('click', 'a.collaborator, a.collaborators', function(e) {
+    e.preventDefault();
+    var url = 'ajax.php/'+$(this).attr('href').substr(1);
+    $.dialog(url, 201, function (xhr) {
+       $('input#emailcollab').show();
+       $('#recipients').text(xhr.responseText);
+       $('.tip_box').remove();
+    }, {
+        onshow: function() { $('#user-search').focus(); }
+    });
+    return false;
+ });
+
 // NOTE: getConfig should be global
 getConfig = (function() {
     var dfd = $.Deferred(),
@@ -623,8 +625,35 @@ getConfig = (function() {
                 dataType: 'json',
                 success: function (json_config) {
                     dfd.resolve(json_config);
+                },
+                error: function() {
+                    requested = null;
                 }
             });
         return dfd;
     }
 })();
+
+$(document).on('pjax:start', function() {
+    // Don't show the spinner on back button
+    if (event instanceof PopStateEvent)
+        return;
+
+    clearInterval(window.ticket_refresh);
+    $('#loading').show().css({opacity:0.7});
+    // Clear all timeouts
+    var id = window.setTimeout(function() {}, 0);
+    while (id--) {
+      window.clearTimeout(id);
+    }
+});
+$(document).on('pjax:complete', function() {
+    $('#loading').hide().css({opacity:1});
+});
+$(document).on('click', 'a', function() {
+    var ul = $(this).closest('ul');
+    if (ul.is('#sub_nav')) {
+        $('a.active', ul).removeClass('active');
+        $(this).addClass('active');
+    }
+});
diff --git a/scp/js/ticket.js b/scp/js/ticket.js
index f16576d6f84320f8f3dc5c928a8d36f7cf950032..b590e5c017adfffcdcd8360b55ce58c8c1aacbb1 100644
--- a/scp/js/ticket.js
+++ b/scp/js/ticket.js
@@ -265,7 +265,7 @@ $.autoLock = autoLock;
    UI & form events
 */
 
-jQuery(function($) {
+var ticket_onload = function($) {
     $('#response_options form').hide();
     $('#ticket_notes').hide();
     if(location.hash != "" && $('#response_options '+location.hash).length) {
@@ -407,7 +407,9 @@ jQuery(function($) {
             // TODO: Add a hover-button to show just one image
         });
     });
-});
+};
+$(ticket_onload);
+$(document).on('pjax:success', function() { ticket_onload(jQuery); });
 
 showImagesInline = function(urls, thread_id) {
     var selector = (thread_id == undefined)
@@ -435,7 +437,7 @@ showImagesInline = function(urls, thread_id) {
                     }
                 ).append($('<div class="caption">')
                     .append('<span class="filename">'+info.filename+'</span>')
-                    .append('<a href="'+info.download_url+'" class="action-button"><i class="icon-download-alt"></i> Download</a>')
+                    .append('<a href="'+info.download_url+'" class="action-button no-pjax"><i class="icon-download-alt"></i> Download</a>')
                 );
             e.data('wrapped', true);
         }
diff --git a/scp/js/tips.js b/scp/js/tips.js
index 9fdd2261701abc3c60012dd54a6ebe10a6b548fc..a4a9e8b4c61cb7499c9cefc9b123ad29a0ec8390 100644
--- a/scp/js/tips.js
+++ b/scp/js/tips.js
@@ -24,20 +24,26 @@ jQuery(function() {
             });
     },
     getHelpTips = (function() {
-        var dfd = $.Deferred(),
-            requested = false,
-            namespace = $('meta[name=tip-namespace]').attr('content');
-        return function() {
-            if (namespace && dfd.state() != 'resolved' && !requested)
-                requested = $.ajax({
+        var dfd, cache = {};
+        return function(namespace) {
+            var namespace = namespace
+                || $('#content').data('tipNamespace')
+                || $('meta[name=tip-namespace]').attr('content');
+            if (!namespace)
+                return false;
+            else if (!cache[namespace])
+                cache[namespace] = {
+                  dfd: dfd = $.Deferred(),
+                  ajax: $.ajax({
                     url: "ajax.php/help/tips/" + namespace,
                     dataType: 'json',
-                    success: function (json_config) {
-                        dfd.resolve(json_config);
-                    }
-                });
-            return dfd;
-        }
+                    success: $.proxy(function (json_config) {
+                        this.resolve(json_config);
+                    }, dfd)
+                  })
+                }
+            return cache[namespace].dfd;
+        };
     })();
 
     //Generic tip.
diff --git a/scp/settings.php b/scp/settings.php
index a4243fd957fb780eea3b151fd96c8b9770ef9ccb..96ee4b97c20c50fde39cce47bc9f28edeb83a1c9 100644
--- a/scp/settings.php
+++ b/scp/settings.php
@@ -48,7 +48,8 @@ if($page && $_POST && !$errors) {
 }
 
 $config=($errors && $_POST)?Format::input($_POST):Format::htmlchars($cfg->getConfigInfo());
-$ost->addExtraHeader('<meta name="tip-namespace" content="'.$page[1].'" />');
+$ost->addExtraHeader('<meta name="tip-namespace" content="'.$page[1].'" />',
+    "$('#content').data('tipNamespace', '".$page[1]."');");
 
 $nav->setTabActive('settings', ('settings.php?t='.$target));
 require_once(STAFFINC_DIR.'header.inc.php');
diff --git a/scp/tickets.php b/scp/tickets.php
index 1655e7027a98ca7af715d3213b4b68c318134692..07ee1ed840cc18a2c28d54ab24d945533dcc24dd 100644
--- a/scp/tickets.php
+++ b/scp/tickets.php
@@ -616,10 +616,25 @@ if($ticket) {
 
     //set refresh rate if the user has it configured
     if(!$_POST && !$_REQUEST['a'] && ($min=$thisstaff->getRefreshRate()))
-        $ost->addExtraHeader('<meta http-equiv="refresh" content="'.($min*60).'" />');
+        $ost->addExtraHeader('',
+            "window.ticket_refresh = setTimeout(function() { $.pjax({url: document.location.href, container:'#content'});},"
+            .($min*60000).");");
 }
 
+$ost->addExtraHeader('<script type="text/javascript" src="js/ticket.js"></script>');
+
 require_once(STAFFINC_DIR.'header.inc.php');
 require_once(STAFFINC_DIR.$inc);
 require_once(STAFFINC_DIR.'footer.inc.php');
+
+if (isset($_SERVER['HTTP_X_PJAX'])) {
+    // Update the ticket queue counts in the navigation
+    ob_start();
+    include STAFFINC_DIR . "templates/sub-navigation.tmpl.php";
+    $nav_content = ob_get_clean();
+?>
+<script type="text/javascript">
+    $('#sub_nav').html(<?php echo JsonDataEncoder::encode($nav_content); ?>);
+</script><?
+}
 ?>