@@ -65,6 +80,41 @@ export class RegentSidebar {
// Cache references
this.sessionsContainer = this.sidebar.querySelector('.regent-sessions');
this.metaEl = this.sidebar.querySelector('.regent-meta');
+ this._searchEl = this.sidebar.querySelector('.regent-search');
+ this._searchInput = this.sidebar.querySelector('.regent-search-input');
+ this._searchResultsEl = this.sidebar.querySelector('.regent-search-results');
+ this._searchDebounce = null;
+ this._agentPanel = this.sidebar.querySelector('.regent-agent-panel');
+ this._agentSelect = this.sidebar.querySelector('.regent-agent-select');
+ this._agentInput = this.sidebar.querySelector('.regent-agent-input');
+ this._agentOutput = this.sidebar.querySelector('.regent-agent-output');
+ this._notificationsEl = this.sidebar.querySelector('.regent-notifications');
+ this._currentRunId = null;
+
+ // Agent run button โ includes selected agentId
+ this.sidebar.querySelector('.regent-agent-run-btn').addEventListener('click', () => {
+ const agentId = this._agentSelect.value;
+ const task = this._agentInput.value.trim();
+ if (!agentId || !task) return;
+ this._agentOutput.textContent = 'Starting agent...';
+ chrome.runtime.sendMessage({
+ action: 'mothershipSend',
+ payload: { type: 'agent:start', payload: { agentId, input: task } },
+ }).catch(() => {});
+ });
+
+ // Search input โ debounced query via WS
+ this._searchInput.addEventListener('input', () => {
+ clearTimeout(this._searchDebounce);
+ const q = this._searchInput.value.trim();
+ if (!q) { this._hideSearchResults(); return; }
+ this._searchDebounce = setTimeout(() => {
+ chrome.runtime.sendMessage({
+ action: 'mothershipSend',
+ payload: { type: 'context:query', payload: { query: q, limit: 15 } },
+ }).catch(() => {});
+ }, 400);
+ });
// Collapse/expand
const collapseBtn = this.sidebar.querySelector('.regent-collapse-btn');
@@ -265,6 +315,177 @@ export class RegentSidebar {
this.sessionsContainer.appendChild(cal);
}
+ /** Set mothership connection status indicator */
+ setConnectionStatus(status) {
+ const dot = this.sidebar?.querySelector('.regent-connection-dot');
+ if (!dot) return;
+ const connected = status === 'connected';
+ dot.classList.toggle('connected', connected);
+ dot.title = `Mothership: ${connected ? 'connected' : 'disconnected'}`;
+ // Show/hide search bar and agent panel based on connection
+ if (this._searchEl) this._searchEl.style.display = connected ? '' : 'none';
+ if (this._agentPanel) this._agentPanel.style.display = connected ? '' : 'none';
+ if (this._notificationsEl) this._notificationsEl.style.display = connected ? '' : 'none';
+ if (!connected) {
+ this._hideSearchResults();
+ if (this._agentOutput) this._agentOutput.textContent = '';
+ }
+ // Fetch agents list when connected
+ if (connected) this._loadAgents();
+ }
+
+ /** Fetch agents from mothership and populate the selector */
+ _loadAgents() {
+ chrome.storage.local.get(['mothershipUrl', 'mothershipToken', 'mothershipWorkspaceId'], async (data) => {
+ if (!data.mothershipUrl || !data.mothershipToken || !data.mothershipWorkspaceId) return;
+ try {
+ const res = await fetch(`${data.mothershipUrl}/api/v1/workspaces/${data.mothershipWorkspaceId}/agents`, {
+ headers: { Authorization: `Bearer ${data.mothershipToken}` },
+ });
+ if (!res.ok) return;
+ const agents = await res.json();
+ if (!this._agentSelect) return;
+ this._agentSelect.innerHTML = '
';
+ for (const a of agents) {
+ const opt = document.createElement('option');
+ opt.value = a.id;
+ opt.textContent = a.name;
+ this._agentSelect.appendChild(opt);
+ }
+ } catch {}
+ });
+ }
+
+ /** Show a notification toast in the sidebar */
+ showNotification(notification) {
+ if (!this._notificationsEl) return;
+ const toast = document.createElement('div');
+ toast.className = 'regent-notification-toast';
+ toast.innerHTML = `
+
${this._escapeHtml(notification.title)}
+ ${notification.body ? `
${this._escapeHtml(notification.body)}
` : ''}
+ `;
+ this._notificationsEl.appendChild(toast);
+ // Auto-remove after 8 seconds
+ setTimeout(() => toast.remove(), 8000);
+ }
+
+ /** Handle agent streaming chunks */
+ handleAgentStream(data) {
+ if (!this._agentOutput) return;
+ if (data.done) {
+ this._agentOutput.textContent += '\n--- Done ---';
+ this._currentRunId = null;
+ return;
+ }
+ if (data.chunk) {
+ if (this._agentOutput.textContent === 'Starting agent...') this._agentOutput.textContent = '';
+ this._agentOutput.textContent += data.chunk;
+ }
+ }
+
+ /** Handle agent started confirmation */
+ handleAgentStarted(data) {
+ this._currentRunId = data.runId;
+ if (this._agentOutput) this._agentOutput.textContent = '';
+ }
+
+ /** Handle agent tool call visualization */
+ handleAgentToolCall(data) {
+ if (!this._agentOutput) return;
+ const el = document.createElement('div');
+ el.className = 'regent-agent-tool-call';
+ el.textContent = `Tool: ${data.tool}`;
+ this._agentOutput.appendChild(el);
+ }
+
+ /** Handle agent error */
+ handleAgentError(data) {
+ if (!this._agentOutput) return;
+ this._agentOutput.textContent += `\nError: ${data.error}`;
+ this._currentRunId = null;
+ }
+
+ /** Display search results from mothership context:results */
+ showSearchResults(results) {
+ if (!results?.length) {
+ this._searchResultsEl.innerHTML = '
No results found
';
+ this._searchResultsEl.style.display = '';
+ return;
+ }
+
+ this._searchResultsEl.innerHTML = '';
+ for (const r of results) {
+ const el = document.createElement('div');
+ el.className = 'regent-search-result';
+ const time = r.created_at ? new Date(r.created_at).toLocaleDateString([], { month: 'short', day: 'numeric' }) : '';
+ el.innerHTML = `
+
${this._escapeHtml(r.content.slice(0, 120))}
+
+ ${r.source_type || 'event'}
+ ${time}
+
+ `;
+ this._searchResultsEl.appendChild(el);
+ }
+ this._searchResultsEl.style.display = '';
+ }
+
+ /** Hide search results panel */
+ _hideSearchResults() {
+ if (this._searchResultsEl) {
+ this._searchResultsEl.style.display = 'none';
+ this._searchResultsEl.innerHTML = '';
+ }
+ }
+
+ /** Display cross-session events received from mothership (other tabs/devices) */
+ addCrossSessionEvents(sessionId, events) {
+ if (!events?.length) return;
+
+ // Get or create a "remote" session section
+ let section = this._sessionElements.get(`remote:${sessionId}`);
+ if (!section) {
+ const empty = this.sessionsContainer?.querySelector('.regent-empty');
+ if (empty) empty.remove();
+
+ section = document.createElement('div');
+ section.className = 'regent-session regent-session-remote';
+ section.dataset.sessionId = `remote:${sessionId}`;
+ section.innerHTML = `
+
+
+ `;
+ this.sessionsContainer?.appendChild(section);
+ this._sessionElements.set(`remote:${sessionId}`, section);
+ }
+
+ const container = section.querySelector('.regent-events');
+ for (const evt of events) {
+ const el = document.createElement('div');
+ el.className = 'regent-event entering';
+ el.dataset.importance = evt.importance || 'medium';
+ const time = new Date(evt.created_at || Date.now()).toLocaleTimeString([], {
+ hour: '2-digit', minute: '2-digit',
+ });
+ el.innerHTML = `
+
+
+
${this._escapeHtml(evt.summary)}
+
+ `;
+ container.appendChild(el);
+ }
+
+ this._updateBadge();
+ }
+
/** Update meta-summary */
updateMeta(text) {
if (!text) {
diff --git a/src/content/regent/RegentSidecar.js b/src/content/regent/RegentSidecar.js
index 2b8cd2c..e9e8a4a 100644
--- a/src/content/regent/RegentSidecar.js
+++ b/src/content/regent/RegentSidecar.js
@@ -92,6 +92,9 @@ export class RegentSidecar {
this._processedCount += batch.length;
this.onEventsUpdate?.(this.sessionId, this.events);
+
+ // Forward extracted events to mothership (fire-and-forget)
+ this._forwardToMothership(aiEvents);
} catch (err) {
console.warn(`[Regent:Sidecar:${this.sessionId}] Summarization failed:`, err.message);
// Drop failed batch to avoid infinite retry loop โ messages are lost but system stays stable
@@ -108,6 +111,28 @@ export class RegentSidecar {
}
}
+ /** Forward extracted events to mothership via background WS */
+ _forwardToMothership(aiEvents) {
+ if (!aiEvents?.length) return;
+ chrome.runtime.sendMessage({
+ action: 'mothershipSend',
+ payload: {
+ type: 'events:store',
+ payload: {
+ sessionId: this.sessionId,
+ sessionName: this.getDisplayName(),
+ url: location.href,
+ hostname: location.hostname,
+ events: aiEvents.map(e => ({
+ title: e.title, summary: e.summary,
+ importance: e.importance || 'medium',
+ messageIndex: e.messageIndex,
+ })),
+ },
+ },
+ }).catch(() => {}); // Silent fail โ mothership is optional
+ }
+
/** Get session display name */
getDisplayName() {
// Try to extract from URL or element content
diff --git a/src/content/regent/regent.css b/src/content/regent/regent.css
index 2091cb9..ade443e 100644
--- a/src/content/regent/regent.css
+++ b/src/content/regent/regent.css
@@ -111,6 +111,26 @@
letter-spacing: -0.01em;
}
+.regent-connection-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--regent-low);
+ display: inline-block;
+ margin: 0 4px;
+ transition: background 0.3s;
+}
+
+.regent-connection-dot.connected {
+ background: #34c759;
+ box-shadow: 0 0 6px rgba(52, 199, 89, 0.4);
+}
+
+.regent-session-remote .session-status.remote {
+ background: rgba(88, 86, 214, 0.12);
+ color: #5856d6;
+}
+
.regent-badge {
font-size: 11px;
font-weight: 500;
@@ -366,6 +386,186 @@
.regent-calibration-btn:disabled { opacity: 0.6; cursor: wait; }
.regent-calibration-btn + .regent-calibration-btn { margin-top: 8px; }
+/* โโโ Memory search โโโ */
+.regent-search {
+ padding: 8px 16px;
+ border-bottom: 1px solid var(--regent-border);
+ flex-shrink: 0;
+}
+.regent-search-input {
+ width: 100%;
+ padding: 6px 10px;
+ border: 1px solid var(--regent-border);
+ border-radius: 8px;
+ background: var(--regent-event-bg);
+ color: var(--regent-text);
+ font-size: 12px;
+ outline: none;
+ transition: border-color 0.2s;
+ box-sizing: border-box;
+ font-family: inherit;
+}
+.regent-search-input:focus {
+ border-color: var(--regent-accent);
+}
+.regent-search-input::placeholder {
+ color: var(--regent-text-secondary);
+}
+
+.regent-search-results {
+ max-height: 240px;
+ overflow-y: auto;
+ border-bottom: 1px solid var(--regent-border);
+ scrollbar-width: thin;
+ scrollbar-color: var(--regent-border) transparent;
+}
+
+.regent-search-result {
+ padding: 8px 16px;
+ border-bottom: 1px solid var(--regent-border);
+ cursor: default;
+}
+.regent-search-result:last-child { border-bottom: none; }
+
+.search-result-content {
+ font-size: 12px;
+ color: var(--regent-text);
+ line-height: 1.4;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.search-result-meta {
+ display: flex;
+ gap: 8px;
+ margin-top: 4px;
+ font-size: 10px;
+ color: var(--regent-text-secondary);
+}
+
+.search-result-type {
+ background: var(--regent-accent-bg);
+ color: var(--regent-accent);
+ padding: 0 5px;
+ border-radius: 4px;
+ font-weight: 500;
+}
+
+.regent-search-empty {
+ padding: 16px;
+ text-align: center;
+ font-size: 12px;
+ color: var(--regent-text-secondary);
+}
+
+/* โโโ Agent panel โโโ */
+.regent-agent-panel {
+ padding: 8px 16px;
+ border-bottom: 1px solid var(--regent-border);
+ flex-shrink: 0;
+}
+.regent-agent-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 6px;
+}
+.regent-agent-title {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--regent-text);
+}
+.regent-agent-run-btn {
+ background: var(--regent-accent);
+ color: #fff;
+ border: none;
+ border-radius: 6px;
+ padding: 3px 10px;
+ font-size: 11px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: opacity 0.15s;
+}
+.regent-agent-run-btn:hover { opacity: 0.85; }
+.regent-agent-input {
+ width: 100%;
+ padding: 6px 10px;
+ border: 1px solid var(--regent-border);
+ border-radius: 8px;
+ background: var(--regent-event-bg);
+ color: var(--regent-text);
+ font-size: 12px;
+ outline: none;
+ box-sizing: border-box;
+ font-family: inherit;
+ margin-bottom: 6px;
+}
+.regent-agent-input:focus { border-color: var(--regent-accent); }
+.regent-agent-input::placeholder { color: var(--regent-text-secondary); }
+.regent-agent-output {
+ max-height: 200px;
+ overflow-y: auto;
+ font-size: 11px;
+ color: var(--regent-text-secondary);
+ line-height: 1.5;
+ white-space: pre-wrap;
+ word-break: break-word;
+ scrollbar-width: thin;
+ scrollbar-color: var(--regent-border) transparent;
+}
+.regent-agent-select {
+ flex: 1;
+ min-width: 0;
+ padding: 3px 6px;
+ border: 1px solid var(--regent-border);
+ border-radius: 6px;
+ background: var(--regent-event-bg);
+ color: var(--regent-text);
+ font-size: 11px;
+ font-family: inherit;
+ outline: none;
+ margin: 0 6px;
+}
+.regent-agent-select:focus { border-color: var(--regent-accent); }
+.regent-agent-tool-call {
+ padding: 2px 6px;
+ margin: 4px 0;
+ background: var(--regent-accent-bg);
+ color: var(--regent-accent);
+ border-radius: 4px;
+ font-size: 10px;
+ font-weight: 500;
+}
+
+/* โโโ Notifications โโโ */
+.regent-notifications {
+ padding: 0 12px;
+}
+.regent-notification-toast {
+ padding: 8px 12px;
+ margin: 4px 0;
+ background: var(--regent-accent-bg);
+ border-left: 3px solid var(--regent-accent);
+ border-radius: 6px;
+ animation: regent-toast-in 0.3s ease-out;
+}
+.regent-notification-toast .notification-title {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--regent-text);
+}
+.regent-notification-toast .notification-body {
+ font-size: 11px;
+ color: var(--regent-text-secondary);
+ margin-top: 2px;
+}
+@keyframes regent-toast-in {
+ from { opacity: 0; transform: translateX(20px); }
+ to { opacity: 1; transform: translateX(0); }
+}
+
/* โโโ Scrollbar smooth โโโ */
.regent-sessions {
scroll-behavior: smooth;
diff --git a/src/popup/MothershipManager.js b/src/popup/MothershipManager.js
new file mode 100644
index 0000000..6b94572
--- /dev/null
+++ b/src/popup/MothershipManager.js
@@ -0,0 +1,170 @@
+/**
+ * MothershipManager โ Popup UI for connecting the extension to the Mothership backend.
+ * Handles URL/token input, workspace selection, connect/disconnect, and status display.
+ */
+export class MothershipManager {
+ constructor() {
+ this.urlInput = document.getElementById('mothershipUrl');
+ this.tokenInput = document.getElementById('mothershipToken');
+ this.workspaceSelect = document.getElementById('mothershipWorkspaceSelect');
+ this.connectBtn = document.getElementById('mothershipConnectBtn');
+ this.disconnectBtn = document.getElementById('mothershipDisconnectBtn');
+ this.statusDot = document.getElementById('mothershipStatusDot');
+ this.statusText = document.getElementById('mothershipStatusText');
+ this._workspaces = [];
+
+ this._bindEvents();
+ this._loadState();
+ }
+
+ _bindEvents() {
+ this.connectBtn.addEventListener('click', () => this._connect());
+ this.disconnectBtn.addEventListener('click', () => this._disconnect());
+ // Auto-fetch workspaces when token field loses focus
+ this.tokenInput.addEventListener('blur', () => this._fetchWorkspaces());
+ this.workspaceSelect.addEventListener('change', () => {
+ const wsId = this.workspaceSelect.value;
+ if (wsId) chrome.storage.local.set({ mothershipWorkspaceId: wsId });
+ });
+ }
+
+ async _loadState() {
+ const data = await new Promise(r =>
+ chrome.storage.local.get(['mothershipUrl', 'mothershipToken', 'mothershipWorkspaceId'], r)
+ );
+
+ if (data.mothershipUrl) this.urlInput.value = data.mothershipUrl;
+ if (data.mothershipToken) this.tokenInput.value = data.mothershipToken;
+
+ // Check connection status
+ chrome.runtime.sendMessage({ action: 'mothershipStatus' }, (res) => {
+ this._updateUI(res?.connected, data.mothershipWorkspaceId);
+ });
+
+ // If we have URL + token, fetch workspaces to populate selector
+ if (data.mothershipUrl && data.mothershipToken) {
+ this._fetchWorkspaces(data.mothershipWorkspaceId);
+ }
+ }
+
+ async _fetchWorkspaces(selectedId) {
+ const url = this.urlInput.value.trim().replace(/\/+$/, '');
+ const token = this.tokenInput.value.trim();
+ if (!url || !token) return;
+
+ try {
+ const res = await fetch(`${url}/api/v1/workspaces`, {
+ headers: { Authorization: `Bearer ${token}` },
+ });
+ if (!res.ok) return;
+ this._workspaces = await res.json();
+
+ // Populate selector
+ this.workspaceSelect.innerHTML = '
';
+ for (const ws of this._workspaces) {
+ const opt = document.createElement('option');
+ opt.value = ws.id;
+ opt.textContent = ws.name;
+ if (ws.id === selectedId) opt.selected = true;
+ this.workspaceSelect.appendChild(opt);
+ }
+ this.workspaceSelect.style.display = this._workspaces.length > 1 ? '' : 'none';
+
+ // Auto-select if only one
+ if (this._workspaces.length === 1 && !selectedId) {
+ this.workspaceSelect.value = this._workspaces[0].id;
+ }
+ } catch {}
+ }
+
+ async _connect() {
+ const url = this.urlInput.value.trim().replace(/\/+$/, '');
+ const token = this.tokenInput.value.trim();
+ if (!url || !token) return;
+
+ this.connectBtn.textContent = 'Connecting...';
+ this.connectBtn.disabled = true;
+
+ try {
+ const healthRes = await fetch(`${url}/api/v1/health`);
+ if (!healthRes.ok) throw new Error('Server unreachable');
+
+ // Fetch workspaces
+ const wsRes = await fetch(`${url}/api/v1/workspaces`, {
+ headers: { Authorization: `Bearer ${token}` },
+ });
+ if (!wsRes.ok) {
+ this._showError('Invalid token');
+ this.connectBtn.textContent = 'Connect';
+ this.connectBtn.disabled = false;
+ return;
+ }
+ this._workspaces = await wsRes.json();
+ if (!this._workspaces.length) {
+ this._showError('No workspace found');
+ this.connectBtn.textContent = 'Connect';
+ this.connectBtn.disabled = false;
+ return;
+ }
+
+ // Use selected workspace or default to first
+ const workspaceId = this.workspaceSelect.value || this._workspaces[0].id;
+
+ // Populate selector
+ this.workspaceSelect.innerHTML = '';
+ for (const ws of this._workspaces) {
+ const opt = document.createElement('option');
+ opt.value = ws.id;
+ opt.textContent = ws.name;
+ if (ws.id === workspaceId) opt.selected = true;
+ this.workspaceSelect.appendChild(opt);
+ }
+ this.workspaceSelect.style.display = this._workspaces.length > 1 ? '' : 'none';
+
+ chrome.storage.local.set({ mothershipUrl: url, mothershipToken: token, mothershipWorkspaceId: workspaceId });
+ } catch {
+ this._showError('Cannot reach server');
+ this.connectBtn.textContent = 'Connect';
+ this.connectBtn.disabled = false;
+ return;
+ }
+
+ // Tell background to connect
+ chrome.runtime.sendMessage({ action: 'mothershipConnect', url, token }, () => {
+ setTimeout(() => {
+ chrome.runtime.sendMessage({ action: 'mothershipStatus' }, (res) => {
+ this._updateUI(res?.connected);
+ this.connectBtn.textContent = 'Connect';
+ this.connectBtn.disabled = false;
+ });
+ }, 1000);
+ });
+ }
+
+ _disconnect() {
+ chrome.runtime.sendMessage({ action: 'mothershipDisconnect' });
+ chrome.storage.local.remove(['mothershipUrl', 'mothershipToken', 'mothershipWorkspaceId']);
+ this.urlInput.value = '';
+ this.tokenInput.value = '';
+ this.workspaceSelect.innerHTML = '
';
+ this.workspaceSelect.style.display = 'none';
+ this._updateUI(false);
+ }
+
+ _updateUI(connected, _selectedWsId) {
+ this.statusDot.style.background = connected ? '#34c759' : '#aaa';
+ this.statusDot.style.boxShadow = connected ? '0 0 6px rgba(52,199,89,0.4)' : 'none';
+ this.statusText.textContent = connected ? 'Connected' : 'Disconnected';
+ this.connectBtn.style.display = connected ? 'none' : '';
+ this.disconnectBtn.style.display = connected ? '' : 'none';
+ }
+
+ _showError(msg) {
+ this.statusText.textContent = msg;
+ this.statusText.style.color = '#ff3b30';
+ setTimeout(() => {
+ this.statusText.style.color = '';
+ this.statusText.textContent = 'Disconnected';
+ }, 3000);
+ }
+}
diff --git a/src/popup/popup.html b/src/popup/popup.html
index 4e67d23..ef552a2 100644
--- a/src/popup/popup.html
+++ b/src/popup/popup.html
@@ -1705,6 +1705,34 @@