Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ A Chrome browser extension that monitors GitHub Copilot Tasks pages and speaks t
- **Highlights & Summary** (default): Speaks Copilot responses and summaries, excludes tool logs
- **Summary Only**: Speaks only final summary messages from Copilot
- **New Only mode** (checkbox, enabled by default): Skip pre-existing content and only speak new content that appears after extension loads
- **Debug Mode** (checkbox, disabled by default): Shows element class names on hover for troubleshooting and understanding content structure
- Adjustable speech rate (0.5x to 2x, default 1.2x)
- Adjustable speech pitch (0.5x to 2x, default 1.0x)
- Speech queue with 2-second delays between items for better pacing
Expand Down Expand Up @@ -70,12 +71,39 @@ When new text content is detected, it is queued for speaking. After the first us
- **Highlights & Summary**: Speaks Copilot responses and summaries (default)
- **Summary Only**: Speaks only final summary messages
- **New Only**: Checkbox to skip pre-existing content (enabled by default)
- **Debug Mode**: Checkbox to show element class names on hover (disabled by default, useful for troubleshooting)
- **Speed Slider**: Adjust speech rate (0.5x to 2x, default 1.2x)
- **Pitch Slider**: Adjust speech pitch (0.5x to 2x, default 1.0x)
6. The status shows your current position (e.g., "Item 3 of 10")

**Note:** The extension requires a user interaction (click or keypress) before it can speak. This is a browser security requirement. Once you interact with the page, all queued content will be spoken automatically with a 2-second delay between items. Elements being spoken are highlighted with a yellow background.

## Debug Mode

The **Debug Mode** feature helps understand what content is being spoken and troubleshoot issues by showing element class names on hover.

**To enable Debug Mode:**
1. Click the extension icon to open the popup
2. Check the "Debug Mode (show class names on hover)" checkbox
3. Close the popup and hover over any div element on the page

**Features:**
- Shows native browser tooltips with element class names when hovering over divs
- Displays `<div> (no classes)` for elements without classes
- Helps identify key containers like:
- `TaskChat-module__stickableContainer--*`
- `Session-module__detailsContainer--*`
- `MarkdownRenderer-module__container--*`
- `Tool-module__detailsContainer--*`
- Automatically applies to dynamically added content
- Setting persists across page reloads and browser sessions

**Use Cases:**
- Understanding GitHub Copilot's DOM structure
- Troubleshooting speech issues (e.g., why certain content is/isn't spoken)
- Identifying which elements are being targeted for speech
- Debugging the extension's behavior with new GitHub UI updates

## File Structure

```
Expand Down
48 changes: 48 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,51 @@
3. **"Not on Copilot Tasks page" in popup**: Verify URL matches `https://github.com/copilot/tasks/*`
4. **"Waiting for interaction" status**: Click anywhere on the page or press any key to enable speech
5. **Speech queue not progressing**: Check if playback is paused (button shows "▶ Play"); click to resume

## Test 22: Debug Mode Feature
**Steps:**
1. Navigate to a Copilot task page
2. Open the extension popup
3. Check the "Debug Mode (show class names on hover)" checkbox
4. Close the popup
5. Hover over various div elements on the page

**Expected Results:**
- Console shows: "CopilotTTS-Content: Debug mode enabled"
- Console shows: "CopilotTTS-Content: Applying debug mode tooltips"
- Console shows: "CopilotTTS-Content: Found X div elements to apply tooltips"
- Hovering over div elements shows native browser tooltips with class names
- Elements without classes show "<div> (no classes)" in tooltip
- Key containers show their full class names (e.g., "Session-module__detailsContainer--abc123")

**Steps to Disable:**
1. Open the popup again
2. Uncheck the "Debug Mode" checkbox

**Expected Results:**
- Console shows: "CopilotTTS-Content: Debug mode disabled"
- Console shows: "CopilotTTS-Content: Removing debug mode tooltips"
- Tooltips are removed from all elements
- Hovering no longer shows class names

**Steps to Test Persistence:**
1. Enable debug mode via checkbox
2. Close and reopen the popup
3. Reload the page

**Expected Results:**
- Debug mode checkbox remains checked after reopening popup
- Debug mode is automatically applied after page reload
- Console shows: "CopilotTTS-Content: Loaded debug mode: true"
- Tooltips appear automatically without needing to toggle again

**Steps to Test Dynamic Content:**
1. Enable debug mode
2. Type a message and submit to Copilot
3. Wait for new content to appear
4. Hover over the new div elements

**Expected Results:**
- New content also has tooltips applied automatically
- No need to re-enable debug mode for new elements
- All dynamically added divs get tooltips
108 changes: 106 additions & 2 deletions content.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const DEFAULT_PITCH = 1;
const DEFAULT_VERBOSITY = 'highlights'; // Default verbosity: Highlights & Summary
const DEFAULT_NEW_ONLY = true; // Default: skip pre-existing content
const GRACE_PERIOD_MS = 2000; // Time window to consider content as pre-existing (milliseconds)
const DEFAULT_DEBUG_MODE = false; // Default: debug mode off

// State management
let spokenItems = [];
Expand All @@ -26,6 +27,7 @@ let speechPitch = DEFAULT_PITCH; // Current speech pitch
let speechVerbosity = DEFAULT_VERBOSITY; // Current speech verbosity: 'all', 'highlights', or 'summary'
let newOnlyMode = DEFAULT_NEW_ONLY; // Whether to skip pre-existing content
let extensionInitTime = Date.now(); // Track when extension initialized to filter old content
let debugMode = DEFAULT_DEBUG_MODE; // Whether debug mode is enabled

// Initialize voices
function initVoices() {
Expand All @@ -42,8 +44,8 @@ function initVoices() {
selectedVoice = voices.find(v => v.name === DESIRED_VOICE_NAME) || voices[0];
console.log(`${TAG}: initVoices: Using voice: ${selectedVoice.name}`);

// Load saved rate, pitch, verbosity, and newOnly from storage
chrome.storage.sync.get(['speechRate', 'speechPitch', 'speechVerbosity', 'newOnly'], function(result) {
// Load saved rate, pitch, verbosity, newOnly, and debugMode from storage
chrome.storage.sync.get(['speechRate', 'speechPitch', 'speechVerbosity', 'newOnly', 'debugMode'], function(result) {
if (result.speechRate !== undefined) {
speechRate = result.speechRate;
console.log(`${TAG}: Loaded speech rate: ${speechRate}`);
Expand All @@ -60,6 +62,14 @@ function initVoices() {
newOnlyMode = result.newOnly;
console.log(`${TAG}: Loaded new only mode: ${newOnlyMode}`);
}
if (result.debugMode !== undefined) {
debugMode = result.debugMode;
console.log(`${TAG}: Loaded debug mode: ${debugMode}`);
// Apply debug mode if it was saved as enabled
if (debugMode) {
applyDebugMode();
}
}
});
}

Expand Down Expand Up @@ -266,6 +276,88 @@ function shouldSpeakElement(element, container) {
return !hasParentWithClass(element, container, 'Tool-module__detailsContainer');
}

// Apply debug mode tooltips to all divs under TaskChat container
function applyDebugMode() {
console.log(`${TAG}: Applying debug mode tooltips`);

const taskChatContainer = document.querySelector('[class*="TaskChat-module__stickableContainer--"]');
if (!taskChatContainer) {
console.log(`${TAG}: TaskChat container not found for debug mode`);
return;
}

// Find all div elements under the TaskChat container
const divs = taskChatContainer.querySelectorAll('div');
console.log(`${TAG}: Found ${divs.length} div elements to apply tooltips`);

divs.forEach(div => {
applyDebugTooltipToElement(div);
});
}

// Apply debug tooltip to a single element
function applyDebugTooltipToElement(element) {
if (element.nodeType !== Node.ELEMENT_NODE || element.tagName !== 'DIV') {
return;
}

// Get all class names
const classNames = element.className;
const tooltipText = classNames || `<div> (no classes)`;

// Set the title attribute with the class names or indication of no classes
element.setAttribute('data-tts-debug-tooltip', 'true');
element.setAttribute('title', tooltipText);
}

// Apply debug mode tooltips to an element and all its div children
function applyDebugModeToElement(element) {
if (!debugMode) {
return;
}

// Apply to the element itself if it's a div
applyDebugTooltipToElement(element);

// Apply to all div children
const divs = element.querySelectorAll ? element.querySelectorAll('div') : [];
divs.forEach(div => {
applyDebugTooltipToElement(div);
});
}

// Remove debug mode tooltips
function removeDebugMode() {
console.log(`${TAG}: Removing debug mode tooltips`);

const taskChatContainer = document.querySelector('[class*="TaskChat-module__stickableContainer--"]');
if (!taskChatContainer) {
console.log(`${TAG}: TaskChat container not found for debug mode removal`);
return;
}

// Find all elements with debug tooltips (more efficient selector)
const elements = taskChatContainer.querySelectorAll('[data-tts-debug-tooltip="true"]');
console.log(`${TAG}: Removing tooltips from ${elements.length} elements`);

elements.forEach(element => {
element.removeAttribute('data-tts-debug-tooltip');
element.removeAttribute('title');
});
}

// Toggle debug mode
function toggleDebugMode(enabled) {
debugMode = enabled;
console.log(`${TAG}: Debug mode ${enabled ? 'enabled' : 'disabled'}`);

if (enabled) {
applyDebugMode();
} else {
removeDebugMode();
}
}

// Helper function to add a spoken item if not already tracked
function addSpokenItem(text, element) {
if (text && !spokenItems.some(item => item.text === text)) {
Expand Down Expand Up @@ -368,6 +460,9 @@ function processSessionContainer(sessionContainer) {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Apply debug mode to new node if enabled
applyDebugModeToElement(node);

// Check if this node or its children contain markdown containers
let newMarkdownContainers = [];
if (node.matches && node.matches('[class*="MarkdownRenderer-module__container--"]')) {
Expand Down Expand Up @@ -483,6 +578,9 @@ function monitorTaskChat() {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Apply debug mode to new node if enabled
applyDebugModeToElement(node);

// Check if this is a session container
if (node.classList && Array.from(node.classList).some(c => c.includes('Session-module__detailsContainer--'))) {
//console.log(`${TAG}: Found new session container element`);
Expand Down Expand Up @@ -646,6 +744,12 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse({ success: true });
break;

case 'setDebugMode':
// Update debug mode
toggleDebugMode(message.debugMode ?? DEFAULT_DEBUG_MODE);
sendResponse({ success: true });
break;

case 'jumpTo':
// Jump to a specific item in the spokenItems array
const targetIndex = message.index;
Expand Down
6 changes: 6 additions & 0 deletions popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,12 @@ <h1>Copilot TTS</h1>
New Only (skip existing content)
</label>
</div>
<div class="verbosity-container">
<label>
<input type="checkbox" id="debugModeCheckbox">
Debug Mode (show class names on hover)
</label>
</div>
<div class="slider-container">
<div class="slider-row">
<label for="rateSlider">Speed:</label>
Expand Down
16 changes: 14 additions & 2 deletions popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ document.addEventListener('DOMContentLoaded', function() {
const progressLabel = document.getElementById('progressLabel');
const verbositySelect = document.getElementById('verbositySelect');
const newOnlyCheckbox = document.getElementById('newOnlyCheckbox');
const debugModeCheckbox = document.getElementById('debugModeCheckbox');

// Helper function to send message to content script
async function sendMessageToActiveTab(message) {
Expand Down Expand Up @@ -182,8 +183,8 @@ document.addEventListener('DOMContentLoaded', function() {
setTimeout(refreshStatus, 2000);
});

// Load saved rate, pitch, verbosity, and newOnly values
chrome.storage.sync.get(['speechRate', 'speechPitch', 'speechVerbosity', 'newOnly'], function(result) {
// Load saved rate, pitch, verbosity, newOnly, and debugMode values
chrome.storage.sync.get(['speechRate', 'speechPitch', 'speechVerbosity', 'newOnly', 'debugMode'], function(result) {
if (result.speechRate !== undefined) {
rateSlider.value = result.speechRate;
rateValue.textContent = result.speechRate + 'x';
Expand All @@ -198,6 +199,9 @@ document.addEventListener('DOMContentLoaded', function() {
if (result.newOnly !== undefined) {
newOnlyCheckbox.checked = result.newOnly;
}
if (result.debugMode !== undefined) {
debugModeCheckbox.checked = result.debugMode;
}
});

// Rate slider handler
Expand Down Expand Up @@ -250,6 +254,14 @@ document.addEventListener('DOMContentLoaded', function() {
console.log(`${TAG}: New Only set to: ${newOnly}`);
});

// Debug Mode checkbox handler
debugModeCheckbox.addEventListener('change', function() {
const debugMode = debugModeCheckbox.checked;
chrome.storage.sync.set({ debugMode: debugMode });
sendMessageToActiveTab({ action: 'setDebugMode', debugMode: debugMode });
console.log(`${TAG}: Debug Mode set to: ${debugMode}`);
});

// Initial status check
refreshStatus();

Expand Down