Zendesk View Agent Name v2.0.0
This userscript enhances the Zendesk ticket viewing experience by displaying the first name of agents below their avatar in the agent collision viewer list. This makes it easier to quickly identify which agents are currently viewing or editing the same ticket.
The script automatically detects when the agent collision viewer list appears and updates the display when agents join or leave.
Script Content
// ==UserScript==
// @name Zendesk: View Agent Name
// @namespace https://www.timhilton.xyz/user-scripts
// @version 2.0.0
// @description Displays the first name of agents below their avatar in the agent collision viewer list.
// @author Tim Hilton using GitHub Copilot
// @match https://*.zendesk.com/agent/tickets/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const LOG_PREFIX = '[Zendesk: View Agent Name]';
let observerFireCount = 0;
console.debug(`${LOG_PREFIX} Script initialised`);
// Add styles for the name labels
const style = document.createElement('style');
style.textContent = `
.agent-name-label {
position: absolute;
bottom: -20px;
left: 50%;
transform: translateX(-50%);
background: green;
color: white;
text-align: center;
padding: 2px 6px;
font-size: 11px;
white-space: nowrap;
border-radius: 2px;
z-index: 1000;
}
#agentCollisionViewerList li {
position: relative;
}
`;
document.head.appendChild(style);
let listObserver = null;
let documentObserver = null;
// Process collision avatars and add name labels
function processCollisionAvatars() {
console.debug(`${LOG_PREFIX} Processing collision avatars`);
const buttons = document.querySelectorAll('#agentCollisionViewerList button[data-test-id="collision-avatar"]');
console.debug(`${LOG_PREFIX} Found ${buttons.length} collision avatar button(s)`);
let processedCount = 0;
buttons.forEach(button => {
const img = button.querySelector('img');
if (!img) {
console.debug(`${LOG_PREFIX} Button has no img element, skipping`);
return;
}
const altText = img.getAttribute('alt');
if (!altText) {
console.debug(`${LOG_PREFIX} img has no alt attribute, skipping`);
return;
}
// Skip if the alt text hasn't changed since we last processed this button
if (button.dataset.lastAlt === altText) {
return;
}
// alt format is "Agent John Smith is isIdle" — strip leading "Agent " then take first word
const nameText = altText.replace(/^Agent\s+/i, '').trim();
const firstName = nameText.split(/\s+/)[0];
if (!firstName) {
console.debug(`${LOG_PREFIX} Could not extract first name from alt: "${altText}"`);
return;
}
console.debug(`${LOG_PREFIX} Extracted first name: "${firstName}" from alt: "${altText}"`);
// Find the parent li element
const parentLi = button.closest('li');
if (!parentLi) {
console.debug(`${LOG_PREFIX} Could not find parent li element`);
return;
}
// Update existing label or create a new one
const existingLabel = parentLi.querySelector('.agent-name-label');
if (existingLabel) {
existingLabel.textContent = firstName;
} else {
const nameLabel = document.createElement('div');
nameLabel.className = 'agent-name-label';
nameLabel.textContent = firstName;
parentLi.appendChild(nameLabel);
}
// Store the alt text we processed so we can detect future changes
button.dataset.lastAlt = altText;
processedCount++;
console.debug(`${LOG_PREFIX} Added/updated name label for: ${firstName}`);
});
if (processedCount > 0) {
console.log(`${LOG_PREFIX} ✅ Successfully added ${processedCount} name label(s)`);
}
}
// Observe mutations within the agentCollisionViewerList
function observeList() {
console.debug(`${LOG_PREFIX} Entering observeList()`);
const list = document.getElementById('agentCollisionViewerList');
if (!list) {
console.debug(`${LOG_PREFIX} agentCollisionViewerList not found, exiting`);
return;
}
console.debug(`${LOG_PREFIX} agentCollisionViewerList found, processing existing avatars`);
// Process existing avatars
processCollisionAvatars();
// Create observer for the list
if (listObserver) {
console.debug(`${LOG_PREFIX} Disconnecting existing list observer`);
listObserver.disconnect();
}
listObserver = new MutationObserver((mutations) => {
observerFireCount++;
console.debug(`${LOG_PREFIX} List MutationObserver fired (count: ${observerFireCount})`);
let shouldProcess = false;
for (const mutation of mutations) {
if (mutation.type === 'childList') {
// Check if buttons were added or removed
if (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0) {
console.debug(`${LOG_PREFIX} childList mutation detected: ${mutation.addedNodes.length} added, ${mutation.removedNodes.length} removed`);
shouldProcess = true;
break;
}
} else if (mutation.type === 'attributes' && mutation.attributeName === 'alt') {
// If img alt changes, we need to reprocess
console.debug(`${LOG_PREFIX} alt attribute changed`);
shouldProcess = true;
break;
}
}
if (shouldProcess) {
console.debug(`${LOG_PREFIX} Mutations require reprocessing`);
processCollisionAvatars();
} else {
console.debug(`${LOG_PREFIX} No relevant mutations, skipping reprocessing`);
}
});
listObserver.observe(list, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['alt']
});
console.debug(`${LOG_PREFIX} Now observing agentCollisionViewerList for mutations`);
}
// Observe the document for when agentCollisionViewerList appears
function observeDocument() {
console.debug(`${LOG_PREFIX} Entering observeDocument()`);
if (documentObserver) {
console.debug(`${LOG_PREFIX} Disconnecting existing document observer`);
documentObserver.disconnect();
}
// Use the narrowest available container to reduce noise
const container = document.querySelector('[data-test-id="workspace-content"]') || document.body;
console.debug(`${LOG_PREFIX} Observing container: ${container === document.body ? 'document.body' : '[data-test-id="workspace-content"]'}`);
documentObserver = new MutationObserver(() => {
console.debug(`${LOG_PREFIX} Document MutationObserver fired`);
const list = document.getElementById('agentCollisionViewerList');
if (list) {
// Found the list — disconnect immediately to stop unnecessary firing
console.debug(`${LOG_PREFIX} agentCollisionViewerList appeared in DOM`);
documentObserver.disconnect();
// Start observing the list
observeList();
// Also observe for when the list disappears
observeListDisappearance();
}
});
documentObserver.observe(container, {
childList: true,
subtree: true
});
console.debug(`${LOG_PREFIX} Now observing container for agentCollisionViewerList`);
// Check if the list already exists
const list = document.getElementById('agentCollisionViewerList');
if (list) {
console.debug(`${LOG_PREFIX} agentCollisionViewerList already exists in DOM`);
documentObserver.disconnect();
observeList();
observeListDisappearance();
} else {
console.debug(`${LOG_PREFIX} agentCollisionViewerList not yet in DOM, waiting for it to appear`);
}
}
// Observe for when the list disappears
function observeListDisappearance() {
console.debug(`${LOG_PREFIX} Setting up disappearance observer`);
// Use the narrowest available container to reduce noise
const container = document.querySelector('[data-test-id="workspace-content"]') || document.body;
const disappearanceObserver = new MutationObserver(() => {
console.debug(`${LOG_PREFIX} Disappearance MutationObserver fired`);
const list = document.getElementById('agentCollisionViewerList');
if (!list) {
// List has disappeared
console.debug(`${LOG_PREFIX} agentCollisionViewerList has disappeared from DOM`);
// Stop all observers
if (listObserver) {
console.debug(`${LOG_PREFIX} Disconnecting list observer`);
listObserver.disconnect();
listObserver = null;
}
disappearanceObserver.disconnect();
// Resume observing the document
console.debug(`${LOG_PREFIX} Resuming document observation`);
observeDocument();
}
});
disappearanceObserver.observe(container, {
childList: true,
subtree: true
});
console.debug(`${LOG_PREFIX} Disappearance observer active`);
}
// Initialise
console.debug(`${LOG_PREFIX} Starting initialisation`);
observeDocument();
console.log(`${LOG_PREFIX} 🎉 Script ready and monitoring for agent collision list`);
})();