Compare commits

...

3 Commits

Author SHA1 Message Date
Augustin
b40bc291ba fix(ext): use <all_urls> host_permissions to allow injection on any page
All checks were successful
Beta Release / beta (push) Successful in 1m41s
Stable Release / stable (push) Successful in 1m39s
💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-28 19:08:02 +02:00
Augustin
1216c80118 chore: bump version to v0.9.6
Some checks failed
Beta Release / beta (push) Successful in 1m50s
Stable Release / stable (push) Has been cancelled
💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-28 19:04:58 +02:00
Augustin
eda493856a feat(ext): add session inject button, auto-inject toggle, and live Studio feed in sidepanel, bump v0.9.6
Some checks failed
Beta Release / beta (push) Successful in 1m40s
Stable Release / stable (push) Has been cancelled
- Configuration tab: inject session script button + auto-inject toggle
- Chat tab replaced by read-only live Studio feed mirror (polls /chat/history)
- content.js now checks auto-inject flag before initializing

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-28 19:02:04 +02:00
10 changed files with 258 additions and 207 deletions

View File

@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## v0.9.6
### Changes since v0.9.5
- feat(ext): add session inject button and auto-inject toggle in sidepanel Configuration tab
- feat(ext): replace interactive Chat tab with live read-only Studio feed mirror
- chore(ext): bump extension to v0.9.6
## v0.9.5 ## v0.9.5
### Changes since v0.9.4 ### Changes since v0.9.4

View File

@@ -1,12 +1,12 @@
{ {
"name": "muyue-extension", "name": "muyue-extension",
"version": "0.1.0", "version": "0.9.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "muyue-extension", "name": "muyue-extension",
"version": "0.1.0", "version": "0.9.0",
"dependencies": { "dependencies": {
"wxt": "^0.20" "wxt": "^0.20"
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "muyue-extension", "name": "muyue-extension",
"version": "0.9.0", "version": "0.9.6",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -3,7 +3,9 @@ import { dispatch } from '../lib/page-rpc';
export default defineContentScript({ export default defineContentScript({
matches: ['http://*/*', 'https://*/*'], matches: ['http://*/*', 'https://*/*'],
runAt: 'document_idle', runAt: 'document_idle',
main() { async main() {
const autoInjectResult = await chrome.storage.local.get('muyue_auto_inject');
if (!autoInjectResult.muyue_auto_inject) return;
if (window.__muyueExtension) return; if (window.__muyueExtension) return;
window.__muyueExtension = true; window.__muyueExtension = true;

View File

@@ -46,7 +46,7 @@
</div> </div>
<div class="footer"> <div class="footer">
<span>Muyue</span> extension v0.9.0 <span>Muyue</span> extension v0.9.6
</div> </div>
</div> </div>

View File

@@ -40,6 +40,26 @@
<a id="btn-dashboard" href="#" class="btn btn-primary" target="_blank"> <a id="btn-dashboard" href="#" class="btn btn-primary" target="_blank">
Open Dashboard Open Dashboard
</a> </a>
<button id="btn-inject-session" class="btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
</svg>
Injecter le script de session
</button>
<div class="inject-status" id="inject-status" style="display:none"></div>
</div>
<div class="toggle-section">
<div class="toggle-row">
<span class="toggle-label">Auto-injection</span>
<label class="toggle-switch">
<input type="checkbox" id="toggle-auto-inject" />
<span class="toggle-slider"></span>
</label>
</div>
<div class="toggle-desc">
Injecte automatiquement le script de session dans chaque page visitée
</div>
</div> </div>
<div class="settings-section"> <div class="settings-section">
@@ -59,28 +79,16 @@
<span>Server offline</span> <span>Server offline</span>
</div> </div>
<div id="chat-area" class="studio-feed-layout" style="display:none"> <div id="chat-area" class="studio-feed-layout" style="display:none">
<div id="chat-feed" class="studio-feed"></div> <div class="chat-live-header">
<div class="studio-input-area"> <svg width="12" height="12" viewBox="0 0 24 24" fill="var(--accent)"><circle cx="12" cy="12" r="5"/></svg>
<div class="studio-input-row"> <span>Live — Studio</span>
<textarea id="chat-input" placeholder="Envoyer un message…" rows="1"></textarea>
<button id="chat-send" class="studio-send-btn">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
</svg>
</button>
<button id="chat-stop" class="studio-stop-btn" style="display:none">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<rect x="4" y="4" width="16" height="16" rx="2"/>
</svg>
</button>
</div>
<div class="studio-input-hint">/clear /help</div>
</div> </div>
<div id="chat-feed" class="studio-feed"></div>
</div> </div>
</section> </section>
<div class="footer"> <div class="footer">
<span>Muyue</span> extension v0.9.0 <span>Muyue</span> extension v0.9.6
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
import '../../styles/panel.css'; import '../../styles/panel.css';
import { getServerUrl, setServerUrl, fetchSessions, checkServerHealth } from '../../lib/config'; import { getServerUrl, setServerUrl, fetchSessions, checkServerHealth } from '../../lib/config';
import { getChatHistory, sendChat, clearChat } from '../../lib/api'; import { getChatHistory } from '../../lib/api';
const $ = (s) => document.querySelector(s); const $ = (s) => document.querySelector(s);
const $$ = (s) => document.querySelectorAll(s); const $$ = (s) => document.querySelectorAll(s);
@@ -15,16 +15,13 @@ const $btnSaveUrl = $('#btn-save-url');
const $chatOffline = $('#chat-offline'); const $chatOffline = $('#chat-offline');
const $chatArea = $('#chat-area'); const $chatArea = $('#chat-area');
const $chatFeed = $('#chat-feed'); const $chatFeed = $('#chat-feed');
const $chatStreaming = $('#chat-streaming'); const $btnInject = $('#btn-inject-session');
const $chatInput = $('#chat-input'); const $injectStatus = $('#inject-status');
const $chatSend = $('#chat-send'); const $toggleAutoInject = $('#toggle-auto-inject');
const $chatStop = $('#chat-stop');
let serverOnline = false; let serverOnline = false;
let messages = []; let lastMessageCount = -1;
let loading = false; let pollInterval = null;
let abortController = null;
let currentStreamingEl = null;
function dot(color) { function dot(color) {
return `<span class="dot dot-${color}"></span>`; return `<span class="dot dot-${color}"></span>`;
@@ -54,6 +51,10 @@ function renderSessions(sessions) {
`; `;
} }
function escapeHtml(text) {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function formatText(text) { function formatText(text) {
let html = text let html = text
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -95,10 +96,6 @@ function renderContent(text) {
return parts; return parts;
} }
function escapeHtml(text) {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function createMessageEl(msg) { function createMessageEl(msg) {
const el = document.createElement('div'); const el = document.createElement('div');
el.className = `chat-msg ${msg.role}`; el.className = `chat-msg ${msg.role}`;
@@ -188,14 +185,6 @@ function createMessageEl(msg) {
return el; return el;
} }
function renderMessages() {
$chatFeed.innerHTML = '';
messages.forEach((msg) => {
$chatFeed.appendChild(createMessageEl(msg));
});
scrollToBottom();
}
function scrollToBottom() { function scrollToBottom() {
requestAnimationFrame(() => { requestAnimationFrame(() => {
$chatFeed.scrollTop = $chatFeed.scrollHeight; $chatFeed.scrollTop = $chatFeed.scrollHeight;
@@ -205,6 +194,9 @@ function scrollToBottom() {
function switchTab(tabName) { function switchTab(tabName) {
$$('.tab').forEach((t) => t.classList.toggle('active', t.dataset.tab === tabName)); $$('.tab').forEach((t) => t.classList.toggle('active', t.dataset.tab === tabName));
$$('.tab-content').forEach((s) => s.classList.toggle('active', s.id === `tab-${tabName}`)); $$('.tab-content').forEach((s) => s.classList.toggle('active', s.id === `tab-${tabName}`));
if (tabName === 'chat' && serverOnline) {
pollStudio();
}
} }
function updateChatVisibility() { function updateChatVisibility() {
@@ -217,187 +209,82 @@ function updateChatVisibility() {
} }
} }
async function loadChatHistory() { async function pollStudio() {
try { try {
const data = await getChatHistory(); const data = await getChatHistory();
if (data.messages && data.messages.length > 0) { const msgs = data.messages || [];
messages = data.messages; if (msgs.length !== lastMessageCount) {
} else { lastMessageCount = msgs.length;
messages = [{ id: 'welcome', role: 'system', content: 'Ready. Type a message to start.' }]; $chatFeed.innerHTML = '';
msgs.forEach((msg) => {
$chatFeed.appendChild(createMessageEl(msg));
});
scrollToBottom();
} }
renderMessages(); } catch {}
} catch {
messages = [{ id: 'welcome', role: 'system', content: 'Ready. Type a message to start.' }];
renderMessages();
}
} }
async function handleSend() { function injectSessionScript() {
const text = $chatInput.value.trim(); $injectStatus.style.display = 'block';
if (!text || loading) return; $injectStatus.className = 'inject-status';
$injectStatus.textContent = 'Injection en cours…';
if (text === '/clear') { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
try { await clearChat(); } catch {} if (!tabs || !tabs[0]) {
messages = [{ id: 'clear-' + Date.now(), role: 'system', content: 'Conversation cleared.' }]; $injectStatus.textContent = 'Erreur: aucun onglet actif';
renderMessages(); $injectStatus.classList.add('inject-error');
$chatInput.value = ''; return;
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 });
} }
}; const tabId = tabs[0].id;
currentStreamingEl = document.createElement('div'); chrome.scripting.executeScript({
currentStreamingEl.className = 'chat-msg assistant streaming'; target: { tabId },
$chatFeed.appendChild(currentStreamingEl); func: () => {
scrollToBottom(); if (window.__muyueExtension) {
return 'already_injected';
try { }
const finalContent = await sendChat(text, true, (partial, event) => { window.__muyueExtension = true;
if (event && (event.thinking !== undefined || event.thinking_start || event.thinking_end)) { return 'fresh_inject';
if (event.thinking !== undefined) thinking += event.thinking; },
}, (results) => {
if (chrome.runtime.lastError) {
$injectStatus.textContent = 'Erreur: ' + chrome.runtime.lastError.message;
$injectStatus.classList.add('inject-error');
return; return;
} }
if (event && event.tool_call) {
updateLastText(partial.slice(textStartIdx)); const status = results?.[0]?.result;
textStartIdx = partial.length;
segments.push({ type: 'tool', call: event.tool_call, result: null }); if (status === 'already_injected') {
} else if (event && event.tool_result) { $injectStatus.textContent = '✓ Script déjà injecté dans cette page';
const segIdx = segments.findIndex((s) => s.type === 'tool' && s.call && s.call.tool_call_id === event.tool_result.tool_call_id); $injectStatus.classList.add('inject-success');
if (segIdx >= 0) segments[segIdx].result = event.tool_result;
} else { } else {
updateLastText(partial.slice(textStartIdx)); chrome.tabs.reload(tabId, {}, () => {
} $injectStatus.textContent = '✓ Page rechargée avec le script de session';
streamText = partial; $injectStatus.classList.add('inject-success');
});
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 += `<div class="chat-thinking"><span class="chat-thinking-icon">⏱</span> Thinking…</div>`;
}
segments.forEach((seg) => {
if (seg.type === 'text' && seg.content) {
const c = seg.content.replace(/<think[^>]*>[\s\S]*?<\/think>/gi, '');
if (c) html += `<div class="chat-content">${formatText(c)}</div>`;
}
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 += `<div class="chat-tool ${done ? 'done' : 'running'} ${isErr ? 'error' : ''}"><div class="chat-tool-header"><span class="chat-tool-icon">${icon}</span><span>${name}</span>${done ? `<span class="chat-tool-status ${isErr ? 'err' : 'ok'}">${isErr ? '✗' : '✓'}</span>` : '<span class="chat-dots"><span></span><span></span><span></span></span>'}</div>${preview ? `<div class="chat-tool-args">${escapeHtml(preview)}</div>` : ''}</div>`;
}
});
if (!html) {
html = '<span class="chat-dots"><span></span><span></span><span></span></span>';
} }
currentStreamingEl.innerHTML = ` setTimeout(() => {
<div class="chat-avatar ai">◆</div> $injectStatus.style.display = 'none';
<div class="chat-body"> }, 3000);
<div class="chat-header"><span class="chat-badge" style="color:#FF9100;border-color:#FF9100">GEN</span></div> });
${html} });
<span class="chat-cursor"></span> }
</div>
`;
scrollToBottom();
}, controller.signal);
if (currentStreamingEl && currentStreamingEl.parentNode) { async function loadAutoInjectSetting() {
currentStreamingEl.remove(); const result = await chrome.storage.local.get('muyue_auto_inject');
} $toggleAutoInject.checked = !!result.muyue_auto_inject;
}
const allText = segments.filter((s) => s.type === 'text').map((s) => s.content).join(''); async function saveAutoInjectSetting(enabled) {
const toolSegs = segments.filter((s) => s.type === 'tool'); await chrome.storage.local.set({ muyue_auto_inject: enabled });
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').forEach((tab) => {
tab.addEventListener('click', () => switchTab(tab.dataset.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) => { $chatFeed.addEventListener('click', (e) => {
const btn = e.target.closest('.chat-copy-btn'); const btn = e.target.closest('.chat-copy-btn');
if (btn) { if (btn) {
@@ -408,6 +295,12 @@ $chatFeed.addEventListener('click', (e) => {
} }
}); });
$btnInject.addEventListener('click', injectSessionScript);
$toggleAutoInject.addEventListener('change', () => {
saveAutoInjectSetting($toggleAutoInject.checked);
});
$btnSaveUrl.addEventListener('click', async () => { $btnSaveUrl.addEventListener('click', async () => {
const url = $serverUrl.value.trim().replace(/\/$/, ''); const url = $serverUrl.value.trim().replace(/\/$/, '');
if (url) { if (url) {
@@ -442,6 +335,11 @@ async function refresh() {
}); });
} }
loadAutoInjectSetting();
refresh(); refresh();
loadChatHistory();
setInterval(refresh, 10000); if (pollInterval) clearInterval(pollInterval);
pollInterval = setInterval(() => {
refresh();
if (serverOnline) pollStudio();
}, 3000);

View File

@@ -704,3 +704,138 @@ header h1 { font-size: 16px; font-weight: 600; letter-spacing: -0.3px; color: va
text-align: center; text-align: center;
margin-top: 6px; margin-top: 6px;
} }
/* ── Inject Session ── */
#btn-inject-session {
background: var(--bg-card);
border-color: var(--accent-dim);
color: var(--text-primary);
}
#btn-inject-session:hover {
background: var(--accent-bg);
border-color: var(--accent);
}
#btn-inject-session svg {
flex-shrink: 0;
}
.inject-status {
font-size: 11px;
text-align: center;
padding: 6px 8px;
border-radius: var(--radius-sm);
background: var(--bg-card);
color: var(--text-tertiary);
animation: fadeIn 0.2s ease-out;
}
.inject-status.inject-success {
color: var(--success);
background: rgba(0, 230, 118, 0.08);
}
.inject-status.inject-error {
color: var(--error);
background: rgba(255, 23, 68, 0.08);
}
/* ── Toggle Switch ── */
.toggle-section {
margin-top: 12px;
padding: 12px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
}
.toggle-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.toggle-label {
font-size: 12px;
font-weight: 600;
color: var(--text-primary);
}
.toggle-desc {
font-size: 11px;
color: var(--text-tertiary);
margin-top: 6px;
line-height: 1.4;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 40px;
height: 22px;
flex-shrink: 0;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 22px;
transition: all 0.2s;
}
.toggle-slider::before {
content: '';
position: absolute;
height: 16px;
width: 16px;
left: 2px;
bottom: 2px;
background: var(--text-tertiary);
border-radius: 50%;
transition: all 0.2s;
}
.toggle-switch input:checked + .toggle-slider {
background: var(--accent-bg);
border-color: var(--accent);
}
.toggle-switch input:checked + .toggle-slider::before {
background: var(--accent);
transform: translateX(18px);
}
/* ── Live Chat Header ── */
.chat-live-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
font-size: 11px;
font-weight: 700;
color: var(--text-tertiary);
text-transform: uppercase;
letter-spacing: 0.5px;
background: var(--bg-surface);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.chat-live-header svg {
animation: pulse-live 2s ease-in-out infinite;
}
@keyframes pulse-live {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}

View File

@@ -14,7 +14,7 @@ export default defineConfig({
'notifications', 'notifications',
'alarms', 'alarms',
], ],
host_permissions: ['http://127.0.0.1:*/*', 'http://localhost:*/*'], host_permissions: ['<all_urls>'],
action: { action: {
default_icon: { default_icon: {
16: 'icon/16.png', 16: 'icon/16.png',

View File

@@ -7,7 +7,7 @@ import (
const ( const (
Name = "muyue" Name = "muyue"
Version = "0.9.5" Version = "0.9.6"
Author = "La Légion de Muyue" Author = "La Légion de Muyue"
) )