Now I understand the existing page. It's a full-page thread view with:
- Left column: Email timeline + AI responses + saved responses
- Right column (400px): Additional thread selection + AI query form
- Multi-provider selection (checkboxes)
- Content processing mode (full/stripped/smart)
- Conversation history (sessionStorage)
- Saves to MongoDB `ai_query_responses`
- Quick view modal for saved responses

The user wants me to enhance this page with:
1. Full page width (not wider than page) - already has max-width:1600px
2. Keep multi-model selection (send to multiple AI simultaneously)

3. Add Agent buttons (General/Quotes/Deception/Profiler/Composer) with sub-buttons for quick prompts
4. New search by email/thread with + buttons to include any thread/email (not just from same contact)
5. Estimated tokens display + don't exceed max
6. Save results (already does this, but should also use our new session system)
7. Continue discussing after initial result (already has conversation history)
8. Log everything to a log file for debugging

This is a significant rewrite. I need to plan the layout carefully—the current 2-column grid works, but I'll need to reorganize the sidebar to fit the Agent buttons and search functionality without making it too cramped.

For the new structure, I'm thinking a layout where the email timeline stays on the left, the context builder and search move to the right, and the Agent buttons plus provider selection sit below as a full-width section, with AI responses displayed at the bottom.

On the backend, I need to add file logging for AI requests and responses alongside the existing MongoDB storage, and on the frontend I'll need to integrate the agent buttons with their quick prompts, the context search functionality, and the token estimation display while keeping everything within the page width. I'm reconsidering the layout approach—the current two-column structure actually works well, so I'll enhance it rather than overhaul it completely. I'll expand the page width constraint and add the new features to the right sidebar: agent quick-action buttons, a context search builder to pull in related threads and contacts, token estimation display, and file logging capabilities, all while keeping the multi-model selection intact. I'm realizing the existing `ai_chat` endpoint in api.php already handles the full workflow—it proxies through codex51, manages tokens, and saves sessions—so I should route the logging through there instead of creating a separate endpoint, and add file logging directly when responses are saved.

But looking at the actual flow, thread_view.php sends directly to the api_opus proxy to support multi-model selection in a single request, which our `ai_chat` endpoint doesn't do. So I'll keep that existing path intact and layer the new features—agent presets, context building, token estimation, and file logging—on top of it rather than refactoring the whole 2000+ line file.

For the UI changes, I'm making the right panel responsive instead of fixed-width, adding agent preset buttons at the top of the form, a context builder with search and a cart for pulling in related threads, token estimation before sending, and a logging function that writes requests to a file. The system prompt will be injected based on whichever agent is selected, and I'll handle the file logging either as a new case in api.php or by extending the existing save_ai_response handler. I'll set the system prompt when an agent is selected, then use a search interface to find and add relevant threads as context. For token estimation, I'll divide the total content length by 3.5 and display it against the model's limit. The logging piece is straightforward—I'll create a PHP endpoint that receives the AI request and response data, then appends it to a file for debugging.

Rather than incrementally patching thread_view.php, I think a full rewrite makes sense given all the new features. I'll keep the PHP data-fetching section mostly intact but rebuild the HTML and JavaScript to include agent selection, the context builder with thread search, token counting, and logging integration. I also need to change the layout from `max-width: 1600px` to `width: 100%` with padding so it spans the full browser width.

Let me start by adding the logging endpoint to api.php, then rewrite thread_view.php with all these features integrated.

Rewriting thread_view.php... Finishing up the requirements—I need to track token usage against limits, maintain the conversation history for follow-up responses, and log all AI interactions to a file for debugging. Now I'm ready to start building this out as a complete rewrite of the thread view since there are so many structural changes. I'll preserve the existing PHP data fetching but redesign the interface with a three-column grid: email timeline on the left, agent selection and context builder in the center, and the response area on the right.

Building the layout structure... I'm weighing whether to make targeted edits or rewrite the whole file—the context builder and agent buttons alone require substantial HTML and JavaScript, so a full rewrite might be cleaner than piecemeal changes, though I'm concerned about the file size already being over 2000 lines.

Let me map out the structure: I'll keep the PHP data fetching and most of the existing layout intact, but reorganize the right column to add the agent selector, quick prompts, context builder, and token estimation display above the existing provider and content sections.

For the JavaScript, I need to define the agent configurations and their associated models with token limits, then build out the functions to handle agent selection, context building, and token counting. I'll also add a logging endpoint to track interactions and enhance the main submission function to work with the new agent system.

Now I'm ready to write the complete file—I'll start with the PHP logging endpoint, then build out the full thread view with all the new components integrated. about what the log file should contain. The user wants "everything we are sending to AI and the AI response". So:
- Timestamp
- Agent used
- Provider(s)
- Model(s)
- Thread IDs
- System prompt
- Full content sent (or at least a summary)
- Estimated tokens
- Question
- Response from each provider
- Cost/timing

For the log file path: `/var/www/mssql.danmarcrm.com/dev1/extension2v1/logs/ai_debug.log` (I'll create the logs directory if needed)

OK, let me implement. I'll modify api.php to add a logging case, then rewrite thread_view.php. The simplest approach is to just modify the existing `save_ai_response` case to also write to a file. Now I need to check if that endpoint exists in extension2v1.


This is a significant enhancement. Let me first understand the existing architecture, then implement everything.


TodoWrite

Add AI debug logging endpoint to api.php

Rewrite thread_view.php with agents, context builder, tokens, logging

Verify PHP syntax and test readiness


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Add AI debug logging endpoint to api.php

Rewrite thread_view.php with agents, context builder, tokens, logging

Verify PHP syntax and test readiness


You're out of extra usage · resets 10pm (UTC)


<ide_opened_file>The user opened the file /var/www/mssql.danmarcrm.com/dev1/extension2v1/public/api.php in the IDE. This may or may not be related to the current task.</ide_opened_file>

Ok, I just found we had something similar implemented ex:
https://mssql.danmarcrm.com/dev1/extension2v1/public/thread_view.php?thread_id=19ae5c0cb75e381c
But what we have now is better in terms as search for contact/thread
Pls implement our new features to existing thread view all page wide.
Make sure the total widh will be the whole page and not go wider
- keep the selection model so from the thread view so we can send to multiple AI at the same time
- but implement the General/Quotes/Deception/Profiler/Composer buttons with subbuttons
- as stated before use the new search by email/thread and + buttons so we can include any thread email etc
- of course add the estimated tokens and make sure we don't go over the max accepted
- as before results should be saved no changes here unless there must be
- we should be able to keep discussing after an initial result
- pls save in a log file everything we are sending to AI and the AI response for easy debug
- the main page should be with threads, and not that many to load it faster. The contacts should display also less records to speed up
- to fix, in threads list view all emails are assigned to Etienne Turner .. or nearly all for sure girls bonuses not related to him
- in Recent Analysis there are marked with Unknown .. not sure if it makes sense all the AI Thread Analysis tab .. if you don't find it usefull hide it and make sure it doesn't take time to load numbers or counts


Let me start by understanding the full scope. I need to explore the existing code to understand the issues and plan properly.


Entered plan mode. You should now focus on exploring the codebase and designing an implementation approach.

In plan mode, you should:
1. Thoroughly explore the codebase to understand existing patterns
2. Identify similar features and architectural approaches
3. Consider multiple approaches and their trade-offs
4. Use AskUserQuestion if you need to clarify the approach
5. Design a concrete implementation strategy
6. When ready, use ExitPlanMode to present your plan for approval

Remember: DO NOT write or edit any files yet. This is a read-only exploration and planning phase.


Task

Explore thread list and contact display

Prompt:
I need to understand how the main index page loads threads and contacts, and why threads might all show the same contact name, and why the AI Thread Analysis tab might be slow.

Explore these files in /var/www/mssql.danmarcrm.com/dev1/extension2v1/public/:

1. Look at `index.php` - find the "threads" tab section and "AI Thread Analysis" tab section. How are threads listed? Is there a tab for AI Thread Analysis?

2. Look at `api.php` - find the `threads` case (action=threads). How does it fetch threads? Look at the SQL/MongoDB query - why might all threads show the same contact name? Also find the `contacts` case and see how many records it loads by default.

3. Look at `api.php` for the `analysis_stats` or `analysis_queue` or any analytics/dashboard stats cases - these could be slowing the page load.

4. Look at `assets/app.js` - find the `loadThreads()` function and `loadContacts()` function. How many records do they request? What is loaded on DOMContentLoaded?

Report back:
- How threads are loaded and why all might show same contact
- How contacts are loaded and default limits
- What the AI Thread Analysis tab loads and how it could be slow
- What gets loaded on page init (DOMContentLoaded)


Explore thread list and contact display


Explore thread_view.php AI proxy

Prompt:
I need to understand how the existing thread_view.php sends requests to the AI proxy, and how the save_ai_response endpoint works. Also explore the AI proxy endpoint to understand multi-provider support.

Explore in /var/www/mssql.danmarcrm.com/dev1/extension2v1/:

1. In `public/api.php` - find the `save_ai_response` case and `get_ai_response` and `list_ai_responses` cases. How are AI responses saved/loaded?

2. Look at `aiapi/api_opus/api/query.php` - how does this proxy work? How does it handle multiple providers in one request? What does the request/response format look like?

3. Look at `aiapi/api_opus/api/` directory for any other relevant files (like providers list endpoint).

4. Check if there's a `logs/` directory already or any existing logging mechanism.

Report:
- The full save_ai_response flow and MongoDB collection used
- How api_opus query.php handles multi-provider requests
- The request/response format for the proxy
- Any existing logging mechanisms


Explore thread_view.php AI proxy


Explore QueryAdapter thread methods

Prompt:
I need to understand how QueryAdapter fetches threads and contacts, specifically why all threads might show the same contact name. Also understand the getThreads method.

Explore in /var/www/mssql.danmarcrm.com/dev1/extension2v1/src/:

1. Look at `QueryAdapter.php` - find the methods: `getThreads()`, `getThreadDetails()`, `getContactsWithProfiles()`. For getThreads, look at how it assigns contact_name to each thread. Is there a bug where the same contact gets assigned to all threads?

2. Also look for any thread listing methods that might have SQL vs MongoDB routing issues (the same bug we found with contacts using raw SQL when data is in MongoDB).

3. Check what `getThreads()` returns - does it have subject, email_count, contact_name fields?

Report:
- The full getThreads() method implementation
- How contact_name is assigned to threads (the bug)
- What fields each thread object contains
- How getContactsWithProfiles works (we already fixed this)
- Any SQL vs MongoDB routing issues in thread methods


Explore QueryAdapter thread methods


Perfect! I found the QueryAdapter.php file. Now let me analyze the key methods to provide you with a comprehensive report.

## Report: QueryAdapter Thread & Contact Fetching Analysis

### 1. **getThreads() Method Implementation**

**Location:** Lines 505-567 (SQL) and 569-650 (MongoDB)

**getThreadsSQL() - Lines 514-567:**
```php
private function getThreadsSQL(?int $contactId, string $search, int $limit, int $offset): array
{
    // Builds WHERE clause for filtering
    $where = 'e.thread_id IS NOT NULL AND e.thread_id != ""';
    // ... filter by contactId if provided
    // ... filter by search term if provided

    // Gets distinct thread count
    $total = $this->db->fetchColumn(
        "SELECT COUNT(DISTINCT e.thread_id) FROM emails e WHERE {$where}",
        $params
    );

    // Gets threads with aggregated data
    $threads = $this->db->fetchAll(
        "SELECT
            e.thread_id,
            MIN(e.id) as first_email_id,
            MAX(e.id) as last_email_id,
            MIN(e.subject) as subject,
            MIN(e.date) as first_date,
            MAX(e.date) as last_date,
            COUNT(*) as email_count,
            SUM(CASE WHEN e.direction = 'inbound' THEN 1 ELSE 0 END) as inbound_count,
            SUM(CASE WHEN e.direction = 'outbound' THEN 1 ELSE 0 END) as outbound_count,
            MAX(e.contact_id) as contact_id,
            MAX(c.name) as contact_name,  // BUG IS HERE!
            ta.is_analyzed,
            ta.needs_reanalysis
         FROM emails e
         LEFT JOIN contacts c ON e.contact_id = c.id
         LEFT JOIN thread_analysis ta ON e.thread_id = ta.thread_id
         WHERE {$where}
         GROUP BY e.thread_id
         ORDER BY MAX(e.date) DESC
         LIMIT ? OFFSET ?",
        array_merge($params, [$limit, $offset])
    );

    return ['threads' => $threads, 'total' => (int)$total];
}
```

**getThreadsMongo() - Lines 569-650:**
```php
private function getThreadsMongo(?int $contactId, string $search, int $limit, int $offset): array
{
    // Build match filter (correctly filters by contactId)
    $matchFilter = ['thread_id' => ['$ne' => null, '$ne' => '']];
    if ($contactId) {
        $matchFilter['contact_id'] = $contactId;
    }
    if ($search) {
        $matchFilter['subject'] = ['$regex' => $search, '$options' => 'i'];
    }

    // Count distinct threads
    $countPipeline = [
        ['$match' => $matchFilter],
        ['$group' => ['_id' => '$thread_id']],
        ['$count' => 'total']
    ];

    // Main aggregation pipeline
    $pipeline = [
        ['$match' => $matchFilter],
        ['$group' => [
            '_id' => '$thread_id',
            'first_email_id' => ['$min' => '$id'],
            'last_email_id' => ['$max' => '$id'],
            'subject' => ['$first' => '$subject'],
            'first_date' => ['$min' => '$date'],
            'last_date' => ['$max' => '$date'],
            'email_count' => ['$sum' => 1],
            'inbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'inbound']], 1, 0]]],
            'outbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'outbound']], 1, 0]]],
            'contact_id' => ['$max' => '$contact_id']  // Takes MAX contact_id
        ]],
        ['$lookup' => [
            'from' => 'contacts',
            'localField' => 'contact_id',
            'foreignField' => 'id',
            'as' => 'contact'
        ]],
        ['$lookup' => [
            'from' => 'thread_analysis',
            'localField' => '_id',
            'foreignField' => 'thread_id',
            'as' => 'analysis'
        ]],
        ['$project' => [
            '_id' => 0,
            'thread_id' => '$_id',
            'first_email_id' => 1,
            'last_email_id' => 1,
            'subject' => 1,
            'first_date' => 1,
            'last_date' => 1,
            'email_count' => 1,
            'inbound_count' => 1,
            'outbound_count' => 1,
            'contact_id' => 1,
            'contact_name' => ['$arrayElemAt' => ['$contact.name', 0]],
            'is_analyzed' => ['$arrayElemAt' => ['$analysis.is_analyzed', 0]],
            'needs_reanalysis' => ['$arrayElemAt' => ['$analysis.needs_reanalysis', 0]]
        ]],
        ['$sort' => ['last_date' => -1]],
        ['$skip' => $offset],
        ['$limit' => $limit]
    ];

    $threads = $mongo->aggregate('emails', $pipeline);

    return ['threads' => $threads, 'total' => $total];
}
```

---

### 2. **THE BUG: How Contact Names Are Assigned (Same Contact for All Threads)**

**Location:** Line 550 (SQL version)

**The Bug:**
```php
MAX(e.contact_id) as contact_id,
MAX(c.name) as contact_name,  // <- THIS IS THE BUG
```

**Why It's a Bug:**
When multiple emails in a thread have different contact_ids (which can happen in multi-participant threads or forwarded emails), the SQL query:
1. Groups by `e.thread_id` (one row per thread)
2. Takes `MAX(e.contact_id)` - picks the HIGHEST numeric contact_id
3. Takes `MAX(c.name)` - picks the HIGHEST alphabetically sorted contact name

**The Problem:**
- `MAX(c.name)` doesn't correspond to `MAX(e.contact_id)`
- The contact name comes from the last contact alphabetically, NOT the contact with the highest ID
- Multiple threads may pick the same "Max" contact name if they share the same highest-alphabetical-value contact

**Example:**
```
Thread A: emails from Alice (id=1), Bob (id=2), Charlie (id=3)
→ MAX(contact_id) = 3 (Charlie)
→ MAX(c.name) = "Charlie" ✓ (happens to match)

Thread B: emails from Alice (id=1), Bob (id=2), Dave (id=4)
→ MAX(contact_id) = 4 (Dave)
→ MAX(c.name) = "Dave" ✓ (happens to match)

Thread C: emails from Alice (id=1), Bob (id=2), Betty (id=5)
→ MAX(contact_id) = 5 (Betty)
→ MAX(c.name) = "Betty" BUT what if Betty's name alphabetically comes before the max?
```

**The Real Issue:** When you have multiple contacts in a thread, the query doesn't properly associate the contact name with the contact_id it selected. This could show the "wrong" contact for a thread that involves multiple people.

---

### 3. **What Fields Each Thread Object Contains**

**Returned from getThreads():**
- `thread_id` - Gmail thread ID
- `first_email_id` - ID of earliest email in thread
- `last_email_id` - ID of latest email in thread
- `subject` - Thread subject (from first email)
- `first_date` - Date of first email
- `last_date` - Date of most recent email
- `email_count` - Total emails in thread
- `inbound_count` - Count of inbound emails
- `outbound_count` - Count of outbound emails
- `contact_id` - The contact ID (MAX value for multi-contact threads)
- `contact_name` - The contact name (MISMATCHED due to bug)
- `is_analyzed` - Whether thread has analysis
- `needs_reanalysis` - Whether thread needs re-analysis

---

### 4. **getContactsWithProfiles() - Already Properly Fixed**

**Location:** Lines 65-186

**Status:** This method is CORRECTLY implemented for both SQL and MongoDB:

**SQL Version (lines 79-120):**
- Properly joins contacts with contact_profiles
- Uses parameterized queries for search
- Validates sort column
- Returns `['contacts' => $contacts, 'total' => $total]`

**MongoDB Version (lines 122-186):**
- Uses proper aggregation pipeline with `$lookup`
- Uses `$unwind` to flatten profile data
- Properly handles null profiles with `preserveNullAndEmptyArrays`
- Flattens profile fields correctly

**Key Pattern:** Both implementations correctly route based on `$this->useMongo` flag.

---

### 5. **SQL vs MongoDB Routing Issues Found**

**getThreadsSQL (Line 550) - BUG IDENTIFIED:**
Using `MAX(c.name)` is problematic because:
1. It doesn't align with `MAX(contact_id)`
2. In multi-contact threads, could return incorrect contact name
3. Should use a proper JOIN or subquery to get the correct contact for the dominant contact_id

**Correct Approach:**
Either:
1. **Option A:** Use a subquery to get contact name for the selected contact_id
2. **Option B:** Group by contact_id first, then pick top contact
3. **Option C:** Use FIRST_VALUE() window function to get first contact's name

**MongoDB (Lines 635) - CORRECT:**
```php
'contact_name' => ['$arrayElemAt' => ['$contact.name', 0]]
```
This correctly gets the name from the contact matched by `contact_id`.

---

### 6. **Other Thread Methods**

**getThreadDetails() (Lines 658-781):**
- Correctly implements SQL and MongoDB versions
- Joins contact info properly using `contact_id`
- No apparent bugs

**getThreadsWithContact() (Lines 472-494):**
- Simple method, correctly routes to MongoRepository or SQL
- Properly joins contacts using `contact_id`
- No bugs detected

**getThreadParticipants() (Lines 790-1162):**
- Complex but well-implemented for both SQL and MongoDB
- Properly extracts emails from FROM, TO, CC fields
- Correctly looks up contact data
- No routing issues

---

## Summary

**CRITICAL BUG FOUND:** In `getThreadsSQL()` at line 550, the query uses `MAX(c.name)` which can return the wrong contact name for threads involving multiple contacts. The MongoDB version correctly handles this with proper aggregation.

**Recommendation:** Fix the SQL version to properly match contact_name with contact_id, possibly by:
1. Using a subquery to get the contact details for the selected contact_id
2. Or using the same `$lookup` and `$arrayElemAt` pattern as MongoDB

agentId: a431619 (for resuming to continue this agent's work if needed)
<usage>total_tokens: 34355
tool_uses: 1
duration_ms: 24964</usage>

Prompt:
I need to understand how QueryAdapter fetches threads and contacts, specifically why all threads might show the same contact name. Also understand the getThreads method.

Explore in /var/www/mssql.danmarcrm.com/dev1/extension2v1/src/:

1. Look at `QueryAdapter.php` - find the methods: `getThreads()`, `getThreadDetails()`, `getContactsWithProfiles()`. For getThreads, look at how it assigns contact_name to each thread. Is there a bug where the same contact gets assigned to all threads?

2. Also look for any thread listing methods that might have SQL vs MongoDB routing issues (the same bug we found with contacts using raw SQL when data is in MongoDB).

3. Check what `getThreads()` returns - does it have subject, email_count, contact_name fields?

Report:
- The full getThreads() method implementation
- How contact_name is assigned to threads (the bug)
- What fields each thread object contains
- How getContactsWithProfiles works (we already fixed this)
- Any SQL vs MongoDB routing issues in thread methods


Perfect! Now I have all the information needed. Let me create a comprehensive report.

## COMPREHENSIVE ANALYSIS REPORT

### 1. HOW THREADS ARE LOADED AND WHY ALL MIGHT SHOW SAME CONTACT

**Threads Tab Loading** (`index.php` lines 195-205):
- Located in the **Threads tab** (called "emails" in the tab ID)
- Displays threads in a table format showing Subject, Contact, Emails count, Status, and Last Activity
- Default action: Click "Refresh" button to call `loadEmails()`

**Data Retrieval** (`api.php` case 'threads' lines 351-380):
- API endpoint fetches threads using `QueryAdapter::getThreads()`
- Parameters: `contactId` (optional), `search`, `limit` (default 20), `offset`
- Default pagination: **20 threads per page**

**Thread Contact Assignment** - **THE PROBLEM**:

The issue lies in `QueryAdapter.php` **line 549** in the `getThreadsSQL()` method:

```php
MAX(e.contact_id) as contact_id,
MAX(c.name) as contact_name,
```

And in MongoDB version (`getThreadsMongo()` line 609):

```php
'contact_id' => ['$max' => '$contact_id']
```

**Root Cause**:
- When a thread has multiple emails, they can be from **different senders/recipients**
- The SQL/MongoDB query uses `MAX(contact_id)` and `MAX(c.name)` to aggregate
- `MAX()` takes the **highest contact_id value**, not necessarily the actual thread participant
- If multiple contacts have email in the same thread, it will show whichever has the **largest numeric ID**

**Result**: All threads may show the same contact if they all happen to have the same highest contact_id (e.g., the last person to reply, or the contact with the highest ID number).

**Rendering** (`app.js` lines 674-692):
```javascript
html += `<td>${escapeHtml(thread.contact_name || 'Unknown')}</td>`;
```

---

### 2. HOW CONTACTS ARE LOADED AND DEFAULT LIMITS

**Contacts Tab Loading** (`index.php` lines 177-193):
- Default tab when page loads
- Has search input and type filter (All Types / Clients / Suppliers / Other)
- "Refresh" button triggers `loadContacts()`

**Data Retrieval** (`api.php` case 'contacts' lines 290-320):
- Fetches from `QueryAdapter::getContactsWithProfiles()`
- Query parameters:
  - `page`: Current page (default 1)
  - `search`: Search term
  - `type`: Contact type filter
  - **`limit`: Default 20, max 100** (line 292)
  - `offset`: Calculated as `(page - 1) * limit`
  - `sort`: Column to sort by (default 'name')
  - `order`: ASC or DESC (default DESC)

**Frontend Loading** (`app.js` lines 159-192):
```javascript
const data = await apiCall('contacts', {
    page: page,
    search: state.contacts.search,
    type: state.contacts.type,
    limit: 20  // <-- HARDCODED DEFAULT
});
```

**Default Load Limits**:
- **Contacts: 20 per page**
- **Threads: 20 per page**

---

### 3. AI THREAD ANALYSIS TAB AND SLOWNESS CAUSES

**AI Thread Analysis Section** (`index.php` lines 136-161):
- Located near the top of the page, **above the main tabs**
- NOT a separate tab, but a **dedicated status section** with:
  - 4 stat cards showing: Total Threads, Analyzed count, Pending count, Progress %
  - Action buttons: "Analyze Threads" and "Refresh Status"
  - Recent analysis list (div id="recent-analysis")

**What Gets Loaded** (`app.js` lines 1516-1566 `refreshAnalysisStatus()`):

On page load (DOMContentLoaded), this function runs immediately and then **every 30 seconds**:

```javascript
refreshAnalysisStatus();
setInterval(refreshAnalysisStatus, 30000);
```

**API Call** (`api.php` case 'analysis_status' lines 990-1054):

```php
// Count ALL distinct threads from emails table
SELECT COUNT(DISTINCT thread_id) FROM emails

// Count analyzed threads
SELECT COUNT(*) FROM thread_analysis WHERE is_analyzed = 1

// Get recent analysis (5 threads)
SELECT ta.*, e.subject, c.name FROM thread_analysis ta
LEFT JOIN emails e ON ta.last_email_id = e.id
LEFT JOIN contacts c ON ta.contact_id = c.id
WHERE ta.is_analyzed = 1 ORDER BY ta.analyzed_at DESC LIMIT 5
```

Then for each recent thread, it runs **another query** per thread (line 1021-1043):
```php
foreach ($recentAnalysis as &$thread) {
    $analysis = $db->fetchOne(
        "SELECT summary, sentiment_label, deception_score, raw_analysis
         FROM email_analysis ea
         JOIN emails e ON ea.email_id = e.id
         WHERE e.thread_id = ?
         ORDER BY ea.analyzed_at DESC LIMIT 1",
        [$thread['thread_id']]
    );
}
```

**PERFORMANCE ISSUES CAUSING SLOWNESS**:

1. **N+1 Query Problem** (lines 1021-1030):
   - Fetches 5 recent threads, then queries **5 additional times** (once per thread) for analysis details
   - Should be a single JOIN instead

2. **Unindexed COUNT(DISTINCT thread_id)** (line 992-993):
   - Counts ALL distinct threads every 30 seconds
   - If thousands of emails exist, this full table scan is expensive

3. **Auto-refresh every 30 seconds** (line 1593):
   - The `setInterval(refreshAnalysisStatus, 30000)` runs whether user is viewing the page or not
   - Generates continuous database traffic

4. **Multiple LEFT JOINs** (lines 1012-1014):
   - Three LEFT JOINs on every call to get recent analysis

---

### 4. WHAT GETS LOADED ON PAGE INIT (DOMContentLoaded)

**File: `index.php` lines 628-639**:
```php
document.addEventListener('DOMContentLoaded', function() {
    // Set up tab switching
    document.querySelectorAll('.tab-btn').forEach(btn => {
        btn.addEventListener('click', function() {
            switchTab(this.dataset.tab);
        });
    });

    // Load initial data
    loadContacts();
    loadFilters();
});
```

**File: `app.js` DOMContentLoaded (lines 1570-1604)**:
```javascript
// Event listeners for search and filters
- Contact search input (300ms debounce)
- Contact type filter
- Quote outcome filter
- Semantic search input

// AUTO-RUN IMMEDIATELY:
refreshAnalysisStatus();  // <-- HEAVY: Counts all threads + 5 detail queries

// AUTO-RUN EVERY 30 SECONDS:
setInterval(refreshAnalysisStatus, 30000);

// AI Chat initialization (another DOMContentLoaded block at line 3409)
initAiProviderDropdown();
```

**And in `app.js` (line 3409-3418)** for AI Chat:
```javascript
document.addEventListener('DOMContentLoaded', () => {
    initAiProviderDropdown();
    // ... draggable panel setup
});
```

**Initial Page Load Summary**:

| Action | Function | Cost | Runs On Init? |
|--------|----------|------|---------------|
| Load Contacts | `loadContacts()` | API call + render 20 contacts | **YES** |
| Load Filters | `loadFilters()` | API call + render | **YES** |
| Analysis Status | `refreshAnalysisStatus()` | 7+ database queries (N+1) | **YES** |
| Auto-refresh Analysis | `setInterval(..., 30s)` | Repeats every 30 seconds | **YES** |
| Tab listeners | `addEventListener` | DOM setup only | YES |
| AI chat init | `initAiProviderDropdown()` | DOM/JS setup | YES |

**SLOW PERFORMANCE SOURCES**:

1. **Initial load**: `loadContacts()` + `loadFilters()` + `refreshAnalysisStatus()` = **3 API calls + rendering**
2. **After 30 seconds**: `refreshAnalysisStatus()` fires automatically (even if not viewed)
3. **If user clicks Threads tab**: `loadEmails()` triggers (another API call + rendering)

---

## SUMMARY TABLE

| Item | Finding |
|------|---------|
| **Thread Contact Issue** | Uses `MAX(contact_id)` which returns highest ID, not actual thread contact - causes all threads to show same contact if they share emails with high-ID contact |
| **Default Limits** | Contacts: 20/page, Threads: 20/page |
| **AI Analysis Tab** | Section above main tabs (not a tab itself) that loads stats + 5 recent analyses |
| **N+1 Query Problem** | `analysis_status` fetches 5 threads, then queries 5 times more (once per thread) for analysis details - should be one JOIN |
| **30-Second Auto-Refresh** | `refreshAnalysisStatus()` fires every 30 seconds automatically, counting all threads |
| **Page Init Load** | Runs `loadContacts()` + `loadFilters()` + `refreshAnalysisStatus()` immediately on DOMContentLoaded |
| **Slowness Culprits** | Unindexed `COUNT(DISTINCT thread_id)`, N+1 queries for recent analysis, 3 LEFT JOINs per call |

agentId: a6a88a1 (for resuming to continue this agent's work if needed)
<usage>total_tokens: 55137
tool_uses: 19
duration_ms: 64065</usage>

Prompt:
I need to understand how the main index page loads threads and contacts, and why threads might all show the same contact name, and why the AI Thread Analysis tab might be slow.

Explore these files in /var/www/mssql.danmarcrm.com/dev1/extension2v1/public/:

1. Look at `index.php` - find the "threads" tab section and "AI Thread Analysis" tab section. How are threads listed? Is there a tab for AI Thread Analysis?

2. Look at `api.php` - find the `threads` case (action=threads). How does it fetch threads? Look at the SQL/MongoDB query - why might all threads show the same contact name? Also find the `contacts` case and see how many records it loads by default.

3. Look at `api.php` for the `analysis_stats` or `analysis_queue` or any analytics/dashboard stats cases - these could be slowing the page load.

4. Look at `assets/app.js` - find the `loadThreads()` function and `loadContacts()` function. How many records do they request? What is loaded on DOMContentLoaded?

Report back:
- How threads are loaded and why all might show same contact
- How contacts are loaded and default limits
- What the AI Thread Analysis tab loads and how it could be slow
- What gets loaded on page init (DOMContentLoaded)


Perfect! Now I have a comprehensive understanding. Let me compile the complete report:

## COMPREHENSIVE REPORT: AI Response Handling & Multi-Provider Support

Based on my exploration of the codebase, here's the complete flow and architecture:

---

### 1. **SAVE_AI_RESPONSE FLOW & MONGODB COLLECTIONS**

**Note:** The `save_ai_response` endpoint does NOT exist as a direct case in `api.php`. Instead, AI responses are **automatically persisted** through these mechanisms:

#### **A. AI Chat Session Persistence (MongoDB)**
**Case: `ai_chat` (lines 2278-2625 in `/var/www/mssql.danmarcrm.com/dev1/extension2/public/api.php`)**

- **Collection:** `ai_chat_sessions`
- **Flow:**
  1. Frontend sends `POST /api.php?action=ai_chat` with: `message`, `provider`, `model`, `agent`, `thread_ids[]`, `contact_id`, `session_id`
  2. API routes through proxy at `https://mssql.danmarcrm.com/dev1/extension2/aiapi/api_codex51/api/index.php`
  3. Proxy returns response with answer, model used, cost
  4. **Auto-saved** to MongoDB at lines 2601-2613:
     ```php
     $ctxMongo->insertOrUpdate('ai_chat_sessions', [
         'session_id' => $chatSessionId,          // Unique session ID
         'created_at' => date('Y-m-d H:i:s'),
         'updated_at' => date('Y-m-d H:i:s'),
         'title' => $sessionTitle,
         'agent' => $chatAgent ?? $chatMode,
         'provider' => $chatProvider,
         'model' => $usedModel,
         'context_thread_ids' => $resolvedThreadIds,
         'contact_id' => $chatContactId ?: null,
         'contact_name' => $ctxContact['name'] ?? null,
         'messages' => $sessionMessages,  // Array of [{role, content, timestamp, model}]
     ], ['session_id']);  // Upsert on session_id
     ```

#### **B. AI Contact Memory (Auto-Extraction)**
**Collections:**
- `ai_contact_memory` — Persistent facts extracted by agents with `autoMemory: true`
- Auto-extraction happens at lines 2550-2584 when agent has `autoMemory: true`
- Extracts facts wrapped in `[MEMORY]fact[/MEMORY]` tags from AI response
- Stores: `contact_id`, `memories` array with `{id, fact, source:'auto'|'manual', agent, session_id, created_at}`

---

### 2. **HOW api_codex51 HANDLES MULTI-PROVIDER REQUESTS**

**Primary Proxy Location:** `/var/www/mssql.danmarcrm.com/dev1/extension2/aiapi/api_codex51/api/index.php`

#### **Request Format (from ai_chat case)**
```php
$proxyPayload = json_encode([
    'action'       => 'ask',
    'user_content' => $chatMessage,
    'system'       => $sysMsg,  // Full system prompt with context
    'models'       => [$chatProvider => $chatModel],  // KEYED BY PROVIDER (critical!)
    'providers'    => [$chatProvider],
    'history'      => array_slice($chatHistory, -10),
]);
```

#### **Proxy Processing (AIOrchestrator, lines 54-99)**
```php
public function ask(string $prompt, array $modelOverrides = [], array $providerSelection = [],
                    string $system = '', array $history = []): array
{
    $responses = [];
    $modelsUsed = [];
    $totalCost = 0.0;

    // Loop through each provider
    foreach ($this->providers as $code => $provider) {
        // Skip if not selected
        if (!empty($providerSelection) && !in_array($code, $providerSelection, true)) {
            continue;
        }

        // Rate limiting check
        if ($this->isRateLimited($code)) {
            $responses[$code . '_response'] = ['error' => 'Rate limited...'];
            continue;
        }

        // Get model override or use default
        $model = $modelOverrides[$code] ?? $this->providersConfig[$code]['default_model'];
        $modelsUsed[$code] = $model;

        // Send to provider
        $result = $provider->send($prompt, $model, $system, $history);
        $this->lastCalls[$code] = microtime(true);

        $responses[$code . '_response'] = $result;
    }

    // Store in ResponseCache (JSON file at /aiapi/api_codex51/data/queries.json)
    $entry = [
        'id' => uniqid('qa_'),
        'user_content' => $prompt,
        'models' => $modelsUsed,
        'responses' => $responses,
        'total_cost' => round($totalCost, 6),
        'created_at' => time(),
    ];

    if (AI_FEATURE_FLAGS['cache_enabled'] ?? true) {
        $this->cache->store($entry);  // Saves to JSON file
    }

    return $entry;
}
```

#### **Key Points:**
- **Models keyed by provider:** `['openai' => 'gpt-4o']` NOT `['gpt-4o']`
- **Response naming:** Each provider response keyed as `{provider}_response` (e.g., `openai_response`)
- **Rate limiting:** Per-provider in-memory tracking with configurable limits
- **Cost calculation:** Extracts actual usage from provider API responses or estimates

---

### 3. **REQUEST/RESPONSE FORMAT FOR THE PROXY**

#### **POST to api_codex51/api/index.php**
```json
{
  "action": "ask",
  "user_content": "What are the key risks in this thread?",
  "system": "You are a deception detector...\n\nCONTEXT:\n=== EMAIL THREAD (3 emails) ===\n...",
  "models": {
    "openai": "gpt-4o",
    "claude": "claude-opus-4-5-20251101"
  },
  "providers": ["openai", "claude"],
  "history": [
    {"role": "user", "content": "Previous question"},
    {"role": "assistant", "content": "Previous answer"}
  ]
}
```

#### **Response from api_codex51**
```json
{
  "success": true,
  "id": "qa_507b46d7d23b8",
  "responses": {
    "openai_response": {
      "response": "The AI answer from OpenAI...",
      "model": "gpt-4o",
      "latency": 2.34,
      "cost": {
        "input_tokens": 1234,
        "output_tokens": 567,
        "usd": 0.0123
      },
      "raw": { ... }
    },
    "claude_response": {
      "response": "The AI answer from Claude...",
      "model": "claude-opus-4-5-20251101",
      "latency": 1.89,
      "cost": { ... }
    }
  },
  "models": {
    "openai": "gpt-4o",
    "claude": "claude-opus-4-5-20251101"
  },
  "total_cost": 0.0234,
  "created_at": 1708451120
}
```

#### **Provider Classes** (at `/var/www/mssql.danmarcrm.com/dev1/extension2/aiapi/api_codex51/src/Providers.php`):
- `OpenAIProvider` → `https://api.openai.com/v1/chat/completions`
- `ClaudeProvider` → `https://api.anthropic.com/v1/messages`
- `GeminiProvider` → `https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent`
- `GrokProvider` → `https://api.x.ai/v1/chat/completions`
- `DeepSeekProvider` → Similar pattern

---

### 4. **EXISTING LOGGING MECHANISMS**

#### **A. AI Analysis Logging** (`/var/www/mssql.danmarcrm.com/dev1/extension2/logs/ai_analysis/`)
- **Directory structure:** `ai_analysis/{YYYY-MM-DD}/{HH-MM-SS}_{thread_id}.txt`
- **Summary file:** `ai_analysis/summary.log` — One-liner per analysis
- **Format:** Thread analysis log with full prompt, response, timing, token usage
- **Current logs:** 6 files from Dec 25, 2025 to Feb 18, 2026
- **Entry example:**
  ```
  2026-02-18 22:15:43 | Thread: 19ae5c0cb75e381c | Provider: openai | Time: 64.14s | Tokens: 0 | File: /var/www/mssql.danmarcrm.com/dev1/extension2/logs/ai_analysis/2026-02-18/22-15-43_19ae5c0cb75e381c.txt
  ```

#### **B. Prompt Logging** (`/var/www/mssql.danmarcrm.com/dev1/extension2/logs/prompt.log`)
- **Format:** `Date: [timestamp] [RequestID: xxx] [ThreadID: yyy] - Prompt: [full prompt text]`
- **Purpose:** Audit trail of all prompts sent to AI
- **Size:** 274KB (as of Feb 18)

#### **C. MongoDB Connection Logging** (`/var/www/mssql.danmarcrm.com/dev1/extension2/logs/mongodb.log`)
- **Format:** `[YYYY-MM-DD HH:MM:SS] [LEVEL]: [message]`
- **Purpose:** Connection health, insertOrUpdate operations
- **Size:** 2.6MB (as of Feb 20)

#### **D. Exception Logging** (`/var/www/mssql.danmarcrm.com/dev1/extension2/logs/exceptions.log`)
- **Format:** Uncaught exceptions with stack traces
- **Size:** 24KB

#### **E. Database Operations** (`/var/www/mssql.danmarcrm.com/dev1/extension2/logs/database.log`)
- **Format:** Database queries and operations
- **Size:** 52KB

#### **F. Thread Analyzer** (`/var/www/mssql.danmarcrm.com/dev1/extension2/logs/thread_analyzer.log`)
- **Purpose:** Specific to ThreadAnalyzer class operations

#### **G. API Logs** (`/var/www/mssql.danmarcrm.com/dev1/extension2/public/apiLogs.log`)
- **Auto-recorded at line 33 of api.php:** All `$_REQUEST` parameters

---

### 5. **MONGODB COLLECTIONS FOR AI**

| Collection | Purpose | Key Fields |
|-----------|---------|-----------|
| `ai_chat_sessions` | Chat session persistence | `session_id`, `messages[]`, `agent`, `provider`, `model`, `context_thread_ids`, `contact_id` |
| `ai_contact_memory` | Persistent facts about contacts | `contact_id`, `memories[]` (each with `id`, `fact`, `source`, `agent`, `created_at`) |
| `ai_analysis_cache` | (Potentially) cached analyses | Thread ID keyed |

---

### 6. **TOKEN BUDGET & CONTEXT MANAGEMENT**

**In ai_chat case (lines 2352-2365):**
```php
$modelTokenLimits = [
    'gpt-4o' => 128000,
    'claude-opus-4-5-20251101' => 200000,
    'gemini-2.5-flash' => 1000000,
    // ... etc
];

$tokenLimit = $modelTokenLimits[$chatModel] ?? 128000;
$contextTokenBudget = (int)($tokenLimit * 0.6);  // 60% for context
```

- If context exceeds budget, it's truncated from the beginning, keeping recent content
- `stripQuotedContent()` removes duplicate quoted replies (60-80% savings)
- Returns `context_stats` with `truncated` flag

---

### 7. **KEY ARCHITECTURAL PATTERNS**

1. **Two-tier AI proxy system:**
   - `api_codex51` (main orchestrator for multi-provider queries)
   - `api_opus` (secondary query/analysis endpoint)

2. **MongoDB-first for persistence:**
   - Chat sessions auto-saved with `insertOrUpdate()`
   - Contact memories extracted and stored
   - No save_ai_response endpoint needed — auto-save on every ai_chat call

3. **Rate limiting:**
   - Per-provider in api_codex51
   - Configurable via AI_PROVIDERS_CONFIG

4. **Cost tracking:**
   - Per-response cost calculation
   - Total cost aggregated
   - Actual tokens from provider APIs preferred over estimates

---

### 8. **FILES INVOLVED**

**Core API:**
- `/var/www/mssql.danmarcrm.com/dev1/extension2/public/api.php` — Main REST endpoint, ai_chat case at line 2278

**Proxy Orchestrators:**
- `/var/www/mssql.danmarcrm.com/dev1/extension2/aiapi/api_codex51/api/index.php` — Multi-provider gateway
- `/var/www/mssql.danmarcrm.com/dev1/extension2/aiapi/api_codex51/src/AIOrchestrator.php` — Request routing & caching
- `/var/www/mssql.danmarcrm.com/dev1/extension2/aiapi/api_codex51/src/Providers.php` — Provider implementations (OpenAI, Claude, Gemini, Grok, DeepSeek)
- `/var/www/mssql.danmarcrm.com/dev1/extension2/aiapi/api_codex51/src/ResponseCache.php` — JSON file-based query history

**Query Proxy:**
- `/var/www/mssql.danmarcrm.com/dev1/extension2/aiapi/api_opus/api/query.php` — Alternative endpoint
- `/var/www/mssql.danmarcrm.com/dev1/extension2/aiapi/api_opus/src/AIManager.php` — AI provider management
- `/var/www/mssql.danmarcrm.com/dev1/extension2/aiapi/api_opus/src/CacheManager.php` — JSON file caching

**Logging:**
- `/var/www/mssql.danmarcrm.com/dev1/extension2/logs/ai_analysis/` — Thread analysis logs
- `/var/www/mssql.danmarcrm.com/dev1/extension2/logs/prompt.log` — All prompts sent
- `/var/www/mssql.danmarcrm.com/dev1/extension2/logs/mongodb.log` — MongoDB operations

agentId: a11985d (for resuming to continue this agent's work if needed)
<usage>total_tokens: 64047
tool_uses: 33
duration_ms: 73027</usage>

Prompt:
I need to understand how the existing thread_view.php sends requests to the AI proxy, and how the save_ai_response endpoint works. Also explore the AI proxy endpoint to understand multi-provider support.

Explore in /var/www/mssql.danmarcrm.com/dev1/extension2v1/:

1. In `public/api.php` - find the `save_ai_response` case and `get_ai_response` and `list_ai_responses` cases. How are AI responses saved/loaded?

2. Look at `aiapi/api_opus/api/query.php` - how does this proxy work? How does it handle multiple providers in one request? What does the request/response format look like?

3. Look at `aiapi/api_opus/api/` directory for any other relevant files (like providers list endpoint).

4. Check if there's a `logs/` directory already or any existing logging mechanism.

Report:
- The full save_ai_response flow and MongoDB collection used
- How api_opus query.php handles multi-provider requests
- The request/response format for the proxy
- Any existing logging mechanisms


Excellent. I now have a thorough understanding of the codebase. Let me also check a few specifics and then design the plan.


Design implementation plan

Prompt:
Based on the following exploration findings, design a detailed implementation plan. The user wants to enhance the existing `thread_view.php` page AND fix several issues on the main `index.php` page.

## Context from exploration:

### Files:
- `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/thread_view.php` — Full-page thread view (2033 lines), has multi-provider AI, conversation history, saved responses
- `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/api.php` — Backend REST API, already has ai_chat case with agents/sessions/memory + save_ai_response endpoint
- `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/assets/app.js` — Main app JS, has AI chat panel code appended at end
- `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/index.php` — Main dashboard with tabs
- `/var/www/mssql.danmarcrm.com/dev1/extension2v1/src/QueryAdapter.php` — Data access layer

### Existing thread_view.php architecture:
- PHP top: Fetches thread data from QueryAdapter, builds participant map, gets saved responses from MongoDB `ai_query_responses` collection
- Layout: 2-column grid (left: email timeline + responses, right: AI form 400px)
- AI form: Multi-provider selection via checkboxes (loads from api_opus/query.php?action=providers), content processing mode (full/stripped/smart), question textarea, context textarea
- Sends to: `../aiapi/api_opus/api/query.php` (multi-provider proxy that sends to ALL selected providers simultaneously)
- Saves responses to MongoDB via `api.php?action=save_ai_response`
- Has conversation history in sessionStorage
- Has saved response quick view modal

### What the user wants (7 tasks):

**TASK 1: Enhance thread_view.php**
- Full page width (currently max-width: 1600px — just needs widening)
- Keep multi-provider selection (send to multiple AI simultaneously)
- Add General/Quotes/Deception/Profiler/Composer agent buttons with sub-buttons (quick prompts)
- Add search by email/thread with + buttons (context builder from the floating panel)
- Add estimated tokens display and enforce max
- Keep results saved (existing save_ai_response works)
- Keep discussing after initial result (conversation history exists)
- Add file logging of everything sent/received for debug

**TASK 2: Speed up main page**
- Load fewer threads on main page
- Load fewer contacts (already 20, could be 10)
- Threads should load faster

**TASK 3: Fix thread contact name bug**
- In QueryAdapter::getThreadsSQL(), `MAX(c.name)` returns wrong contact
- In MongoDB version, `'contact_id' => ['$max' => '$contact_id']` also wrong
- All threads show same contact name (e.g., "Etienne Turner")
- Fix: For each thread, find the most FREQUENT external sender (not user's own email), use THAT as contact

**TASK 4: Fix/hide AI Thread Analysis tab**
- The analysis_status endpoint is heavy (N+1 queries, COUNT DISTINCT, runs every 30s)
- Recent analysis shows "Unknown" for contact names
- User doesn't find it useful
- Solution: Hide it entirely and stop the auto-refresh interval

### Key constraints:
- Data lives in MongoDB (not SQL) — always use QueryAdapter
- `MongoDatabase::insertOrUpdate()` for upserts
- Node.js on server is v10.19 — no ES6+ arrow syntax in server-side checks
- The AI proxy at `aiapi/api_opus/api/query.php` handles multi-provider. The codex51 proxy handles single-provider.
- For thread_view.php: Keep using api_opus proxy for multi-provider support (NOT ai_chat endpoint which is single-provider)
- Agent system prompts need to be injected before sending to the proxy

### Design the plan for each task with specific code changes.

For TASK 1 (thread_view.php enhancement), the key architectural decisions:
- Keep the existing multi-provider flow (api_opus proxy)
- Add agent presets as JS constants (same 5 agents from app.js)
- Agent sets a system prompt that gets prepended to the content
- Add context builder: search input calls api.php?action=contacts and api.php?action=threads, results show + Add buttons, added threads get fetched and included in content
- Token estimation: calculate content length / 3.5, show in UI, check against model limits per provider
- File logging: Add a PHP endpoint `log_ai_debug` that appends to `/var/www/mssql.danmarcrm.com/dev1/extension2v1/logs/ai_debug.log`

For TASK 3 (contact name fix), the approach:
- In getThreadsMongo(): Instead of `$max` on contact_id, use a more complex aggregation that groups by contact_id, counts frequency, and picks the most frequent one (excluding user's own email)
- In getThreadsSQL(): Use a subquery or different approach to get the most frequent contact per thread

Please provide a detailed implementation plan with specific file changes, function modifications, and code patterns to use.


Design implementation plan


I now have comprehensive understanding of all four areas. Let me compile the detailed implementation plan.

---

## DETAILED IMPLEMENTATION PLAN

### TASK 1: Enhance thread_view.php

This is the largest task. The existing `thread_view.php` (2033 lines) has a solid foundation: multi-provider selection via checkboxes, conversation history in sessionStorage, saved responses in MongoDB, and a quick view modal. The enhancements add: full-width layout, agent buttons with quick prompts, a context builder (search + add threads), token estimation, and file-based debug logging.

#### 1A. Full-width layout

**File:** `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/thread_view.php`

**Change at line 174:** Change `.page-container` max-width from `1600px` to `100%` with padding:

```css
.page-container {
    max-width: 100%;
    margin: 0 auto;
    padding: 20px 30px;
}
```

**Change at line 213-214:** Change `.main-grid` from `1fr 400px` to `1fr 500px` to give the right panel more room for agents and context builder:

```css
.main-grid {
    display: grid;
    grid-template-columns: 1fr 500px;
    gap: 20px;
}
```

#### 1B. Agent buttons with quick prompts

**File:** `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/thread_view.php`

**Add JS constant** (after `const emailContent = ...` around line 1059). Copy the `AI_AGENTS` definition from `app.js` (lines 3272-3278) and also define agent system prompts matching what `api.php` has at lines 2288-2308:

```javascript
const TV_AGENTS = {
    general: {
        id: 'general', name: 'General', icon: '💬',
        systemPrompt: 'You are a business intelligence assistant...',
        quickPrompts: ['Summarize this conversation', 'What are the key risks?', 'What action should I take next?']
    },
    quote_analyst: {
        id: 'quote_analyst', name: 'Quotes', icon: '💰',
        systemPrompt: 'You are a pricing and quote analyst...',
        quickPrompts: ['How much have we quoted?', 'Price sensitivity?', 'Optimal pricing?', 'Discounts given?']
    },
    deception_detector: {
        id: 'deception_detector', name: 'Deception', icon: '🔍',
        systemPrompt: 'You are a deception and inconsistency analyst...',
        quickPrompts: ['Deception level?', 'Contradictions?', 'Red flags?', 'Rate honesty 1-10']
    },
    contact_profiler: {
        id: 'contact_profiler', name: 'Profiler', icon: '👤',
        systemPrompt: 'You are a contact intelligence profiler...',
        quickPrompts: ['Build complete profile', 'Communication style', 'Decision patterns', 'How to approach?']
    },
    email_composer: {
        id: 'email_composer', name: 'Composer', icon: '✍',
        systemPrompt: 'You are a professional email composer...',
        quickPrompts: ['Professional reply', 'Firm but polite', 'Follow-up email', 'Brief acknowledgment']
    },
};
let selectedAgent = 'general';
```

**Add HTML** -- insert a new `.card` inside the right-column (before the "Ask AI" card, around line 970). The agent buttons row plus quick-prompt sub-buttons:

```html
<!-- Agent Selection -->
<div class="card">
    <div class="card-header">
        <h2>AI Agent</h2>
    </div>
    <div class="agent-buttons" id="agent-buttons">
        <!-- Rendered by JS -->
    </div>
    <div class="quick-prompts" id="quick-prompts" style="margin-top: 10px;">
        <!-- Sub-buttons rendered by JS when agent selected -->
    </div>
</div>
```

**Add CSS for agents** (in the `<style>` block):

```css
.agent-buttons {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
}
.agent-btn {
    padding: 8px 14px;
    border: 2px solid var(--border-color);
    border-radius: 8px;
    background: #f9fafb;
    cursor: pointer;
    font-size: 0.85em;
    transition: all 0.2s;
}
.agent-btn.active {
    background: rgba(102, 126, 234, 0.15);
    border-color: var(--accent-primary);
    font-weight: 600;
}
.quick-prompts {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
}
.quick-prompt-btn {
    padding: 4px 10px;
    border: 1px solid #d1d5db;
    border-radius: 12px;
    background: white;
    font-size: 0.8em;
    cursor: pointer;
    transition: all 0.2s;
}
.quick-prompt-btn:hover {
    background: var(--accent-primary);
    color: white;
    border-color: var(--accent-primary);
}
```

**Add JS functions** to render agents and handle selection:

```javascript
function renderAgents() {
    const container = document.getElementById('agent-buttons');
    container.innerHTML = Object.values(TV_AGENTS).map(agent =>
        `<button class="agent-btn ${agent.id === selectedAgent ? 'active' : ''}"
                 onclick="selectTVAgent('${agent.id}')">
            ${agent.icon} ${agent.name}
        </button>`
    ).join('');
    renderQuickPrompts();
}

function selectTVAgent(agentId) {
    selectedAgent = agentId;
    renderAgents();
}

function renderQuickPrompts() {
    const container = document.getElementById('quick-prompts');
    const agent = TV_AGENTS[selectedAgent];
    container.innerHTML = agent.quickPrompts.map(prompt =>
        `<button class="quick-prompt-btn" onclick="useQuickPrompt(this.textContent.trim())">${prompt}</button>`
    ).join('');
}

function useQuickPrompt(prompt) {
    document.getElementById('ai-question').value = prompt;
}
```

**Modify `submitToAI()`** (around line 1488) to inject the agent's system prompt into the request. The api_opus proxy accepts `system_prompt` (confirmed at line 77 of query.php). Add the system prompt to the requestBody:

```javascript
// In submitToAI(), before building requestBody (~line 1567):
const agent = TV_AGENTS[selectedAgent];
const systemPrompt = agent.systemPrompt || '';

// Line ~1568: Add system_prompt to requestBody
const requestBody = {
    user_content: fullContent,
    providers: selectedProvidersList,
    system_prompt: systemPrompt  // <-- NEW
};
```

Also update the save call to include the agent:

```javascript
// In the saveAIResponse call (~line 1611), add:
agent: selectedAgent,
```

#### 1C. Context builder (search + add threads)

**File:** `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/thread_view.php`

**Add HTML** -- a new `.card` in the right column (after agents, before the Ask AI card):

```html
<!-- Context Builder -->
<div class="card">
    <div class="card-header">
        <h2>Context Builder</h2>
        <span id="context-summary" style="font-size: 0.85em; color: var(--success);"></span>
    </div>
    <div class="ai-form-section">
        <div style="display: flex; gap: 8px;">
            <input type="text" id="context-search-input" placeholder="Search contacts or threads..."
                   onkeydown="if(event.key==='Enter')searchContext()">
            <button class="btn btn-secondary" onclick="searchContext()" style="padding: 8px 16px;">Search</button>
        </div>
    </div>
    <div id="context-search-results" style="max-height: 200px; overflow-y: auto;"></div>
    <div id="context-threads-list" style="margin-top: 10px;"></div>
</div>
```

**Add CSS** for context builder items:

```css
.context-result-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 6px 10px;
    border-radius: 6px;
    font-size: 0.85em;
    cursor: pointer;
}
.context-result-item:hover {
    background: #f0f0f5;
}
.context-thread-pill {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    background: rgba(102, 126, 234, 0.1);
    border: 1px solid var(--accent-primary);
    border-radius: 8px;
    padding: 4px 10px;
    font-size: 0.8em;
    margin: 3px;
}
.context-thread-pill .remove-btn {
    cursor: pointer;
    color: var(--error);
    font-weight: bold;
    margin-left: 4px;
}
```

**Add JS** for the context builder. This reuses the same `api.php?action=contacts` and `api.php?action=threads` endpoints that `app.js` context builder uses:

```javascript
let contextThreads = []; // [{thread_id, subject, email_count}]

async function searchContext() {
    const query = document.getElementById('context-search-input').value.trim().replace(/[*%]/g, '');
    if (!query) return;

    const resultsDiv = document.getElementById('context-search-results');
    resultsDiv.innerHTML = '<p style="color: var(--text-secondary); font-size: 0.85em;">Searching...</p>';

    try {
        const [contactsData, threadsData] = await Promise.all([
            fetch(`api.php?action=contacts&search=${encodeURIComponent(query)}&limit=5`).then(r => r.json()),
            fetch(`api.php?action=threads&search=${encodeURIComponent(query)}&limit=10`).then(r => r.json())
        ]);

        let html = '';
        // Contacts section
        if (contactsData.contacts && contactsData.contacts.length > 0) {
            html += '<div style="font-weight:600; font-size:0.8em; color:var(--accent-primary); margin:8px 0 4px;">CONTACTS</div>';
            contactsData.contacts.forEach(c => {
                html += `<div class="context-result-item" onclick="loadContactThreads(${c.id}, '${escapeHtml(c.name||c.email)}')">
                    <span>👤 ${escapeHtml(c.name||'')} <span style="color:var(--text-secondary)">${escapeHtml(c.email)}</span></span>
                    <span style="color:var(--accent-primary); font-size:0.85em;">show threads ▶</span>
                </div>`;
            });
        }
        // Threads section
        if (threadsData.threads && threadsData.threads.length > 0) {
            html += '<div style="font-weight:600; font-size:0.8em; color:var(--accent-primary); margin:8px 0 4px;">THREADS</div>';
            threadsData.threads.forEach(t => {
                const isAdded = contextThreads.some(ct => ct.thread_id === t.thread_id);
                html += `<div class="context-result-item">
                    <span>📧 ${escapeHtml((t.subject||'(No Subject)').substring(0,50))} (${t.email_count||0})</span>
                    <button class="btn btn-secondary" style="padding:3px 10px; font-size:0.8em;"
                            onclick="addContextThread('${t.thread_id}', '${escapeHtml((t.subject||'').substring(0,50))}', ${t.email_count||0})"
                            ${isAdded ? 'disabled' : ''}>
                        ${isAdded ? '✓ Added' : '+ Add'}
                    </button>
                </div>`;
            });
        }
        if (!html) html = '<p style="color:var(--text-secondary); font-size:0.85em;">No results found</p>';
        resultsDiv.innerHTML = html;
    } catch (err) {
        resultsDiv.innerHTML = '<p style="color:var(--error); font-size:0.85em;">Error: ' + err.message + '</p>';
    }
}

function addContextThread(threadId, subject, emailCount) {
    if (contextThreads.some(t => t.thread_id === threadId)) return;
    contextThreads.push({ thread_id: threadId, subject: subject, email_count: emailCount });
    renderContextThreads();
    updateTokenEstimate();
}

function removeContextThread(threadId) {
    contextThreads = contextThreads.filter(t => t.thread_id !== threadId);
    renderContextThreads();
    updateTokenEstimate();
}

function renderContextThreads() {
    const list = document.getElementById('context-threads-list');
    const summary = document.getElementById('context-summary');
    if (contextThreads.length === 0) {
        list.innerHTML = '';
        summary.textContent = '';
        return;
    }
    const totalEmails = contextThreads.reduce((sum, t) => sum + (t.email_count || 0), 0);
    summary.textContent = `${contextThreads.length} threads, ~${totalEmails} emails`;
    list.innerHTML = contextThreads.map(t =>
        `<span class="context-thread-pill">
            📧 ${escapeHtml(t.subject.substring(0,30))} (${t.email_count})
            <span class="remove-btn" onclick="removeContextThread('${t.thread_id}')">✕</span>
        </span>`
    ).join('');
}
```

**Modify `submitToAI()`** -- incorporate context threads. After fetching additional threads from the existing checkbox selection (~line 1538), also fetch context builder threads:

```javascript
// After the existing additionalThreads loop, add:
for (const ct of contextThreads) {
    if (ct.thread_id === currentThreadId) continue; // skip if same as current
    if (additionalThreads.includes(ct.thread_id)) continue; // skip if already included

    const threadData = await fetch(`api.php?action=thread&thread_id=${ct.thread_id}`);
    const thread = await threadData.json();
    if (thread.emails && thread.emails.length > 0) {
        fullContent += '\n\n--- CONTEXT THREAD ---\n';
        fullContent += 'Subject: ' + (thread.emails[0].subject || '(No Subject)') + '\n\n';
        fullContent += processAdditionalThreadContent(thread.emails, contentMode);
    }
}
```

Also update the `saveAIResponse` call to include context thread IDs:

```javascript
thread_ids: [currentThreadId, ...additionalThreads, ...contextThreads.map(t => t.thread_id)],
```

#### 1D. Token estimation display

**File:** `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/thread_view.php`

**Add a JS constant** with model token limits (matching the PHP array from api.php lines 2332-2341):

```javascript
const MODEL_TOKEN_LIMITS = {
    // Copy from api.php $modelTokenLimits
    'claude-sonnet-4-5-20250929': 200000,
    'claude-opus-4-5-20251101': 200000,
    // ... etc
};
```

**Add HTML** -- a token estimation display below the submit button (after line 1013):

```html
<div id="token-estimate" style="margin-top: 10px; font-size: 0.85em; color: var(--text-secondary); text-align: center;">
    Estimated tokens: <span id="token-count">-</span> |
    Budget: <span id="token-budget">-</span>
    <div id="token-warning" style="color: var(--error); display: none; margin-top: 5px;"></div>
</div>
```

**Add JS function** `updateTokenEstimate()` -- called when content changes (agent switch, content mode change, context threads change):

```javascript
function updateTokenEstimate() {
    const content = buildEmailContent();
    const question = document.getElementById('ai-question').value;
    const context = document.getElementById('ai-context').value;
    const conversationCtx = buildConversationContext();

    // Approximate additional context thread content
    let additionalEstimate = 0;
    const additionalThreads = getSelectedThreads();
    // Rough estimate: each additional thread's email is ~500 chars average
    additionalEstimate += additionalThreads.length * 3 * 500; // 3 emails avg per thread
    additionalEstimate += contextThreads.reduce((sum, t) => sum + (t.email_count || 3) * 500, 0);

    const totalChars = content.length + question.length + context.length + conversationCtx.length + additionalEstimate;
    const estimatedTokens = Math.ceil(totalChars / 3.5); // ~3.5 chars per token is more accurate

    document.getElementById('token-count').textContent = estimatedTokens.toLocaleString();

    // Get lowest budget across all selected providers
    const selectedProvidersList = getSelectedProviders();
    let minBudget = Infinity;
    selectedProvidersList.forEach(provider => {
        const modelSelect = document.getElementById('model_' + provider);
        const model = modelSelect ? modelSelect.value : '';
        const limit = MODEL_TOKEN_LIMITS[model] || 128000;
        const budget = Math.floor(limit * 0.6);
        if (budget < minBudget) minBudget = budget;
    });

    if (minBudget === Infinity) minBudget = 76800; // default 60% of 128K
    document.getElementById('token-budget').textContent = minBudget.toLocaleString();

    const warningDiv = document.getElementById('token-warning');
    if (estimatedTokens > minBudget) {
        warningDiv.style.display = 'block';
        warningDiv.textContent = `Warning: Estimated tokens (${estimatedTokens.toLocaleString()}) exceed budget (${minBudget.toLocaleString()}). Content may be truncated.`;
    } else {
        warningDiv.style.display = 'none';
    }
}
```

**Hook up events** -- call `updateTokenEstimate()` on:
- DOMContentLoaded (after providers load)
- content-mode change: `document.getElementById('content-mode').addEventListener('change', updateTokenEstimate);`
- question/context textarea input events
- agent selection
- provider/model checkbox changes
- context thread add/remove

Also in `submitToAI()`, check token budget before sending and warn:

```javascript
// Before the fetch call, add a budget check:
const totalChars = fullContent.length;
const estimatedTokens = Math.ceil(totalChars / 3.5);
// ... check against minBudget, warn user with confirm() if over budget
```

#### 1E. File-based debug logging

**File:** `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/api.php`

**Add a new case** `log_ai_debug` (can be added near the `save_ai_response` case around line 4749):

```php
case 'log_ai_debug':
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') jsonError('POST required', 405);

    $jsonData = json_decode(file_get_contents('php://input'), true);
    if (!$jsonData) jsonError('Invalid JSON');

    $logFile = dirname(__DIR__) . '/logs/ai_debug.log';
    $timestamp = date('Y-m-d H:i:s');
    $logEntry = "=== [{$timestamp}] {$jsonData['event']} ===\n";

    if (isset($jsonData['data'])) {
        $logEntry .= json_encode($jsonData['data'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
    }

    $logEntry .= "---\n\n";

    file_put_contents($logFile, $logEntry, FILE_APPEND | LOCK_EX);

    jsonResponse(['success' => true, 'logged' => true]);
    break;
```

**File:** `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/thread_view.php`

**Add JS helper** to log debug info:

```javascript
async function logAIDebug(event, data) {
    try {
        await fetch('api.php?action=log_ai_debug', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ event: event, data: data })
        });
    } catch (e) {
        console.error('Debug log failed:', e);
    }
}
```

**In `submitToAI()`** -- add logging calls:
1. Before sending to AI proxy: `logAIDebug('AI_REQUEST', { providers, models: modelsMap, agent: selectedAgent, contentLength: fullContent.length, estimatedTokens, contentMode, contextThreadCount: contextThreads.length });`
2. After receiving response: `logAIDebug('AI_RESPONSE', { queryId: data.query_id, success: data.success, totalCost: data.total_cost, responseProviders: Object.keys(data.responses || {}) });`
3. On error: `logAIDebug('AI_ERROR', { error: error.message });`

---

### TASK 2: Speed up main page

#### 2A. Reduce thread loading defaults

**File:** `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/assets/app.js`

**Line 650:** Change `limit: 20` to `limit: 10` in `loadEmails()`:

```javascript
const params = { page: page, limit: 10 };
```

#### 2B. Reduce contact loading defaults

**File:** `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/assets/app.js`

**Line 176:** Change `limit: 20` to `limit: 10` in `loadContacts()`:

```javascript
limit: 10
```

#### 2C. Consider deferred loading

Both loadContacts() and loadEmails() run on page load (line 78 calls `loadContacts()`, and switching to the emails tab triggers `loadEmails()`). Since contacts is the default tab and emails only load when the tab is clicked, threads are already deferred. No change needed there.

---

### TASK 3: Fix thread contact name bug

The bug is in `QueryAdapter::getThreadsMongo()` at line 609:

```php
'contact_id' => ['$max' => '$contact_id']
```

And in `getThreadsSQL()` at line 549-550:

```php
MAX(e.contact_id) as contact_id,
MAX(c.name) as contact_name,
```

Both simply pick the MAX contact_id, which is arbitrary and wrong. The fix needs to find the most FREQUENT external sender per thread.

**File:** `/var/www/mssql.danmarcrm.com/dev1/extension2v1/src/QueryAdapter.php`

#### 3A. Fix getThreadsMongo() (lines 569-650)

Replace the simple `$group` with a two-stage aggregation:

1. First, filter out the user's own email from contact resolution
2. Group by thread_id AND from_email to count per-sender frequency
3. Then group by thread_id to get the most frequent sender

New pipeline:

```php
private function getThreadsMongo(?int $contactId, string $search, int $limit, int $offset): array
{
    $mongo = MongoDatabase::getInstance();
    $userEmail = defined('GMAIL_USER_EMAIL') ? strtolower(GMAIL_USER_EMAIL) : '';

    // Build match filter
    $matchFilter = [
        'thread_id' => ['$ne' => null],
    ];
    // Also filter out empty string thread_id
    $matchFilter['thread_id']['$ne'] = '';
    // Actually need both conditions - use $and or $nin
    $matchFilter = [
        'thread_id' => ['$exists' => true, '$nin' => [null, '']]
    ];

    if ($contactId) {
        $matchFilter['contact_id'] = $contactId;
    }
    if ($search) {
        $matchFilter['subject'] = ['$regex' => $search, '$options' => 'i'];
    }

    // Count total threads
    $countPipeline = [
        ['$match' => $matchFilter],
        ['$group' => ['_id' => '$thread_id']],
        ['$count' => 'total']
    ];
    $countResult = $mongo->aggregate('emails', $countPipeline);
    $total = !empty($countResult) ? $countResult[0]['total'] : 0;

    // Main pipeline: Find most frequent external sender per thread
    $pipeline = [
        ['$match' => $matchFilter],
        // Stage 1: Group by thread_id + contact_id, counting emails per sender
        // Exclude user's own email from contact consideration
        ['$group' => [
            '_id' => [
                'thread_id' => '$thread_id',
                'contact_id' => '$contact_id',
                'from_email' => ['$toLower' => '$from_email']
            ],
            'sender_count' => ['$sum' => 1],
            'subject' => ['$first' => '$subject'],
            'first_date' => ['$min' => '$date'],
            'last_date' => ['$max' => '$date'],
        ]],
        // Stage 2: Sort so most frequent sender is first within each thread
        ['$sort' => ['_id.thread_id' => 1, 'sender_count' => -1]],
        // Stage 3: Re-group by thread_id, picking first (most frequent) sender
        ['$group' => [
            '_id' => '$_id.thread_id',
            'subject' => ['$first' => '$subject'],
            'first_date' => ['$min' => '$first_date'],
            'last_date' => ['$max' => '$last_date'],
            'email_count' => ['$sum' => '$sender_count'],
            // Collect all senders with counts to pick the best one
            'senders' => ['$push' => [
                'contact_id' => '$_id.contact_id',
                'from_email' => '$_id.from_email',
                'count' => '$sender_count'
            ]],
        ]],
        // Stage 4: Filter out user's own email and pick most frequent external sender
        ['$addFields' => [
            'external_senders' => [
                '$filter' => [
                    'input' => '$senders',
                    'as' => 's',
                    'cond' => ['$ne' => ['$$s.from_email', $userEmail]]
                ]
            ]
        ]],
        ['$addFields' => [
            'primary_sender' => ['$arrayElemAt' => ['$external_senders', 0]],
            'inbound_count' => ['$size' => [
                '$filter' => [
                    'input' => '$senders',
                    'as' => 's',
                    'cond' => ['$ne' => ['$$s.from_email', $userEmail]]
                ]
            ]],
        ]],
        ['$addFields' => [
            'contact_id' => '$primary_sender.contact_id',
        ]],
        // Lookup contact name
        ['$lookup' => [
            'from' => 'contacts',
            'localField' => 'contact_id',
            'foreignField' => 'id',
            'as' => 'contact'
        ]],
        ['$lookup' => [
            'from' => 'thread_analysis',
            'localField' => '_id',
            'foreignField' => 'thread_id',
            'as' => 'analysis'
        ]],
        ['$project' => [
            '_id' => 0,
            'thread_id' => '$_id',
            'subject' => 1,
            'first_date' => 1,
            'last_date' => 1,
            'email_count' => 1,
            'contact_id' => 1,
            'contact_name' => ['$arrayElemAt' => ['$contact.name', 0]],
            'is_analyzed' => ['$arrayElemAt' => ['$analysis.is_analyzed', 0]],
            'needs_reanalysis' => ['$arrayElemAt' => ['$analysis.needs_reanalysis', 0]]
        ]],
        ['$sort' => ['last_date' => -1]],
        ['$skip' => $offset],
        ['$limit' => $limit]
    ];

    $threads = $mongo->aggregate('emails', $pipeline);

    return [
        'threads' => $threads,
        'total' => $total
    ];
}
```

**NOTE:** The above pipeline is complex. A simpler alternative approach that is more robust: keep the original simple pipeline, but add a post-processing step in PHP that does a second query to resolve contact names. However, the aggregation approach is more efficient as it does everything in one query.

**SIMPLER ALTERNATIVE for MongoDB fix (recommended):**

Keep the existing pipeline mostly intact but change the `contact_id` accumulator from `$max` to use `$push` to collect all contact_ids and from_emails, then post-process in PHP:

```php
// In the $group stage, replace:
//   'contact_id' => ['$max' => '$contact_id']
// With:
'contacts' => ['$push' => [
    'contact_id' => '$contact_id',
    'from_email' => ['$toLower' => '$from_email'],
    'direction' => '$direction'
]],
```

Then in the `$project` stage, remove the contact lookup. After getting results, do a PHP loop:

```php
$threads = $mongo->aggregate('emails', $pipeline);

// Post-process: find most frequent external sender for each thread
$userEmail = defined('GMAIL_USER_EMAIL') ? strtolower(GMAIL_USER_EMAIL) : '';
foreach ($threads as &$thread) {
    $contactCounts = [];
    foreach ($thread['contacts'] ?? [] as $c) {
        $email = $c['from_email'] ?? '';
        if ($email === $userEmail || empty($c['contact_id'])) continue;
        $cid = $c['contact_id'];
        $contactCounts[$cid] = ($contactCounts[$cid] ?? 0) + 1;
    }
    arsort($contactCounts);
    $thread['contact_id'] = !empty($contactCounts) ? array_key_first($contactCounts) : null;
    unset($thread['contacts']); // remove raw data

    // Lookup contact name
    if ($thread['contact_id']) {
        $contact = $mongo->findOne('contacts', ['id' => $thread['contact_id']]);
        $thread['contact_name'] = $contact['name'] ?? null;
    } else {
        $thread['contact_name'] = null;
    }
}
unset($thread);
```

This simpler approach avoids complex aggregation stages. The downside is N queries for contact names, but it only fetches 10-20 threads per page.

#### 3B. Fix getThreadsSQL() (lines 514-567)

Replace `MAX(e.contact_id) as contact_id, MAX(c.name) as contact_name` with a subquery that finds the most frequent non-user sender:

```sql
SELECT
    e.thread_id,
    MIN(e.subject) as subject,
    MIN(e.date) as first_date,
    MAX(e.date) as last_date,
    COUNT(*) as email_count,
    SUM(CASE WHEN e.direction = 'inbound' THEN 1 ELSE 0 END) as inbound_count,
    SUM(CASE WHEN e.direction = 'outbound' THEN 1 ELSE 0 END) as outbound_count,
    -- Subquery for most frequent external contact
    (SELECT e2.contact_id
     FROM emails e2
     WHERE e2.thread_id = e.thread_id
       AND e2.contact_id IS NOT NULL
       AND LOWER(e2.from_email) != ?  -- user's email
     GROUP BY e2.contact_id
     ORDER BY COUNT(*) DESC
     LIMIT 1) as contact_id,
    ta.is_analyzed,
    ta.needs_reanalysis
 FROM emails e
 LEFT JOIN thread_analysis ta ON e.thread_id = ta.thread_id
 WHERE {$where}
 GROUP BY e.thread_id
 ORDER BY MAX(e.date) DESC
 LIMIT ? OFFSET ?
```

Then join the contact name in a wrapper query or do a separate lookup. The simplest approach is a wrapper:

```sql
SELECT t.*, c.name as contact_name
FROM (
    -- inner query as above
) t
LEFT JOIN contacts c ON t.contact_id = c.id
```

Add $userEmail to params: `$params = array_merge([$userEmail], $params, [$limit, $offset]);`

---

### TASK 4: Hide AI Thread Analysis tab and stop auto-refresh

#### 4A. Hide the analysis section in index.php

**File:** `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/index.php`

**Line 136:** Add `style="display:none"` to the analysis section:

```html
<div class="analysis-status-section" id="analysis-status-section" style="display: none;">
```

#### 4B. Remove the auto-refresh interval in app.js

**File:** `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/assets/app.js`

**Line 1589:** Comment out or remove `refreshAnalysisStatus();`:

```javascript
// refreshAnalysisStatus(); // DISABLED: Heavy N+1 queries, not useful
```

**Line 1592:** Comment out or remove the setInterval:

```javascript
// setInterval(refreshAnalysisStatus, 30000); // DISABLED: Heavy N+1 queries, not useful
```

---

### Implementation Order and Dependencies

The recommended implementation sequence:

1. **TASK 4 first** (5 minutes) -- smallest change, immediate performance benefit. Just comment out 2 lines in app.js and hide the HTML section in index.php.

2. **TASK 2 next** (5 minutes) -- change two `limit` values in app.js. Instant speed improvement.

3. **TASK 3 next** (30 minutes) -- fix the contact name bug in QueryAdapter.php. This is a standalone backend fix with no frontend dependencies. Test by loading threads in the main page and verifying contact names are correct and varied.

4. **TASK 1 last** (2-3 hours) -- this is the big one. Implement in sub-steps:
   - 1A: Full-width layout CSS (2 minutes)
   - 1B: Agent buttons and quick prompts (30 minutes)
   - 1C: Context builder search and add (45 minutes)
   - 1D: Token estimation (30 minutes)
   - 1E: Debug file logging (20 minutes)
   - Integration testing: ensure submitToAI sends system_prompt, context threads are fetched, tokens estimated correctly

### Critical Files for Implementation

- `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/thread_view.php` - Primary file for TASK 1: all layout, agents, context builder, token estimation, and logging JS/HTML changes
- `/var/www/mssql.danmarcrm.com/dev1/extension2v1/src/QueryAdapter.php` - TASK 3: Fix getThreadsMongo() and getThreadsSQL() contact name resolution
- `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/assets/app.js` - TASK 2 and TASK 4: reduce loading limits, disable analysis auto-refresh
- `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/index.php` - TASK 4: hide analysis status section HTML
- `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/api.php` - TASK 1E: add log_ai_debug endpoint

agentId: a13c580 (for resuming to continue this agent's work if needed)
<usage>total_tokens: 87731
tool_uses: 47
duration_ms: 364065</usage>

Prompt:
Based on the following exploration findings, design a detailed implementation plan. The user wants to enhance the existing `thread_view.php` page AND fix several issues on the main `index.php` page.

## Context from exploration:

### Files:
- `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/thread_view.php` — Full-page thread view (2033 lines), has multi-provider AI, conversation history, saved responses
- `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/api.php` — Backend REST API, already has ai_chat case with agents/sessions/memory + save_ai_response endpoint
- `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/assets/app.js` — Main app JS, has AI chat panel code appended at end
- `/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/index.php` — Main dashboard with tabs
- `/var/www/mssql.danmarcrm.com/dev1/extension2v1/src/QueryAdapter.php` — Data access layer

### Existing thread_view.php architecture:
- PHP top: Fetches thread data from QueryAdapter, builds participant map, gets saved responses from MongoDB `ai_query_responses` collection
- Layout: 2-column grid (left: email timeline + responses, right: AI form 400px)
- AI form: Multi-provider selection via checkboxes (loads from api_opus/query.php?action=providers), content processing mode (full/stripped/smart), question textarea, context textarea
- Sends to: `../aiapi/api_opus/api/query.php` (multi-provider proxy that sends to ALL selected providers simultaneously)
- Saves responses to MongoDB via `api.php?action=save_ai_response`
- Has conversation history in sessionStorage
- Has saved response quick view modal

### What the user wants (7 tasks):

**TASK 1: Enhance thread_view.php**
- Full page width (currently max-width: 1600px — just needs widening)
- Keep multi-provider selection (send to multiple AI simultaneously)
- Add General/Quotes/Deception/Profiler/Composer agent buttons with sub-buttons (quick prompts)
- Add search by email/thread with + buttons (context builder from the floating panel)
- Add estimated tokens display and enforce max
- Keep results saved (existing save_ai_response works)
- Keep discussing after initial result (conversation history exists)
- Add file logging of everything sent/received for debug

**TASK 2: Speed up main page**
- Load fewer threads on main page
- Load fewer contacts (already 20, could be 10)
- Threads should load faster

**TASK 3: Fix thread contact name bug**
- In QueryAdapter::getThreadsSQL(), `MAX(c.name)` returns wrong contact
- In MongoDB version, `'contact_id' => ['$max' => '$contact_id']` also wrong
- All threads show same contact name (e.g., "Etienne Turner")
- Fix: For each thread, find the most FREQUENT external sender (not user's own email), use THAT as contact

**TASK 4: Fix/hide AI Thread Analysis tab**
- The analysis_status endpoint is heavy (N+1 queries, COUNT DISTINCT, runs every 30s)
- Recent analysis shows "Unknown" for contact names
- User doesn't find it useful
- Solution: Hide it entirely and stop the auto-refresh interval

### Key constraints:
- Data lives in MongoDB (not SQL) — always use QueryAdapter
- `MongoDatabase::insertOrUpdate()` for upserts
- Node.js on server is v10.19 — no ES6+ arrow syntax in server-side checks
- The AI proxy at `aiapi/api_opus/api/query.php` handles multi-provider. The codex51 proxy handles single-provider.
- For thread_view.php: Keep using api_opus proxy for multi-provider support (NOT ai_chat endpoint which is single-provider)
- Agent system prompts need to be injected before sending to the proxy

### Design the plan for each task with specific code changes.

For TASK 1 (thread_view.php enhancement), the key architectural decisions:
- Keep the existing multi-provider flow (api_opus proxy)
- Add agent presets as JS constants (same 5 agents from app.js)
- Agent sets a system prompt that gets prepended to the content
- Add context builder: search input calls api.php?action=contacts and api.php?action=threads, results show + Add buttons, added threads get fetched and included in content
- Token estimation: calculate content length / 3.5, show in UI, check against model limits per provider
- File logging: Add a PHP endpoint `log_ai_debug` that appends to `/var/www/mssql.danmarcrm.com/dev1/extension2v1/logs/ai_debug.log`

For TASK 3 (contact name fix), the approach:
- In getThreadsMongo(): Instead of `$max` on contact_id, use a more complex aggregation that groups by contact_id, counts frequency, and picks the most frequent one (excluding user's own email)
- In getThreadsSQL(): Use a subquery or different approach to get the most frequent contact per thread

Please provide a detailed implementation plan with specific file changes, function modifications, and code patterns to use.


Now let me read a few critical spots I need to verify before finalizing the plan.


   569→    private function getThreadsMongo(?int $contactId, string $search, int $limit, int $offset): array
   570→    {
   571→        $mongo = MongoDatabase::getInstance();
   572→
   573→        // Build match filter
   574→        $matchFilter = [
   575→            'thread_id' => ['$ne' => null, '$ne' => '']
   576→        ];
   577→
   578→        if ($contactId) {
   579→            $matchFilter['contact_id'] = $contactId;
   580→        }
   581→
   582→        if ($search) {
   583→            $matchFilter['subject'] = ['$regex' => $search, '$options' => 'i'];
   584→        }
   585→
   586→        // Count distinct threads for total
   587→        $countPipeline = [
   588→            ['$match' => $matchFilter],
   589→            ['$group' => ['_id' => '$thread_id']],
   590→            ['$count' => 'total']
   591→        ];
   592→
   593→        $countResult = $mongo->aggregate('emails', $countPipeline);
   594→        $total = !empty($countResult) ? $countResult[0]['total'] : 0;
   595→
   596→        // Get threads with aggregated data
   597→        $pipeline = [
   598→            ['$match' => $matchFilter],
   599→            ['$group' => [
   600→                '_id' => '$thread_id',
   601→                'first_email_id' => ['$min' => '$id'],
   602→                'last_email_id' => ['$max' => '$id'],
   603→                'subject' => ['$first' => '$subject'],
   604→                'first_date' => ['$min' => '$date'],
   605→                'last_date' => ['$max' => '$date'],
   606→                'email_count' => ['$sum' => 1],
   607→                'inbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'inbound']], 1, 0]]],
   608→                'outbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'outbound']], 1, 0]]],
   609→                'contact_id' => ['$max' => '$contact_id']
   610→            ]],
   611→            ['$lookup' => [
   612→                'from' => 'contacts',
   613→                'localField' => 'contact_id',
   614→                'foreignField' => 'id',
   615→                'as' => 'contact'
   616→            ]],
   617→            ['$lookup' => [
   618→                'from' => 'thread_analysis',
   619→                'localField' => '_id',
   620→                'foreignField' => 'thread_id',
   621→                'as' => 'analysis'
   622→            ]],
   623→            ['$project' => [
   624→                '_id' => 0,
   625→                'thread_id' => '$_id',
   626→                'first_email_id' => 1,
   627→                'last_email_id' => 1,
   628→                'subject' => 1,
   629→                'first_date' => 1,
   630→                'last_date' => 1,
   631→                'email_count' => 1,
   632→                'inbound_count' => 1,
   633→                'outbound_count' => 1,
   634→                'contact_id' => 1,
   635→                'contact_name' => ['$arrayElemAt' => ['$contact.name', 0]],
   636→                'is_analyzed' => ['$arrayElemAt' => ['$analysis.is_analyzed', 0]],
   637→                'needs_reanalysis' => ['$arrayElemAt' => ['$analysis.needs_reanalysis', 0]]
   638→            ]],
   639→            ['$sort' => ['last_date' => -1]],
   640→            ['$skip' => $offset],
   641→            ['$limit' => $limit]
   642→        ];
   643→
   644→        $threads = $mongo->aggregate('emails', $pipeline);
   645→
   646→        return [
   647→            'threads' => $threads,
   648→            'total' => $total
   649→        ];
   650→    }
   651→
   652→    /**
   653→     * Get full thread details with all emails and analysis

    private function getThreadsMongo(?int $contactId, string $search, int $limit, int $offset): array
    {
        $mongo = MongoDatabase::getInstance();

        // Build match filter
        $matchFilter = [
            'thread_id' => ['$ne' => null, '$ne' => '']
        ];

        if ($contactId) {
            $matchFilter['contact_id'] = $contactId;
        }

        if ($search) {
            $matchFilter['subject'] = ['$regex' => $search, '$options' => 'i'];
        }

        // Count distinct threads for total
        $countPipeline = [
            ['$match' => $matchFilter],
            ['$group' => ['_id' => '$thread_id']],
            ['$count' => 'total']
        ];

        $countResult = $mongo->aggregate('emails', $countPipeline);
        $total = !empty($countResult) ? $countResult[0]['total'] : 0;

        // Get threads with aggregated data
        $pipeline = [
            ['$match' => $matchFilter],
            ['$group' => [
                '_id' => '$thread_id',
                'first_email_id' => ['$min' => '$id'],
                'last_email_id' => ['$max' => '$id'],
                'subject' => ['$first' => '$subject'],
                'first_date' => ['$min' => '$date'],
                'last_date' => ['$max' => '$date'],
                'email_count' => ['$sum' => 1],
                'inbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'inbound']], 1, 0]]],
                'outbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'outbound']], 1, 0]]],
                'contact_id' => ['$max' => '$contact_id']
            ]],
            ['$lookup' => [
                'from' => 'contacts',
                'localField' => 'contact_id',
                'foreignField' => 'id',
                'as' => 'contact'
            ]],
            ['$lookup' => [
                'from' => 'thread_analysis',
                'localField' => '_id',
                'foreignField' => 'thread_id',
                'as' => 'analysis'
            ]],
            ['$project' => [
                '_id' => 0,
                'thread_id' => '$_id',
                'first_email_id' => 1,
                'last_email_id' => 1,
                'subject' => 1,
                'first_date' => 1,
                'last_date' => 1,
                'email_count' => 1,
                'inbound_count' => 1,
                'outbound_count' => 1,
                'contact_id' => 1,
                'contact_name' => ['$arrayElemAt' => ['$contact.name', 0]],
                'is_analyzed' => ['$arrayElemAt' => ['$analysis.is_analyzed', 0]],
                'needs_reanalysis' => ['$arrayElemAt' => ['$analysis.needs_reanalysis', 0]]
            ]],
            ['$sort' => ['last_date' => -1]],
            ['$skip' => $offset],
            ['$limit' => $limit]
        ];

        $threads = $mongo->aggregate('emails', $pipeline);

        return [
            'threads' => $threads,
            'total' => $total
        ];
    }

    /**
     * Get full thread details with all emails and analysis


155→
   156→function hideLoading() {
   157→    document.getElementById('loading-overlay').style.display = 'none';
   158→}
   159→
   160→// ============================================
   161→// Contacts
   162→// ============================================
   163→async function loadContacts(page = 1) {
   164→    const container = document.getElementById('contacts-list');
   165→    container.innerHTML = '
Loading contacts...
';
   166→
   167→    try {
   168→        state.contacts.page = page;
   169→        state.contacts.search = document.getElementById('contact-search')?.value || '';
   170→        state.contacts.type = document.getElementById('contact-type-filter')?.value || '';
   171→
   172→        const data = await apiCall('contacts', {
   173→            page: page,
   174→            search: state.contacts.search,
   175→            type: state.contacts.type,
   176→            limit: 20
   177→        });
   178→
   179→        if (data.contacts.length === 0) {
   180→            container.innerHTML = '
No contacts found
';
   181→            return;
   182→        }
   183→
   184→        let html = `
   185→

   186→

   187→

   188→                        Contact
   189→                        Type
   190→                        Emails
   191→                        Relationship
   192→                        Risk
   193→                        Last Contact
   194→                        Actions

function hideLoading() {
    document.getElementById('loading-overlay').style.display = 'none';
}

// ============================================
// Contacts
// ============================================
async function loadContacts(page = 1) {
    const container = document.getElementById('contacts-list');
    container.innerHTML = '
Loading contacts...
';

    try {
        state.contacts.page = page;
        state.contacts.search = document.getElementById('contact-search')?.value || '';
        state.contacts.type = document.getElementById('contact-type-filter')?.value || '';

        const data = await apiCall('contacts', {
            page: page,
            search: state.contacts.search,
            type: state.contacts.type,
            limit: 20
        });

        if (data.contacts.length === 0) {
            container.innerHTML = '
No contacts found
';
            return;
        }

        let html = `

                        Contact
                        Type
                        Emails
                        Relationship
                        Risk
                        Last Contact
                        Actions


Conversation compacted