mirror of
https://github.com/MervinPraison/PraisonAIUI.git
synced 2026-11-07 05:56:17 -10:00
Merge pull request #205 from MervinPraison/claude/issue-200-20260714-code-studio
feat: Code Studio — Monaco editor with sandbox execute and Send to Agent (STITCH-006)
This commit is contained in:
commit
6d194fce1b
5 files changed
+431
No files matched your search
@@ -3468,6 +3468,14 @@ def create_app(
|
||||
"description": "Agent evaluation & accuracy",
|
||||
"order": 40,
|
||||
},
|
||||
{
|
||||
"id": "code-studio",
|
||||
"title": "Code Studio",
|
||||
"icon": "💻",
|
||||
"group": "Control",
|
||||
"description": "Write, run, and share code in a sandbox",
|
||||
"order": 45,
|
||||
},
|
||||
{
|
||||
"id": "telemetry",
|
||||
"title": "Telemetry",
|
||||
@@ -3516,6 +3524,7 @@ def create_app(
|
||||
"channels": "/api/channels",
|
||||
"mcp": "/api/mcp/servers",
|
||||
"marketplace": "/api/marketplace/plugins",
|
||||
"code-studio": "/api/code/languages",
|
||||
}
|
||||
|
||||
# Read page whitelist from config.yaml if set_pages() was not called
|
||||
|
||||
@@ -177,6 +177,7 @@ const BUILTIN_VIEWS = {
|
||||
api: '/plugins/views/api.js',
|
||||
mcp: '/plugins/views/mcp.js',
|
||||
marketplace: '/plugins/views/marketplace.js',
|
||||
'code-studio': '/plugins/views/code-studio.js',
|
||||
};
|
||||
|
||||
const SLOT_REGISTRY = {};
|
||||
|
||||
@@ -818,6 +818,17 @@ export async function render(container) {
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
|
||||
});
|
||||
|
||||
window.addEventListener('aiui:prefill-composer', (e) => {
|
||||
const text = e.detail && e.detail.text;
|
||||
if (!text) return;
|
||||
const el = document.getElementById('chat-input');
|
||||
if (!el) return;
|
||||
el.value = text;
|
||||
el.style.height = 'auto';
|
||||
el.style.height = Math.min(el.scrollHeight, 150) + 'px';
|
||||
el.focus();
|
||||
});
|
||||
input.addEventListener('input', () => {
|
||||
input.style.height = 'auto';
|
||||
input.style.height = Math.min(input.scrollHeight, 150) + 'px';
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
/**
|
||||
* Code Studio View — VS Code-lite in-dashboard IDE.
|
||||
* Write code, run in sandbox, view output, and send snippets to chat.
|
||||
* API: GET /api/code/languages, POST /api/code/execute
|
||||
*/
|
||||
import { helpBanner } from '/plugins/views/_helpers.js';
|
||||
|
||||
const MONACO_CDN = 'https://cdn.jsdelivr.net/npm/[email protected]/min/vs';
|
||||
const HISTORY_MAX = 10;
|
||||
const SNIPPET_KEY = 'aiui_snippets';
|
||||
const OUTPUT_MAX = 200000;
|
||||
|
||||
const DEFAULT_CODE = {
|
||||
python: "print('hello from Code Studio')\n",
|
||||
javascript: "console.log('hello from Code Studio');\n",
|
||||
bash: "echo 'hello from Code Studio'\n",
|
||||
};
|
||||
|
||||
const MONACO_LANG = { python: 'python', javascript: 'javascript', bash: 'shell' };
|
||||
const FILE_EXT = { python: 'py', javascript: 'js', bash: 'sh' };
|
||||
|
||||
let editor = null;
|
||||
let monacoNs = null;
|
||||
let running = false;
|
||||
let timerId = null;
|
||||
const history = [];
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s == null ? '' : s)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function currentCode() {
|
||||
if (editor && typeof editor.getValue === 'function') return editor.getValue();
|
||||
const ta = document.getElementById('cs-fallback');
|
||||
return ta ? ta.value : '';
|
||||
}
|
||||
|
||||
function setCode(code) {
|
||||
if (editor && typeof editor.setValue === 'function') editor.setValue(code);
|
||||
else { const ta = document.getElementById('cs-fallback'); if (ta) ta.value = code; }
|
||||
}
|
||||
|
||||
function setEditorLanguage(lang) {
|
||||
if (monacoNs && editor) {
|
||||
const model = editor.getModel();
|
||||
if (model) monacoNs.editor.setModelLanguage(model, MONACO_LANG[lang] || 'plaintext');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMonaco(host, lang) {
|
||||
if (window.monaco) monacoNs = window.monaco;
|
||||
if (!monacoNs) {
|
||||
await new Promise((resolve, reject) => {
|
||||
const s = document.createElement('script');
|
||||
s.src = MONACO_CDN + '/loader.js';
|
||||
s.onload = resolve;
|
||||
s.onerror = reject;
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
if (!window.require) throw new Error('Monaco loader unavailable');
|
||||
window.require.config({ paths: { vs: MONACO_CDN } });
|
||||
monacoNs = await new Promise((resolve, reject) => {
|
||||
try { window.require(['vs/editor/editor.main'], () => resolve(window.monaco)); }
|
||||
catch (e) { reject(e); }
|
||||
});
|
||||
}
|
||||
editor = monacoNs.editor.create(host, {
|
||||
value: DEFAULT_CODE[lang] || '',
|
||||
language: MONACO_LANG[lang] || 'plaintext',
|
||||
theme: 'vs-dark',
|
||||
automaticLayout: true,
|
||||
fontSize: 13,
|
||||
fontFamily: "'JetBrains Mono','SF Mono',Monaco,Consolas,monospace",
|
||||
lineNumbers: 'on',
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
});
|
||||
}
|
||||
|
||||
function mountFallback(host, lang) {
|
||||
host.innerHTML = `<textarea id="cs-fallback" spellcheck="false"
|
||||
style="width:100%;height:100%;box-sizing:border-box;resize:none;border:0;outline:none;padding:12px;background:#1e1e1e;color:#d4d4d4;font-family:'JetBrains Mono','SF Mono',Monaco,Consolas,monospace;font-size:13px;line-height:1.5">${escapeHtml(DEFAULT_CODE[lang] || '')}</textarea>`;
|
||||
}
|
||||
|
||||
function pickTab(container, tab) {
|
||||
container.querySelectorAll('.cs-tab').forEach((b) => {
|
||||
const on = b.dataset.tab === tab;
|
||||
b.style.color = on ? 'var(--db-text,#e4e4e7)' : 'var(--db-text-dim,#a1a1aa)';
|
||||
b.style.borderBottom = on ? '2px solid var(--db-accent,#6366f1)' : '2px solid transparent';
|
||||
});
|
||||
container.querySelectorAll('.cs-tab-panel').forEach((p) => {
|
||||
p.style.display = p.dataset.panel === tab ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function truncate(text) {
|
||||
const s = String(text == null ? '' : text);
|
||||
if (s.length <= OUTPUT_MAX) return { text: s, truncated: false };
|
||||
return { text: s.slice(0, OUTPUT_MAX), truncated: true };
|
||||
}
|
||||
|
||||
function renderOutput(container, result) {
|
||||
const out = result.output != null ? result.output : (result.stdout || '');
|
||||
const err = result.error != null ? result.error : (result.stderr || '');
|
||||
const artifacts = Array.isArray(result.artifacts) ? result.artifacts : [];
|
||||
const failed = ['error', 'failed', 'degraded'].includes(result.status);
|
||||
|
||||
const banner = container.querySelector('#cs-banner');
|
||||
if (failed) {
|
||||
banner.style.display = 'flex';
|
||||
const code = result.exit_code != null ? ` (exit ${result.exit_code})` : '';
|
||||
banner.querySelector('#cs-banner-text').textContent = `Execution ${result.status}${code}`;
|
||||
} else {
|
||||
banner.style.display = 'none';
|
||||
}
|
||||
|
||||
const o = truncate(out);
|
||||
const e = truncate(err);
|
||||
container.querySelector('#cs-out-body').innerHTML =
|
||||
`<pre style="margin:0;white-space:pre-wrap;word-break:break-word;color:#22c55e">${escapeHtml(o.text) || '<span style="color:#71717a">(no output)</span>'}</pre>` +
|
||||
(o.truncated ? '<div style="color:#71717a;font-size:11px;margin-top:6px">Output truncated</div>' : '');
|
||||
container.querySelector('#cs-err-body').innerHTML =
|
||||
`<pre style="margin:0;white-space:pre-wrap;word-break:break-word;color:#ef4444">${escapeHtml(e.text) || '<span style="color:#71717a">(no errors)</span>'}</pre>` +
|
||||
(e.truncated ? '<div style="color:#71717a;font-size:11px;margin-top:6px">Output truncated</div>' : '');
|
||||
container.querySelector('#cs-art-body').innerHTML = artifacts.length
|
||||
? `<pre style="margin:0;white-space:pre-wrap">${escapeHtml(JSON.stringify(artifacts, null, 2))}</pre>`
|
||||
: '<span style="color:#71717a">(no artifacts)</span>';
|
||||
|
||||
pickTab(container, failed && err ? 'errors' : 'output');
|
||||
}
|
||||
|
||||
function renderHistory(container) {
|
||||
const list = container.querySelector('#cs-history');
|
||||
if (!history.length) {
|
||||
list.innerHTML = '<div style="color:#71717a;font-size:12px;padding:8px">No runs yet</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = history.map((h, i) => {
|
||||
const ok = !['error', 'failed', 'degraded'].includes(h.result.status);
|
||||
const icon = ok ? '<span style="color:#22c55e">●</span>' : '<span style="color:#ef4444">●</span>';
|
||||
const dur = h.duration != null ? `${(h.duration / 1000).toFixed(1)}s` : '';
|
||||
return `<div class="cs-history-row" data-idx="${i}" style="display:flex;align-items:center;gap:8px;padding:8px;border-radius:6px;cursor:pointer;font-size:12px" onmouseover="this.style.background='rgba(255,255,255,.05)'" onmouseout="this.style.background='transparent'">
|
||||
${icon}<span style="flex:1">${escapeHtml(h.language)}</span><span style="color:#71717a">${dur}</span></div>`;
|
||||
}).join('');
|
||||
list.querySelectorAll('.cs-history-row').forEach((row) => {
|
||||
row.addEventListener('click', () => {
|
||||
const h = history[Number(row.dataset.idx)];
|
||||
if (!h) return;
|
||||
const sel = container.querySelector('#cs-language');
|
||||
if (sel) { sel.value = h.language; setEditorLanguage(h.language); }
|
||||
setCode(h.code);
|
||||
renderOutput(container, h.result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function pushHistory(container, entry) {
|
||||
history.unshift(entry);
|
||||
if (history.length > HISTORY_MAX) history.length = HISTORY_MAX;
|
||||
renderHistory(container);
|
||||
}
|
||||
|
||||
async function runCode(container) {
|
||||
if (running) return;
|
||||
const code = currentCode();
|
||||
if (!code.trim()) {
|
||||
alert('Enter code to run');
|
||||
return;
|
||||
}
|
||||
const lang = container.querySelector('#cs-language').value;
|
||||
const runBtn = container.querySelector('#cs-run');
|
||||
running = true;
|
||||
runBtn.disabled = true;
|
||||
runBtn.textContent = 'Running…';
|
||||
const start = Date.now();
|
||||
timerId = setInterval(() => {
|
||||
runBtn.textContent = `Executing… ${((Date.now() - start) / 1000).toFixed(1)}s`;
|
||||
}, 100);
|
||||
|
||||
let result;
|
||||
try {
|
||||
const res = await fetch('/api/code/execute', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ language: lang, code, timeout: 30 }),
|
||||
});
|
||||
result = await res.json();
|
||||
} catch (e) {
|
||||
result = { status: 'failed', error: String(e), language: lang };
|
||||
} finally {
|
||||
running = false;
|
||||
clearInterval(timerId);
|
||||
timerId = null;
|
||||
runBtn.disabled = false;
|
||||
runBtn.textContent = '▶ Run';
|
||||
}
|
||||
|
||||
const duration = result.duration_ms != null ? result.duration_ms : (Date.now() - start);
|
||||
renderOutput(container, result);
|
||||
pushHistory(container, { language: lang, code, result, duration });
|
||||
}
|
||||
|
||||
function saveSnippet(container) {
|
||||
const code = currentCode();
|
||||
const lang = container.querySelector('#cs-language').value;
|
||||
try {
|
||||
const raw = localStorage.getItem(SNIPPET_KEY);
|
||||
const snippets = raw ? JSON.parse(raw) : [];
|
||||
snippets.push({ language: lang, code, ts: Date.now() });
|
||||
localStorage.setItem(SNIPPET_KEY, JSON.stringify(snippets));
|
||||
const btn = container.querySelector('#cs-save');
|
||||
btn.textContent = 'Saved!';
|
||||
setTimeout(() => { btn.textContent = 'Save'; }, 1500);
|
||||
} catch (e) {
|
||||
alert('Could not save snippet (storage full)');
|
||||
}
|
||||
}
|
||||
|
||||
function sendToAgent(container) {
|
||||
const code = currentCode();
|
||||
const lang = container.querySelector('#cs-language').value;
|
||||
const text = '```' + lang + '\n' + code + '\n```\nPlease review this code.';
|
||||
if (window.aiui && typeof window.aiui.selectPage === 'function') {
|
||||
Promise.resolve(window.aiui.selectPage('chat')).then(() => {
|
||||
window.dispatchEvent(new CustomEvent('aiui:prefill-composer', { detail: { text } }));
|
||||
});
|
||||
} else {
|
||||
window.dispatchEvent(new CustomEvent('aiui:prefill-composer', { detail: { text } }));
|
||||
}
|
||||
}
|
||||
|
||||
export async function render(container) {
|
||||
container.setAttribute('data-page', 'code-studio');
|
||||
|
||||
let languages = [];
|
||||
try {
|
||||
const r = await fetch('/api/code/languages');
|
||||
const d = await r.json();
|
||||
languages = d.languages || [];
|
||||
} catch (e) {}
|
||||
if (!languages.length) {
|
||||
languages = [{ id: 'python', name: 'Python' }, { id: 'javascript', name: 'JavaScript' }, { id: 'bash', name: 'Bash' }];
|
||||
}
|
||||
const firstLang = languages[0].id;
|
||||
|
||||
const langOptions = languages
|
||||
.map((l) => `<option value="${escapeHtml(l.id)}">${escapeHtml(l.name || l.id)}</option>`)
|
||||
.join('');
|
||||
|
||||
container.innerHTML = helpBanner({
|
||||
title: 'Code Studio',
|
||||
what: 'Write code, run it in a sandbox, inspect output, and hand snippets to your agent.',
|
||||
howToUse: 'Pick a language, edit the code, then click Run. Use "Send to Agent" to continue in chat.',
|
||||
tip: 'Code runs server-side in a sandbox — never in your browser.',
|
||||
collapsed: true,
|
||||
}) + `
|
||||
<div class="cs-root" style="display:flex;gap:12px;height:calc(100vh - 220px);min-height:480px">
|
||||
<div style="flex:1;display:flex;flex-direction:column;border:1px solid var(--db-border);border-radius:10px;overflow:hidden;background:#1e1e1e">
|
||||
<div style="display:flex;align-items:center;gap:8px;padding:8px;background:#18181b;border-bottom:1px solid var(--db-border);flex-wrap:wrap">
|
||||
<select id="cs-language" style="padding:6px 10px;background:var(--db-card-bg);color:var(--db-text);border:1px solid var(--db-border);border-radius:6px;font-size:13px">${langOptions}</select>
|
||||
<input id="cs-filename" type="text" value="script.${FILE_EXT[firstLang] || 'txt'}" style="padding:6px 10px;background:var(--db-card-bg);color:var(--db-text);border:1px solid var(--db-border);border-radius:6px;font-size:13px;width:140px" />
|
||||
<button id="cs-run" style="padding:6px 16px;background:#22c55e;color:#fff;border:0;border-radius:6px;cursor:pointer;font-size:13px;font-weight:600">▶ Run</button>
|
||||
<button id="cs-save" style="padding:6px 14px;background:transparent;color:var(--db-text);border:1px solid var(--db-border);border-radius:6px;cursor:pointer;font-size:13px">Save</button>
|
||||
</div>
|
||||
<div id="cs-editor" style="flex:3;min-height:180px"></div>
|
||||
<div style="flex:2;display:flex;flex-direction:column;border-top:1px solid var(--db-border);background:#0d1117;min-height:120px">
|
||||
<div id="cs-banner" style="display:none;align-items:center;gap:10px;padding:8px 12px;background:rgba(239,68,68,.12);border-bottom:1px solid rgba(239,68,68,.3);font-size:12px">
|
||||
<span id="cs-banner-text" style="color:#ef4444;flex:1"></span>
|
||||
<button id="cs-fix" style="padding:4px 12px;background:transparent;color:#ef4444;border:1px solid rgba(239,68,68,.4);border-radius:6px;cursor:pointer;font-size:12px">Ask agent to fix</button>
|
||||
</div>
|
||||
<div style="display:flex;gap:4px;padding:0 8px;border-bottom:1px solid var(--db-border)">
|
||||
<button class="cs-tab" data-tab="output" style="padding:8px 12px;background:transparent;border:0;border-bottom:2px solid transparent;color:var(--db-text-dim,#a1a1aa);cursor:pointer;font-size:12px">Output</button>
|
||||
<button class="cs-tab" data-tab="errors" style="padding:8px 12px;background:transparent;border:0;border-bottom:2px solid transparent;color:var(--db-text-dim,#a1a1aa);cursor:pointer;font-size:12px">Errors</button>
|
||||
<button class="cs-tab" data-tab="artifacts" style="padding:8px 12px;background:transparent;border:0;border-bottom:2px solid transparent;color:var(--db-text-dim,#a1a1aa);cursor:pointer;font-size:12px">Artifacts</button>
|
||||
</div>
|
||||
<div style="flex:1;overflow:auto;padding:12px;font-family:'JetBrains Mono','SF Mono',Monaco,Consolas,monospace;font-size:12px">
|
||||
<div class="cs-tab-panel" data-panel="output" id="cs-out-body"><span style="color:#71717a">Run code to see output</span></div>
|
||||
<div class="cs-tab-panel" data-panel="errors" id="cs-err-body" style="display:none"></div>
|
||||
<div class="cs-tab-panel" data-panel="artifacts" id="cs-art-body" style="display:none"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width:240px;display:flex;flex-direction:column;gap:12px">
|
||||
<div style="border:1px solid var(--db-border);border-radius:10px;padding:12px;background:var(--db-card-bg)">
|
||||
<div style="font-size:12px;font-weight:600;margin-bottom:8px">History</div>
|
||||
<div id="cs-history"></div>
|
||||
</div>
|
||||
<div style="border:1px solid var(--db-border);border-radius:10px;padding:12px;background:var(--db-card-bg);font-size:12px;color:var(--db-text-dim)">
|
||||
<div style="font-weight:600;color:var(--db-text);margin-bottom:6px">Environment</div>
|
||||
<div>Timeout: 30s</div>
|
||||
<div>Sandbox: server-side</div>
|
||||
</div>
|
||||
<button id="cs-send-agent" style="padding:10px;background:var(--db-accent,#6366f1);color:#fff;border:0;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600">→ Send to Agent</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const editorHost = container.querySelector('#cs-editor');
|
||||
try {
|
||||
await loadMonaco(editorHost, firstLang);
|
||||
} catch (e) {
|
||||
console.warn('[code-studio] Monaco load failed, using fallback textarea:', e);
|
||||
mountFallback(editorHost, firstLang);
|
||||
}
|
||||
|
||||
renderHistory(container);
|
||||
pickTab(container, 'output');
|
||||
|
||||
const langSel = container.querySelector('#cs-language');
|
||||
langSel.addEventListener('change', () => {
|
||||
const lang = langSel.value;
|
||||
setEditorLanguage(lang);
|
||||
const fn = container.querySelector('#cs-filename');
|
||||
if (fn) fn.value = `script.${FILE_EXT[lang] || 'txt'}`;
|
||||
});
|
||||
|
||||
container.querySelector('#cs-run').addEventListener('click', () => runCode(container));
|
||||
container.querySelector('#cs-save').addEventListener('click', () => saveSnippet(container));
|
||||
container.querySelector('#cs-send-agent').addEventListener('click', () => sendToAgent(container));
|
||||
container.querySelector('#cs-fix').addEventListener('click', () => sendToAgent(container));
|
||||
|
||||
container.querySelectorAll('.cs-tab').forEach((b) => {
|
||||
b.addEventListener('click', () => pickTab(container, b.dataset.tab));
|
||||
});
|
||||
}
|
||||
|
||||
export function cleanup() {
|
||||
if (timerId) { clearInterval(timerId); timerId = null; }
|
||||
if (editor && typeof editor.dispose === 'function') { try { editor.dispose(); } catch (e) {} }
|
||||
editor = null;
|
||||
running = false;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Tests for the Code Studio view (STITCH-006)."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from praisonaiui.server import create_app
|
||||
|
||||
VIEWS = Path("src/praisonaiui/templates/frontend/plugins/views")
|
||||
PLUGINS = Path("src/praisonaiui/templates/frontend/plugins")
|
||||
|
||||
|
||||
class TestCodeStudioPage:
|
||||
def test_page_registered(self):
|
||||
client = TestClient(create_app())
|
||||
r = client.get("/api/pages")
|
||||
assert r.status_code == 200
|
||||
ids = {p["id"] for p in r.json()["pages"]}
|
||||
assert "code-studio" in ids
|
||||
|
||||
def test_page_metadata(self):
|
||||
client = TestClient(create_app())
|
||||
pages = {p["id"]: p for p in client.get("/api/pages").json()["pages"]}
|
||||
page = pages["code-studio"]
|
||||
assert page["title"] == "Code Studio"
|
||||
assert page["api_endpoint"] == "/api/code/languages"
|
||||
|
||||
|
||||
class TestCodeExecutionApi:
|
||||
def test_languages(self):
|
||||
client = TestClient(create_app())
|
||||
r = client.get("/api/code/languages")
|
||||
assert r.status_code == 200
|
||||
d = r.json()
|
||||
assert "languages" in d and "count" in d
|
||||
|
||||
def test_execute(self):
|
||||
client = TestClient(create_app())
|
||||
r = client.post(
|
||||
"/api/code/execute",
|
||||
json={"code": "print('hi')", "language": "python", "timeout": 30},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
d = r.json()
|
||||
assert "status" in d and "language" in d
|
||||
|
||||
|
||||
class TestCodeStudioFrontend:
|
||||
def test_view_file_exists(self):
|
||||
assert (VIEWS / "code-studio.js").is_file()
|
||||
|
||||
def test_view_exports_render(self):
|
||||
src = (VIEWS / "code-studio.js").read_text()
|
||||
assert "export async function render" in src
|
||||
assert "export function cleanup" in src
|
||||
|
||||
def test_view_calls_execute_api(self):
|
||||
src = (VIEWS / "code-studio.js").read_text()
|
||||
assert "/api/code/execute" in src
|
||||
assert "/api/code/languages" in src
|
||||
|
||||
def test_no_browser_eval(self):
|
||||
src = (VIEWS / "code-studio.js").read_text()
|
||||
assert "eval(" not in src
|
||||
|
||||
def test_registered_in_dashboard(self):
|
||||
src = (PLUGINS / "dashboard.js").read_text()
|
||||
assert "'code-studio'" in src
|
||||
assert "code-studio.js" in src
|
||||
|
||||
def test_send_to_agent_event(self):
|
||||
src = (VIEWS / "code-studio.js").read_text()
|
||||
assert "aiui:prefill-composer" in src
|
||||
|
||||
def test_chat_has_prefill_listener(self):
|
||||
src = (VIEWS / "chat.js").read_text()
|
||||
assert "aiui:prefill-composer" in src
|
||||
Reference in new issue
Block a user