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

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 -

Muyue Side Panel

+

Muyue

-
-
- Server - - Checking… - -
-
- Active sessions - -
-
- Console errors - 0 -
-
+ -
- -
- - Open Dashboard - -
- -
- -
- - +
+
+
+ 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 `
${p.lang || ''}
${escapeHtml(p.content)}
`; + } + 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 `
${icon}${name}${done ? `${isErr ? '✗' : '✓'}` : ''}
${preview ? `
${escapeHtml(preview)}
` : ''}${resultText ? `
${escapeHtml(resultText)}
` : ''}
`; + } + return ''; + }).join(''); + } else { + if (cleanContent) { + bodyHtml = renderContent(cleanContent).map((p) => { + if (p.type === 'code') { + return `
${p.lang || ''}
${escapeHtml(p.content)}
`; + } + 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 `
${icon}${name}
`; + }).join('') + bodyHtml; + } + } + + if (!bodyHtml) bodyHtml = ''; + + el.innerHTML = ` +
${avatar}
+
+
${label}
+
${bodyHtml}
+
+ `; + 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 += `
${icon}${name}${done ? `${isErr ? '✗' : '✓'}` : ''}
${preview ? `
${escapeHtml(preview)}
` : ''}
`; + } + }); + + if (!html) { + html = ''; + } + + currentStreamingEl.innerHTML = ` +
+
+
GEN
+ ${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" )