mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-23 02:07:52 +00:00
chore(chrome-qwen-bridge): connect & them
This commit is contained in:
@@ -32,7 +32,14 @@ function connectToNativeHost() {
|
||||
}
|
||||
|
||||
nativePort.onMessage.addListener((message) => {
|
||||
console.log('Native message received:', message);
|
||||
// 简化日志输出,直接显示 data 内容
|
||||
if (message.type === 'event' && message.data) {
|
||||
console.log('[Native Event]', message.data.type, message.data.update || message.data);
|
||||
} else if (message.type === 'response') {
|
||||
console.log('[Native Response]', 'id:', message.id, message.success ? '✓' : '✗', message.data || message.error);
|
||||
} else {
|
||||
console.log('[Native Message]', message.type, message.data || message);
|
||||
}
|
||||
handleNativeMessage(message);
|
||||
});
|
||||
|
||||
@@ -41,6 +48,7 @@ function connectToNativeHost() {
|
||||
console.log('Native host disconnected');
|
||||
if (error) {
|
||||
console.error('Disconnect error:', error);
|
||||
console.error('Disconnect error message:', error.message);
|
||||
}
|
||||
nativePort = null;
|
||||
isConnected = false;
|
||||
@@ -97,14 +105,35 @@ function handleNativeMessage(message) {
|
||||
delete nativePort._handshakeTimeout;
|
||||
}
|
||||
|
||||
qwenCliStatus = message.qwenStatus || 'connected';
|
||||
// Native host is connected, but Qwen CLI might not be running yet
|
||||
// 'disconnected' from host means Qwen CLI is not running, but we ARE connected to native host
|
||||
const hostQwenStatus = message.qwenStatus || 'disconnected';
|
||||
// Set our status to 'connected' (to native host), or 'running' if Qwen CLI is already running
|
||||
qwenCliStatus = hostQwenStatus === 'running' ? 'running' : 'connected';
|
||||
|
||||
// Notify popup of connection
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'STATUS_UPDATE',
|
||||
status: qwenCliStatus,
|
||||
capabilities: message.capabilities
|
||||
capabilities: message.capabilities,
|
||||
qwenInstalled: message.qwenInstalled,
|
||||
qwenVersion: message.qwenVersion
|
||||
}).catch(() => {});
|
||||
} else if (message.type === 'browser_request') {
|
||||
// Handle browser requests from Qwen CLI via Native Host
|
||||
handleBrowserRequest(message);
|
||||
} else if (message.type === 'permission_request') {
|
||||
// Forward permission request from Native Host to UI
|
||||
console.log('[Permission Request]', message);
|
||||
broadcastToUI({
|
||||
type: 'permissionRequest',
|
||||
data: {
|
||||
requestId: message.requestId,
|
||||
sessionId: message.sessionId,
|
||||
toolCall: message.toolCall,
|
||||
options: message.options
|
||||
}
|
||||
});
|
||||
} else if (message.type === 'response' && message.id !== undefined) {
|
||||
// Handle response to a specific request
|
||||
const handler = pendingRequests.get(message.id);
|
||||
@@ -147,24 +176,226 @@ async function sendToNativeHost(message) {
|
||||
});
|
||||
}
|
||||
|
||||
// Handle events from Qwen CLI
|
||||
function handleQwenEvent(event) {
|
||||
console.log('Qwen event:', event);
|
||||
// Handle browser requests from Qwen CLI (via Native Host)
|
||||
async function handleBrowserRequest(message) {
|
||||
const { browserRequestId, requestType, params } = message;
|
||||
console.log('Browser request:', requestType, params);
|
||||
|
||||
// Forward event to content scripts and popup
|
||||
try {
|
||||
let data;
|
||||
|
||||
switch (requestType) {
|
||||
case 'read_page':
|
||||
data = await getBrowserPageContent();
|
||||
break;
|
||||
|
||||
case 'capture_screenshot':
|
||||
data = await getBrowserScreenshot();
|
||||
break;
|
||||
|
||||
case 'get_network_logs':
|
||||
data = await getBrowserNetworkLogs();
|
||||
break;
|
||||
|
||||
case 'get_console_logs':
|
||||
data = await getBrowserConsoleLogs();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown browser request type: ${requestType}`);
|
||||
}
|
||||
|
||||
// Send response back to native host
|
||||
nativePort.postMessage({
|
||||
type: 'browser_response',
|
||||
browserRequestId,
|
||||
data
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Browser request error:', error);
|
||||
nativePort.postMessage({
|
||||
type: 'browser_response',
|
||||
browserRequestId,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get current page content
|
||||
async function getBrowserPageContent() {
|
||||
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
const tab = tabs[0];
|
||||
|
||||
if (!tab) {
|
||||
throw new Error('No active tab found');
|
||||
}
|
||||
|
||||
// Check if we can access this page
|
||||
if (tab.url && (tab.url.startsWith('chrome://') ||
|
||||
tab.url.startsWith('chrome-extension://') ||
|
||||
tab.url.startsWith('edge://') ||
|
||||
tab.url.startsWith('about:'))) {
|
||||
throw new Error('Cannot access browser internal page');
|
||||
}
|
||||
|
||||
// Try to inject content script
|
||||
try {
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
files: ['content/content-script.js']
|
||||
});
|
||||
} catch (injectError) {
|
||||
console.log('Script injection skipped:', injectError.message);
|
||||
}
|
||||
|
||||
// Request page data from content script
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.tabs.sendMessage(tab.id, { type: 'EXTRACT_DATA' }, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(new Error(chrome.runtime.lastError.message + '. Try refreshing the page.'));
|
||||
} else if (response && response.success) {
|
||||
resolve({
|
||||
url: tab.url,
|
||||
title: tab.title,
|
||||
content: response.data?.content || { text: '', markdown: '' },
|
||||
links: response.data?.links || [],
|
||||
images: response.data?.images || []
|
||||
});
|
||||
} else {
|
||||
reject(new Error(response?.error || 'Failed to extract page data'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Capture screenshot of current tab
|
||||
async function getBrowserScreenshot() {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.tabs.captureVisibleTab(null, { format: 'png' }, (dataUrl) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(new Error(chrome.runtime.lastError.message));
|
||||
} else {
|
||||
resolve({ dataUrl });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Get network logs
|
||||
async function getBrowserNetworkLogs() {
|
||||
// Use the existing getNetworkLogs function
|
||||
const logs = await getNetworkLogs(null);
|
||||
return { logs };
|
||||
}
|
||||
|
||||
// Get console logs (requires content script)
|
||||
async function getBrowserConsoleLogs() {
|
||||
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
const tab = tabs[0];
|
||||
|
||||
if (!tab) {
|
||||
throw new Error('No active tab found');
|
||||
}
|
||||
|
||||
// Check if we can access this page
|
||||
if (tab.url && (tab.url.startsWith('chrome://') ||
|
||||
tab.url.startsWith('chrome-extension://') ||
|
||||
tab.url.startsWith('edge://') ||
|
||||
tab.url.startsWith('about:'))) {
|
||||
throw new Error('Cannot access browser internal page');
|
||||
}
|
||||
|
||||
// Try to inject content script
|
||||
try {
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
files: ['content/content-script.js']
|
||||
});
|
||||
} catch (injectError) {
|
||||
console.log('Script injection skipped:', injectError.message);
|
||||
}
|
||||
|
||||
// Request console logs from content script
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.tabs.sendMessage(tab.id, { type: 'GET_CONSOLE_LOGS' }, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(new Error(chrome.runtime.lastError.message));
|
||||
} else if (response && response.success) {
|
||||
resolve({ logs: response.data || [] });
|
||||
} else {
|
||||
reject(new Error(response?.error || 'Failed to get console logs'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Handle events from Qwen CLI (ACP events)
|
||||
function handleQwenEvent(event) {
|
||||
const eventData = event.data;
|
||||
|
||||
// 简化日志:显示事件类型和关键信息
|
||||
if (eventData?.type === 'session_update') {
|
||||
const update = eventData.update;
|
||||
console.log('[Qwen]', update?.sessionUpdate, update?.content?.text?.slice(0, 50) || update);
|
||||
} else {
|
||||
console.log('[Qwen]', eventData?.type, eventData);
|
||||
}
|
||||
|
||||
// Map ACP events to UI-compatible messages
|
||||
if (eventData?.type === 'session_update') {
|
||||
const update = eventData.update;
|
||||
|
||||
if (update?.sessionUpdate === 'agent_message_chunk') {
|
||||
// Stream chunk
|
||||
broadcastToUI({
|
||||
type: 'streamChunk',
|
||||
data: { chunk: update.content?.text || '' }
|
||||
});
|
||||
} else if (update?.sessionUpdate === 'user_message_chunk') {
|
||||
// User message (usually echo)
|
||||
broadcastToUI({
|
||||
type: 'message',
|
||||
data: {
|
||||
role: 'user',
|
||||
content: update.content?.text || '',
|
||||
timestamp: Date.now()
|
||||
}
|
||||
});
|
||||
} else if (update?.sessionUpdate === 'tool_call' || update?.sessionUpdate === 'tool_call_update') {
|
||||
// Tool call
|
||||
broadcastToUI({
|
||||
type: 'toolCall',
|
||||
data: update
|
||||
});
|
||||
} else if (update?.sessionUpdate === 'plan') {
|
||||
// Plan update
|
||||
broadcastToUI({
|
||||
type: 'plan',
|
||||
data: { entries: update.entries }
|
||||
});
|
||||
}
|
||||
} else if (eventData?.type === 'qwen_stopped') {
|
||||
qwenCliStatus = 'stopped';
|
||||
broadcastToUI({
|
||||
type: 'STATUS_UPDATE',
|
||||
status: 'stopped'
|
||||
});
|
||||
}
|
||||
|
||||
// Also forward raw event for compatibility
|
||||
chrome.tabs.query({}, (tabs) => {
|
||||
tabs.forEach(tab => {
|
||||
chrome.tabs.sendMessage(tab.id, {
|
||||
type: 'QWEN_EVENT',
|
||||
event: event.data
|
||||
}).catch(() => {}); // Ignore errors for tabs without content script
|
||||
event: eventData
|
||||
}).catch(() => {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'QWEN_EVENT',
|
||||
event: event.data
|
||||
}).catch(() => {});
|
||||
// Broadcast message to all UI components (side panel, popup, etc.)
|
||||
function broadcastToUI(message) {
|
||||
chrome.runtime.sendMessage(message).catch(() => {});
|
||||
}
|
||||
|
||||
// Message handlers from extension components
|
||||
@@ -192,17 +423,114 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle sendMessage from side panel (for chat)
|
||||
if (request.type === 'sendMessage') {
|
||||
const text = request.data?.text;
|
||||
if (!text) {
|
||||
sendResponse({ success: false, error: 'No text provided' });
|
||||
return false;
|
||||
}
|
||||
|
||||
// First ensure Qwen CLI is started
|
||||
const startAndSend = async () => {
|
||||
try {
|
||||
// Check if connected
|
||||
if (!isConnected) {
|
||||
await connectToNativeHost();
|
||||
}
|
||||
|
||||
// Start Qwen CLI if not running
|
||||
if (qwenCliStatus !== 'running') {
|
||||
broadcastToUI({ type: 'streamStart' });
|
||||
await sendToNativeHost({
|
||||
type: 'start_qwen',
|
||||
cwd: request.data?.cwd || '/'
|
||||
});
|
||||
qwenCliStatus = 'running';
|
||||
}
|
||||
|
||||
// Send the prompt
|
||||
await sendToNativeHost({
|
||||
type: 'qwen_prompt',
|
||||
text: text
|
||||
});
|
||||
|
||||
sendResponse({ success: true });
|
||||
} catch (error) {
|
||||
console.error('sendMessage error:', error);
|
||||
broadcastToUI({
|
||||
type: 'error',
|
||||
data: { message: error.message }
|
||||
});
|
||||
sendResponse({ success: false, error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
startAndSend();
|
||||
return true; // Will respond asynchronously
|
||||
}
|
||||
|
||||
// Handle cancel streaming
|
||||
if (request.type === 'cancelStreaming') {
|
||||
sendToNativeHost({ type: 'qwen_cancel' })
|
||||
.then(() => {
|
||||
broadcastToUI({ type: 'streamEnd' });
|
||||
sendResponse({ success: true });
|
||||
})
|
||||
.catch(error => {
|
||||
sendResponse({ success: false, error: error.message });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle permission response
|
||||
if (request.type === 'permissionResponse') {
|
||||
sendToNativeHost({
|
||||
type: 'permission_response',
|
||||
requestId: request.data?.requestId,
|
||||
optionId: request.data?.optionId
|
||||
})
|
||||
.then(() => sendResponse({ success: true }))
|
||||
.catch(error => sendResponse({ success: false, error: error.message }));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (request.type === 'EXTRACT_PAGE_DATA') {
|
||||
// Request page data from content script
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
|
||||
if (tabs[0]) {
|
||||
chrome.tabs.sendMessage(tabs[0].id, {
|
||||
const tab = tabs[0];
|
||||
|
||||
// Check if we can inject content script (skip chrome:// and other protected pages)
|
||||
if (tab.url && (tab.url.startsWith('chrome://') ||
|
||||
tab.url.startsWith('chrome-extension://') ||
|
||||
tab.url.startsWith('edge://') ||
|
||||
tab.url.startsWith('about:'))) {
|
||||
sendResponse({
|
||||
success: false,
|
||||
error: 'Cannot access this page (browser internal page)'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to inject content script first in case it's not loaded
|
||||
try {
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
files: ['content/content-script.js']
|
||||
});
|
||||
} catch (injectError) {
|
||||
// Script might already be injected or page doesn't allow injection
|
||||
console.log('Script injection skipped:', injectError.message);
|
||||
}
|
||||
|
||||
chrome.tabs.sendMessage(tab.id, {
|
||||
type: 'EXTRACT_DATA'
|
||||
}, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
sendResponse({
|
||||
success: false,
|
||||
error: chrome.runtime.lastError.message
|
||||
error: chrome.runtime.lastError.message + '. Try refreshing the page.'
|
||||
});
|
||||
} else {
|
||||
sendResponse(response);
|
||||
|
||||
@@ -389,6 +389,14 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
});
|
||||
break;
|
||||
|
||||
case 'GET_CONSOLE_LOGS':
|
||||
// Get captured console logs
|
||||
sendResponse({
|
||||
success: true,
|
||||
data: consoleLogs.slice() // Return a copy
|
||||
});
|
||||
break;
|
||||
|
||||
case 'GET_SELECTED_TEXT':
|
||||
// Get currently selected text
|
||||
sendResponse({
|
||||
|
||||
@@ -318,10 +318,33 @@ getSelectedBtn.addEventListener('click', async () => {
|
||||
throw new Error('No active tab found');
|
||||
}
|
||||
|
||||
// Check if we can access this page
|
||||
if (tab.url && (tab.url.startsWith('chrome://') ||
|
||||
tab.url.startsWith('chrome-extension://') ||
|
||||
tab.url.startsWith('edge://') ||
|
||||
tab.url.startsWith('about:'))) {
|
||||
throw new Error('Cannot access this page (browser internal page)');
|
||||
}
|
||||
|
||||
// Try to inject content script first
|
||||
try {
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
files: ['content/content-script.js']
|
||||
});
|
||||
} catch (injectError) {
|
||||
console.log('Script injection skipped:', injectError.message);
|
||||
}
|
||||
|
||||
// Get selected text from content script
|
||||
const response = await chrome.tabs.sendMessage(tab.id, {
|
||||
type: 'GET_SELECTED_TEXT'
|
||||
});
|
||||
let response;
|
||||
try {
|
||||
response = await chrome.tabs.sendMessage(tab.id, {
|
||||
type: 'GET_SELECTED_TEXT'
|
||||
});
|
||||
} catch (msgError) {
|
||||
throw new Error('Cannot connect to page. Please refresh the page and try again.');
|
||||
}
|
||||
|
||||
if (response.success && response.data) {
|
||||
// Send to Qwen CLI
|
||||
@@ -379,10 +402,33 @@ consoleLogsBtn.addEventListener('click', async () => {
|
||||
throw new Error('No active tab found');
|
||||
}
|
||||
|
||||
// Check if we can access this page
|
||||
if (tab.url && (tab.url.startsWith('chrome://') ||
|
||||
tab.url.startsWith('chrome-extension://') ||
|
||||
tab.url.startsWith('edge://') ||
|
||||
tab.url.startsWith('about:'))) {
|
||||
throw new Error('Cannot access this page (browser internal page)');
|
||||
}
|
||||
|
||||
// Try to inject content script first
|
||||
try {
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
files: ['content/content-script.js']
|
||||
});
|
||||
} catch (injectError) {
|
||||
console.log('Script injection skipped:', injectError.message);
|
||||
}
|
||||
|
||||
// Get console logs from content script
|
||||
const response = await chrome.tabs.sendMessage(tab.id, {
|
||||
type: 'EXTRACT_DATA'
|
||||
});
|
||||
let response;
|
||||
try {
|
||||
response = await chrome.tabs.sendMessage(tab.id, {
|
||||
type: 'EXTRACT_DATA'
|
||||
});
|
||||
} catch (msgError) {
|
||||
throw new Error('Cannot connect to page. Please refresh the page and try again.');
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
const consoleLogs = response.data.consoleLogs || [];
|
||||
|
||||
@@ -3,138 +3,57 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Qwen CLI Bridge</title>
|
||||
<link rel="stylesheet" href="sidepanel.css">
|
||||
<title>Qwen Code</title>
|
||||
<style>
|
||||
/* Base reset and full-height container */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html, body, #root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background-color: #1e1e1e;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
/* Loading state */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 16px;
|
||||
}
|
||||
.loading-spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid #333;
|
||||
border-top-color: #615fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.loading-text {
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<div class="logo">
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<h1>Qwen CLI Bridge</h1>
|
||||
</div>
|
||||
<div class="status-indicator" id="statusIndicator">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-text">Disconnected</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Connection Section -->
|
||||
<section class="section connection-section">
|
||||
<h2>Connection</h2>
|
||||
<div class="connection-controls">
|
||||
<button id="connectBtn" class="btn btn-primary">
|
||||
Connect to Qwen CLI
|
||||
</button>
|
||||
<button id="startQwenBtn" class="btn btn-secondary" disabled>
|
||||
Start Qwen CLI
|
||||
</button>
|
||||
</div>
|
||||
<div id="connectionError" class="error-message" style="display: none;"></div>
|
||||
</section>
|
||||
|
||||
<!-- Actions Section -->
|
||||
<section class="section actions-section">
|
||||
<h2>Quick Actions</h2>
|
||||
<div class="action-buttons">
|
||||
<button id="extractDataBtn" class="action-btn" disabled>
|
||||
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Extract Page Data
|
||||
</button>
|
||||
|
||||
<button id="captureScreenBtn" class="action-btn" disabled>
|
||||
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
Capture Screenshot
|
||||
</button>
|
||||
|
||||
<button id="analyzePageBtn" class="action-btn" disabled>
|
||||
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
Analyze with AI
|
||||
</button>
|
||||
|
||||
<button id="getSelectedBtn" class="action-btn" disabled>
|
||||
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Send Selected Text
|
||||
</button>
|
||||
|
||||
<button id="networkLogsBtn" class="action-btn" disabled>
|
||||
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
|
||||
</svg>
|
||||
Network Logs
|
||||
</button>
|
||||
|
||||
<button id="consoleLogsBtn" class="action-btn" disabled>
|
||||
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Console Logs
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Response Section -->
|
||||
<section class="section response-section" id="responseSection" style="display: none;">
|
||||
<h2>Response</h2>
|
||||
<div class="response-container">
|
||||
<div class="response-header">
|
||||
<span id="responseType" class="response-type"></span>
|
||||
<button id="copyResponseBtn" class="btn-icon" title="Copy to clipboard">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<pre id="responseContent" class="response-content"></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Settings Section -->
|
||||
<section class="section settings-section">
|
||||
<details>
|
||||
<summary>Advanced Settings</summary>
|
||||
<div class="settings-content">
|
||||
<div class="setting-item">
|
||||
<label for="mcpServers">MCP Servers:</label>
|
||||
<input type="text" id="mcpServers" placeholder="chrome-devtools,playwright" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label for="httpPort">HTTP Port:</label>
|
||||
<input type="number" id="httpPort" placeholder="8080" value="8080" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label for="autoConnect">
|
||||
<input type="checkbox" id="autoConnect" />
|
||||
Auto-connect on startup
|
||||
</label>
|
||||
</div>
|
||||
<button id="saveSettingsBtn" class="btn btn-small">Save Settings</button>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<a href="#" id="openOptionsBtn">Options</a>
|
||||
<span>•</span>
|
||||
<a href="#" id="helpBtn">Help</a>
|
||||
<span>•</span>
|
||||
<span class="version">v1.0.0</span>
|
||||
</footer>
|
||||
<div id="root">
|
||||
<div class="loading-container">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">Loading Qwen Code...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="sidepanel.js"></script>
|
||||
<script src="sidepanel-app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user