diff --git a/.gitea/workflows/ci-develop.yml b/.gitea/workflows/ci-develop.yml
index da9f507..1d58e28 100644
--- a/.gitea/workflows/ci-develop.yml
+++ b/.gitea/workflows/ci-develop.yml
@@ -40,14 +40,6 @@ jobs:
restore-keys: |
${{ runner.os }}-node-web-
- - name: Cache Node modules (extension)
- uses: actions/cache@v4
- with:
- path: extension/node_modules
- key: ${{ runner.os }}-node-ext-${{ hashFiles('extension/package-lock.json') }}
- restore-keys: |
- ${{ runner.os }}-node-ext-
-
- name: Download Go dependencies
run: go mod download
@@ -62,7 +54,6 @@ jobs:
cd extension
npm ci
npx wxt zip
- npx wxt zip --browser firefox
mkdir -p ../dist
mv .output/muyue-extension-*.zip ../dist/
diff --git a/.gitea/workflows/ci-main.yml b/.gitea/workflows/ci-main.yml
index bbdf3dc..772ba02 100644
--- a/.gitea/workflows/ci-main.yml
+++ b/.gitea/workflows/ci-main.yml
@@ -40,14 +40,6 @@ jobs:
restore-keys: |
${{ runner.os }}-node-web-
- - name: Cache Node modules (extension)
- uses: actions/cache@v4
- with:
- path: extension/node_modules
- key: ${{ runner.os }}-node-ext-${{ hashFiles('extension/package-lock.json') }}
- restore-keys: |
- ${{ runner.os }}-node-ext-
-
- name: Download dependencies
run: go mod download
@@ -62,7 +54,6 @@ jobs:
cd extension
npm ci
npx wxt zip
- npx wxt zip --browser firefox
mkdir -p ../dist
mv .output/muyue-extension-*.zip ../dist/
diff --git a/Makefile b/Makefile
index b80df4c..7076fc5 100644
--- a/Makefile
+++ b/Makefile
@@ -9,7 +9,7 @@ WEB_DIR = web
EXT_DIR = extension
-.PHONY: build install clean test test-short run scan fmt lint build-all deps vet frontend dev-desktop ext ext-chrome ext-firefox ext-zip
+.PHONY: build install clean test test-short run scan fmt lint build-all deps vet frontend dev-desktop ext ext-zip
frontend:
cd $(WEB_DIR) && $(NPM) ci && $(NPM) run build
@@ -66,16 +66,10 @@ build-all: frontend
GOOS=windows GOARCH=arm64 $(GO) build -o dist/$(BINARY)-windows-arm64.exe ./cmd/muyue/
ext:
- cd $(EXT_DIR) && $(NPM) ci && $(NPM) run build && $(NPM) run build:firefox
-
-ext-chrome:
cd $(EXT_DIR) && $(NPM) ci && $(NPM) run build
-ext-firefox:
- cd $(EXT_DIR) && $(NPM) ci && $(NPM) run build:firefox
-
ext-zip:
- cd $(EXT_DIR) && $(NPM) ci && $(NPM) run zip && $(NPM) run zip:firefox
+ cd $(EXT_DIR) && $(NPM) ci && $(NPM) run zip
deps:
$(GO) mod tidy
diff --git a/extension/package.json b/extension/package.json
index 87a2dc2..810b8df 100644
--- a/extension/package.json
+++ b/extension/package.json
@@ -1,14 +1,12 @@
{
"name": "muyue-extension",
- "version": "0.8.0",
+ "version": "0.9.0",
"private": true,
"type": "module",
"scripts": {
"dev": "wxt",
"build": "wxt build",
- "build:firefox": "wxt build --browser firefox",
- "zip": "wxt zip",
- "zip:firefox": "wxt zip --browser firefox"
+ "zip": "wxt zip"
},
"dependencies": {
"wxt": "^0.20"
diff --git a/extension/src/entrypoints/popup/index.html b/extension/src/entrypoints/popup/index.html
index be5dc25..716b8df 100644
--- a/extension/src/entrypoints/popup/index.html
+++ b/extension/src/entrypoints/popup/index.html
@@ -5,7 +5,7 @@
-
+
Muyue
@@ -33,7 +33,7 @@
Open Dashboard
@@ -46,7 +46,7 @@
diff --git a/extension/src/entrypoints/sidepanel/index.html b/extension/src/entrypoints/sidepanel/index.html
index 9e3aa3f..7fd3f3a 100644
--- a/extension/src/entrypoints/sidepanel/index.html
+++ b/extension/src/entrypoints/sidepanel/index.html
@@ -2,50 +2,83 @@
-
+
- Muyue Side Panel
+ Muyue
-
-
- Server
-
- Checking…
-
-
-
- Active sessions
- —
-
-
- Console errors
- 0
-
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Server offline
+
+
+
diff --git a/extension/src/entrypoints/sidepanel/main.js b/extension/src/entrypoints/sidepanel/main.js
index 06050bc..b5d398c 100644
--- a/extension/src/entrypoints/sidepanel/main.js
+++ b/extension/src/entrypoints/sidepanel/main.js
@@ -1,13 +1,30 @@
import '../../styles/panel.css';
-import { getServerUrl, setServerUrl, fetchSessions } from '../../lib/config';
+import { getServerUrl, setServerUrl, fetchSessions, checkServerHealth } from '../../lib/config';
+import { getChatHistory, sendChat, clearChat } from '../../lib/api';
-const $serverStatus = document.getElementById('server-status');
-const $sessionCount = document.getElementById('session-count');
-const $errorCount = document.getElementById('error-count');
-const $sessionsList = document.getElementById('sessions-list');
-const $btnDashboard = document.getElementById('btn-dashboard');
-const $serverUrl = document.getElementById('server-url');
-const $btnSaveUrl = document.getElementById('btn-save-url');
+const $ = (s) => document.querySelector(s);
+const $$ = (s) => document.querySelectorAll(s);
+
+const $serverStatus = $('#server-status');
+const $sessionCount = $('#session-count');
+const $errorCount = $('#error-count');
+const $sessionsList = $('#sessions-list');
+const $btnDashboard = $('#btn-dashboard');
+const $serverUrl = $('#server-url');
+const $btnSaveUrl = $('#btn-save-url');
+const $chatOffline = $('#chat-offline');
+const $chatArea = $('#chat-area');
+const $chatFeed = $('#chat-feed');
+const $chatStreaming = $('#chat-streaming');
+const $chatInput = $('#chat-input');
+const $chatSend = $('#chat-send');
+const $chatStop = $('#chat-stop');
+
+let serverOnline = false;
+let messages = [];
+let loading = false;
+let abortController = null;
+let currentStreamingEl = null;
function dot(color) {
return `
`;
@@ -18,7 +35,6 @@ function renderSessions(sessions) {
$sessionsList.innerHTML = '';
return;
}
-
$sessionsList.innerHTML = `
@@ -38,28 +54,360 @@ function renderSessions(sessions) {
`;
}
-async function refresh() {
- const url = await getServerUrl();
- $serverUrl.value = url;
- $btnDashboard.href = url;
+function formatText(text) {
+ let html = text
+ .replace(/&/g, '&').replace(//g, '>');
+ html = html
+ .replace(/\*\*(.+?)\*\*/g, '
$1')
+ .replace(/`([^`]+)`/g, '
$1')
+ .replace(/^### (.+)$/gm, '
$1
')
+ .replace(/^## (.+)$/gm, '
$1
')
+ .replace(/^# (.+)$/gm, '
$1
')
+ .replace(/^\s*[-*] (.+)$/gm, '
• $1
')
+ .replace(/^\s*(\d+)[.)] (.+)$/gm, '
$1 $2
')
+ .replace(/\n/g, '
');
+ html = html
+ .replace(/
\s*
/g, '
')
+ .replace(/
\s*(
${escapeHtml(msg.content)}
`;
+ return el;
}
- chrome.runtime.sendMessage({ type: 'get_state' }, (state) => {
- if (chrome.runtime.lastError || !state) return;
- $errorCount.textContent = state.errorCount || 0;
+ const isUser = msg.role === 'user';
+ const avatar = isUser ? '★' : '◆';
+ const label = isUser ? 'CDT' : 'GEN';
+
+ let displayContent = msg.content;
+ let parsedToolCalls = null;
+ let parsedSegments = null;
+
+ try {
+ const parsed = JSON.parse(msg.content);
+ if (parsed && Array.isArray(parsed.segments)) {
+ parsedSegments = parsed.segments;
+ displayContent = parsed.content || '';
+ } else if (parsed && Array.isArray(parsed.tool_calls)) {
+ parsedToolCalls = parsed.tool_calls;
+ displayContent = parsed.content || '';
+ }
+ } catch {}
+
+ const cleanContent = displayContent.replace(/
]*>[\s\S]*?<\/think>/gi, '');
+
+ let bodyHtml = '';
+ if (parsedSegments) {
+ bodyHtml = parsedSegments.map((seg) => {
+ if (seg.type === 'text' && seg.content) {
+ const c = seg.content.replace(/]*>[\s\S]*?<\/think>/gi, '');
+ if (!c) return '';
+ return renderContent(c).map((p) => {
+ if (p.type === 'code') {
+ return ``;
+ }
+ return `${formatText(p.content)}`;
+ }).join('');
+ }
+ if (seg.type === 'tool') {
+ const name = seg.call?.name || 'tool';
+ const icon = { terminal: '⌨', crush_run: '⚡', read_file: '📄', list_files: '📁', search_files: '🔍', web_fetch: '🌐' }[name] || '🔧';
+ const done = seg.result;
+ const isErr = done && done.is_error;
+ const preview = (() => {
+ try {
+ const args = typeof seg.call.args === 'string' ? JSON.parse(seg.call.args) : seg.call.args;
+ return args.command || args.task || args.path || args.url || JSON.stringify(args).slice(0, 60);
+ } catch { return ''; }
+ })();
+ const resultText = done ? (done.content || '').slice(0, 500) : '';
+ return ``;
+ }
+ return '';
+ }).join('');
+ } else {
+ if (cleanContent) {
+ bodyHtml = renderContent(cleanContent).map((p) => {
+ if (p.type === 'code') {
+ return ``;
+ }
+ return `${formatText(p.content)}`;
+ }).join('');
+ }
+ if (parsedToolCalls && parsedToolCalls.length > 0) {
+ bodyHtml = parsedToolCalls.map((tc) => {
+ const name = tc.name || 'tool';
+ const icon = { terminal: '⌨', crush_run: '⚡', read_file: '📄', web_fetch: '🌐' }[name] || '🔧';
+ return ``;
+ }).join('') + bodyHtml;
+ }
+ }
+
+ if (!bodyHtml) bodyHtml = '';
+
+ el.innerHTML = `
+ ${avatar}
+
+ `;
+ return el;
+}
+
+function renderMessages() {
+ $chatFeed.innerHTML = '';
+ messages.forEach((msg) => {
+ $chatFeed.appendChild(createMessageEl(msg));
+ });
+ scrollToBottom();
+}
+
+function scrollToBottom() {
+ requestAnimationFrame(() => {
+ $chatFeed.scrollTop = $chatFeed.scrollHeight;
});
}
+function switchTab(tabName) {
+ $$('.tab').forEach((t) => t.classList.toggle('active', t.dataset.tab === tabName));
+ $$('.tab-content').forEach((s) => s.classList.toggle('active', s.id === `tab-${tabName}`));
+}
+
+function updateChatVisibility() {
+ if (serverOnline) {
+ $chatOffline.style.display = 'none';
+ $chatArea.style.display = 'flex';
+ } else {
+ $chatOffline.style.display = 'flex';
+ $chatArea.style.display = 'none';
+ }
+}
+
+async function loadChatHistory() {
+ try {
+ const data = await getChatHistory();
+ if (data.messages && data.messages.length > 0) {
+ messages = data.messages;
+ } else {
+ messages = [{ id: 'welcome', role: 'system', content: 'Ready. Type a message to start.' }];
+ }
+ renderMessages();
+ } catch {
+ messages = [{ id: 'welcome', role: 'system', content: 'Ready. Type a message to start.' }];
+ renderMessages();
+ }
+}
+
+async function handleSend() {
+ const text = $chatInput.value.trim();
+ if (!text || loading) return;
+
+ if (text === '/clear') {
+ try { await clearChat(); } catch {}
+ messages = [{ id: 'clear-' + Date.now(), role: 'system', content: 'Conversation cleared.' }];
+ renderMessages();
+ $chatInput.value = '';
+ return;
+ }
+
+ $chatInput.value = '';
+ $chatInput.style.height = 'auto';
+
+ const userMsg = { id: Date.now().toString(), role: 'user', content: text };
+ messages.push(userMsg);
+ $chatFeed.appendChild(createMessageEl(userMsg));
+ scrollToBottom();
+
+ loading = true;
+ $chatSend.style.display = 'none';
+ $chatStop.style.display = 'flex';
+
+ const controller = new AbortController();
+ abortController = controller;
+
+ let segments = [];
+ let thinking = '';
+ let textStartIdx = 0;
+ let streamText = '';
+
+ const updateLastText = (text) => {
+ if (!text) return;
+ const last = segments.length > 0 ? segments[segments.length - 1] : null;
+ if (last && last.type === 'text') {
+ last.content = text;
+ } else {
+ segments.push({ type: 'text', content: text });
+ }
+ };
+
+ currentStreamingEl = document.createElement('div');
+ currentStreamingEl.className = 'chat-msg assistant streaming';
+ $chatFeed.appendChild(currentStreamingEl);
+ scrollToBottom();
+
+ try {
+ const finalContent = await sendChat(text, true, (partial, event) => {
+ if (event && (event.thinking !== undefined || event.thinking_start || event.thinking_end)) {
+ if (event.thinking !== undefined) thinking += event.thinking;
+ return;
+ }
+ if (event && event.tool_call) {
+ updateLastText(partial.slice(textStartIdx));
+ textStartIdx = partial.length;
+ segments.push({ type: 'tool', call: event.tool_call, result: null });
+ } else if (event && event.tool_result) {
+ const segIdx = segments.findIndex((s) => s.type === 'tool' && s.call && s.call.tool_call_id === event.tool_result.tool_call_id);
+ if (segIdx >= 0) segments[segIdx].result = event.tool_result;
+ } else {
+ updateLastText(partial.slice(textStartIdx));
+ }
+ streamText = partial;
+
+ const allText = segments.filter((s) => s.type === 'text').map((s) => s.content).join('');
+ const toolSegs = segments.filter((s) => s.type === 'tool');
+
+ let html = '';
+ if (thinking) {
+ html += `⏱ Thinking…
`;
+ }
+ segments.forEach((seg) => {
+ if (seg.type === 'text' && seg.content) {
+ const c = seg.content.replace(/]*>[\s\S]*?<\/think>/gi, '');
+ if (c) html += `${formatText(c)}
`;
+ }
+ if (seg.type === 'tool') {
+ const name = seg.call?.name || 'tool';
+ const icon = { terminal: '⌨', crush_run: '⚡', read_file: '📄', web_fetch: '🌐' }[name] || '🔧';
+ const done = seg.result;
+ const isErr = done && done.is_error;
+ const preview = (() => {
+ try {
+ const args = typeof seg.call.args === 'string' ? JSON.parse(seg.call.args) : seg.call.args;
+ return args.command || args.task || args.path || JSON.stringify(args).slice(0, 60);
+ } catch { return ''; }
+ })();
+ html += ``;
+ }
+ });
+
+ if (!html) {
+ html = '';
+ }
+
+ currentStreamingEl.innerHTML = `
+ ◆
+
+
+ ${html}
+
+
+ `;
+ scrollToBottom();
+ }, controller.signal);
+
+ if (currentStreamingEl && currentStreamingEl.parentNode) {
+ currentStreamingEl.remove();
+ }
+
+ const allText = segments.filter((s) => s.type === 'text').map((s) => s.content).join('');
+ const toolSegs = segments.filter((s) => s.type === 'tool');
+ const aiMsg = {
+ id: (Date.now() + 1).toString(),
+ role: 'assistant',
+ content: toolSegs.length > 0 ? JSON.stringify({
+ segments: segments.map((s) => s.type === 'text'
+ ? { type: 'text', content: s.content }
+ : { type: 'tool', call: s.call, result: s.result ? { content: s.result.content || '', is_error: s.result.is_error || false, tool_call_id: s.call?.tool_call_id } : null }),
+ content: allText,
+ }) : (allText || finalContent),
+ };
+ messages.push(aiMsg);
+ $chatFeed.appendChild(createMessageEl(aiMsg));
+ scrollToBottom();
+ } catch (err) {
+ if (currentStreamingEl && currentStreamingEl.parentNode) {
+ currentStreamingEl.remove();
+ }
+ if (err.name !== 'AbortError') {
+ const errMsg = { id: (Date.now() + 1).toString(), role: 'system', content: `Error: ${err.message}` };
+ messages.push(errMsg);
+ $chatFeed.appendChild(createMessageEl(errMsg));
+ scrollToBottom();
+ }
+ } finally {
+ loading = false;
+ abortController = null;
+ currentStreamingEl = null;
+ $chatSend.style.display = 'flex';
+ $chatStop.style.display = 'none';
+ }
+}
+
+$$('.tab').forEach((tab) => {
+ tab.addEventListener('click', () => switchTab(tab.dataset.tab));
+});
+
+$chatInput.addEventListener('input', () => {
+ $chatInput.style.height = 'auto';
+ $chatInput.style.height = Math.min($chatInput.scrollHeight, 100) + 'px';
+});
+
+$chatInput.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ handleSend();
+ }
+});
+
+$chatSend.addEventListener('click', handleSend);
+$chatStop.addEventListener('click', () => {
+ if (abortController) abortController.abort();
+});
+
+$chatFeed.addEventListener('click', (e) => {
+ const btn = e.target.closest('.chat-copy-btn');
+ if (btn) {
+ navigator.clipboard.writeText(decodeURIComponent(btn.dataset.code));
+ const orig = btn.textContent;
+ btn.textContent = 'Copied!';
+ setTimeout(() => { btn.textContent = orig; }, 1200);
+ }
+});
+
$btnSaveUrl.addEventListener('click', async () => {
const url = $serverUrl.value.trim().replace(/\/$/, '');
if (url) {
@@ -68,5 +416,32 @@ $btnSaveUrl.addEventListener('click', async () => {
}
});
+async function refresh() {
+ const url = await getServerUrl();
+ $serverUrl.value = url;
+ $btnDashboard.href = url;
+
+ try {
+ const sessions = await fetchSessions();
+ serverOnline = true;
+ $serverStatus.innerHTML = `${dot('green')} Online`;
+ $sessionCount.textContent = sessions.length;
+ renderSessions(sessions);
+ } catch {
+ serverOnline = false;
+ $serverStatus.innerHTML = `${dot('red')} Offline`;
+ $sessionCount.textContent = '—';
+ $sessionsList.innerHTML = '';
+ }
+
+ updateChatVisibility();
+
+ chrome.runtime.sendMessage({ type: 'get_state' }, (state) => {
+ if (chrome.runtime.lastError || !state) return;
+ $errorCount.textContent = state.errorCount || 0;
+ });
+}
+
refresh();
-setInterval(refresh, 5000);
+loadChatHistory();
+setInterval(refresh, 10000);
diff --git a/extension/src/lib/api.js b/extension/src/lib/api.js
new file mode 100644
index 0000000..c31c76f
--- /dev/null
+++ b/extension/src/lib/api.js
@@ -0,0 +1,77 @@
+import { getServerUrl } from './config';
+
+async function request(path, options = {}) {
+ const base = await getServerUrl();
+ const res = await fetch(`${base}/api${path}`, {
+ ...options,
+ headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
+ });
+ if (!res.ok) {
+ const err = await res.json().catch(() => ({ error: res.statusText }));
+ throw new Error(err.error || res.statusText);
+ }
+ return res.json();
+}
+
+export async function getChatHistory() {
+ return request('/chat/history');
+}
+
+export async function clearChat() {
+ return request('/chat/clear', { method: 'POST' });
+}
+
+export async function summarizeChat() {
+ return request('/chat/summarize', { method: 'POST' });
+}
+
+export async function sendChat(message, stream = true, onChunk, signal) {
+ const base = await getServerUrl();
+
+ if (!stream) {
+ return request('/chat', {
+ method: 'POST',
+ body: JSON.stringify({ message, stream: false }),
+ });
+ }
+
+ return new Promise((resolve, reject) => {
+ fetch(`${base}/api/chat`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ message, stream: true }),
+ signal,
+ }).then(async (res) => {
+ if (!res.ok) {
+ const err = await res.json().catch(() => ({ error: res.statusText }));
+ reject(new Error(err.error || res.statusText));
+ return;
+ }
+ const reader = res.body.getReader();
+ const decoder = new TextDecoder();
+ let full = '';
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ const text = decoder.decode(value, { stream: true });
+ for (const line of text.split('\n')) {
+ if (!line.startsWith('data: ')) continue;
+ try {
+ const data = JSON.parse(line.slice(6));
+ if (data.error) { reject(new Error(data.error)); return; }
+ if (data.done) { resolve(full); return; }
+ if (data.content) {
+ full += data.content;
+ if (onChunk) onChunk(full, data);
+ } else if (data.thinking !== undefined || data.thinking_end) {
+ if (onChunk) onChunk(full, data);
+ } else if (data.tool_call || data.tool_result) {
+ if (onChunk) onChunk(full, data);
+ }
+ } catch {}
+ }
+ }
+ resolve(full);
+ }).catch(reject);
+ });
+}
diff --git a/extension/src/styles/panel.css b/extension/src/styles/panel.css
index 2985a3b..9aa5c28 100644
--- a/extension/src/styles/panel.css
+++ b/extension/src/styles/panel.css
@@ -30,17 +30,19 @@ body {
}
.panel {
- width: 320px;
- padding: 16px;
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ overflow: hidden;
}
-header {
+.panel > header {
display: flex;
align-items: center;
gap: 10px;
- margin-bottom: 16px;
- padding-bottom: 12px;
+ padding: 12px 16px;
border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
}
header img {
@@ -54,6 +56,47 @@ header h1 {
letter-spacing: -0.3px;
}
+.tabs {
+ display: flex;
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+ padding: 0 8px;
+}
+
+.tab {
+ flex: 1;
+ padding: 10px 8px;
+ background: none;
+ border: none;
+ border-bottom: 2px solid transparent;
+ color: var(--text-secondary);
+ font-size: 12px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.15s;
+}
+
+.tab:hover {
+ color: var(--text-primary);
+}
+
+.tab.active {
+ color: var(--accent);
+ border-bottom-color: var(--accent);
+}
+
+.tab-content {
+ display: none;
+ flex: 1;
+ overflow-y: auto;
+ padding: 12px 16px;
+}
+
+.tab-content.active {
+ display: flex;
+ flex-direction: column;
+}
+
.status-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
@@ -189,12 +232,13 @@ header h1 {
}
.footer {
- margin-top: 12px;
- padding-top: 10px;
+ margin-top: auto;
+ padding: 10px 16px;
border-top: 1px solid var(--border);
text-align: center;
color: var(--text-secondary);
font-size: 10px;
+ flex-shrink: 0;
}
.footer span {
@@ -209,3 +253,351 @@ header h1 {
.loading {
animation: pulse 1.5s ease-in-out infinite;
}
+
+.chat-offline {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ color: var(--text-secondary);
+ font-size: 13px;
+}
+
+.chat-area {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ gap: 0;
+}
+
+.chat-feed {
+ flex: 1;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding-bottom: 8px;
+}
+
+.chat-msg {
+ display: flex;
+ gap: 8px;
+ max-width: 100%;
+}
+
+.chat-msg.system {
+ align-items: center;
+ gap: 6px;
+ padding: 6px 0;
+}
+
+.chat-system-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--text-secondary);
+ flex-shrink: 0;
+}
+
+.chat-system-text {
+ color: var(--text-secondary);
+ font-size: 12px;
+ font-style: italic;
+}
+
+.chat-avatar {
+ width: 28px;
+ height: 28px;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ font-weight: 700;
+ flex-shrink: 0;
+}
+
+.chat-avatar.user {
+ background: rgba(255, 215, 64, 0.15);
+ color: #FFD740;
+}
+
+.chat-avatar.ai {
+ background: rgba(255, 145, 0, 0.15);
+ color: #FF9100;
+}
+
+.chat-body {
+ flex: 1;
+ min-width: 0;
+ overflow-wrap: break-word;
+}
+
+.chat-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-bottom: 4px;
+}
+
+.chat-badge {
+ font-size: 10px;
+ font-weight: 700;
+ padding: 1px 5px;
+ border-radius: 3px;
+ border: 1px solid;
+ line-height: 1.3;
+}
+
+.chat-content {
+ font-size: 13px;
+ line-height: 1.5;
+}
+
+.chat-content h2 { font-size: 14px; margin: 8px 0 4px; }
+.chat-content h3 { font-size: 13px; margin: 6px 0 3px; }
+.chat-content h4 { font-size: 12px; margin: 4px 0 2px; }
+.chat-content strong { color: #fff; }
+.chat-bullet, .chat-step {
+ padding-left: 4px;
+ margin: 2px 0;
+}
+.chat-step-num {
+ display: inline-block;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ background: var(--accent-dim);
+ text-align: center;
+ line-height: 18px;
+ font-size: 10px;
+ font-weight: 600;
+ margin-right: 4px;
+}
+.inline-code {
+ background: rgba(255, 255, 255, 0.08);
+ padding: 1px 5px;
+ border-radius: 3px;
+ font-family: var(--font-mono);
+ font-size: 12px;
+}
+
+.chat-code-block {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ margin: 6px 0;
+ overflow: hidden;
+}
+
+.chat-code-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 4px 8px;
+ background: rgba(255, 255, 255, 0.03);
+ border-bottom: 1px solid var(--border);
+}
+
+.chat-code-lang {
+ font-size: 10px;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+}
+
+.chat-copy-btn {
+ background: none;
+ border: none;
+ color: var(--accent);
+ font-size: 10px;
+ cursor: pointer;
+ padding: 2px 6px;
+ border-radius: 3px;
+}
+
+.chat-copy-btn:hover {
+ background: var(--accent-dim);
+}
+
+.chat-code-block pre {
+ padding: 8px;
+ overflow-x: auto;
+ font-family: var(--font-mono);
+ font-size: 11px;
+ line-height: 1.4;
+}
+
+.chat-code-block code {
+ font-family: inherit;
+}
+
+.chat-tool {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 8px 10px;
+ margin: 6px 0;
+ font-size: 12px;
+}
+
+.chat-tool.error {
+ border-color: var(--red);
+}
+
+.chat-tool-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.chat-tool-icon {
+ font-size: 14px;
+}
+
+.chat-tool-status {
+ margin-left: auto;
+ font-weight: 700;
+}
+
+.chat-tool-status.ok { color: var(--green); }
+.chat-tool-status.err { color: var(--red); }
+
+.chat-tool-args {
+ color: var(--text-secondary);
+ font-family: var(--font-mono);
+ font-size: 11px;
+ margin-top: 4px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.chat-tool-result {
+ margin-top: 6px;
+ padding-top: 6px;
+ border-top: 1px solid var(--border);
+ font-family: var(--font-mono);
+ font-size: 11px;
+ color: var(--text-secondary);
+ max-height: 120px;
+ overflow-y: auto;
+}
+
+.chat-thinking {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 8px;
+ background: rgba(255, 255, 255, 0.03);
+ border-radius: 6px;
+ margin-bottom: 6px;
+ font-size: 11px;
+ color: var(--text-secondary);
+}
+
+.chat-thinking-icon {
+ font-size: 13px;
+}
+
+@keyframes chatDotPulse {
+ 0%, 80%, 100% { opacity: 0.2; transform: scale(0.8); }
+ 40% { opacity: 1; transform: scale(1); }
+}
+
+.chat-dots {
+ display: inline-flex;
+ gap: 4px;
+ padding: 4px 0;
+}
+
+.chat-dots span {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--text-secondary);
+ animation: chatDotPulse 1.2s ease-in-out infinite;
+}
+
+.chat-dots span:nth-child(2) { animation-delay: 0.2s; }
+.chat-dots span:nth-child(3) { animation-delay: 0.4s; }
+
+@keyframes cursorBlink {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0; }
+}
+
+.chat-cursor {
+ display: inline-block;
+ width: 2px;
+ height: 14px;
+ background: var(--accent);
+ margin-left: 2px;
+ vertical-align: text-bottom;
+ animation: cursorBlink 0.8s ease infinite;
+}
+
+.chat-input-row {
+ display: flex;
+ align-items: flex-end;
+ gap: 6px;
+ padding: 8px 0 0;
+ border-top: 1px solid var(--border);
+ flex-shrink: 0;
+}
+
+.chat-input-row textarea {
+ flex: 1;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 8px 10px;
+ color: var(--text-primary);
+ font-family: var(--font-sans);
+ font-size: 13px;
+ resize: none;
+ outline: none;
+ max-height: 100px;
+ line-height: 1.4;
+}
+
+.chat-input-row textarea:focus {
+ border-color: var(--accent);
+}
+
+.chat-send-btn, .chat-stop-btn {
+ width: 36px;
+ height: 36px;
+ border-radius: 8px;
+ border: 1px solid var(--accent);
+ background: var(--accent);
+ color: #fff;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ transition: all 0.15s;
+}
+
+.chat-send-btn:hover, .chat-stop-btn:hover {
+ box-shadow: 0 0 12px var(--accent-glow);
+}
+
+.chat-stop-btn {
+ background: var(--bg-secondary);
+ border-color: var(--border);
+ color: var(--text-primary);
+}
+
+.chat-msg.user .chat-content {
+ background: var(--bg-tertiary);
+ padding: 8px 10px;
+ border-radius: 0 8px 8px 8px;
+}
+
+.chat-msg.assistant .chat-content {
+ padding: 2px 0;
+}
diff --git a/extension/wxt.config.js b/extension/wxt.config.js
index 4d0e730..79f232e 100644
--- a/extension/wxt.config.js
+++ b/extension/wxt.config.js
@@ -2,19 +2,17 @@ import { defineConfig } from 'wxt';
export default defineConfig({
srcDir: 'src',
- suppressWarnings: {
- firefoxDataCollection: true,
- },
- manifest: ({ browser }) => ({
+ manifest: {
name: 'Muyue',
description: 'AI-powered browser testing & automation — connected to your Muyue desktop app',
permissions: [
'storage',
'activeTab',
'tabs',
- ...(browser === 'chrome' ? ['sidePanel'] : []),
+ 'sidePanel',
'scripting',
'notifications',
+ 'alarms',
],
host_permissions: ['http://127.0.0.1:*/*', 'http://localhost:*/*'],
action: {
@@ -26,5 +24,5 @@ export default defineConfig({
side_panel: {
default_path: 'sidepanel.html',
},
- }),
+ },
});
diff --git a/internal/version/version.go b/internal/version/version.go
index e974069..3973f86 100644
--- a/internal/version/version.go
+++ b/internal/version/version.go
@@ -7,7 +7,7 @@ import (
const (
Name = "muyue"
- Version = "0.8.0"
+ Version = "0.9.0"
Author = "La Légion de Muyue"
)