Files
archy/packages/app/src/components/renderers/CodeRunner.vue
T
DorianandClaude Opus 4.6 7fa06ce500 feat(renderer): sandboxed code runner for HTML/JS/CSS (M10.11)
- CodeRunner.vue: sandboxed iframe with srcdoc, allow-scripts only
- Console capture via postMessage (log + error)
- Run button, clear output, code preview
- Extracts runnable code blocks from markdown

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:02:36 +00:00

134 lines
4.0 KiB
Vue

<template>
<div class="code-runner rounded-xl bg-white/5 border border-white/10 overflow-hidden">
<!-- Toolbar -->
<div class="flex items-center gap-2 px-3 py-2 border-b border-white/5">
<span class="text-[10px] text-white/30 uppercase tracking-wider">{{ language }}</span>
<div class="flex-1" />
<button
class="text-[10px] px-2.5 py-1 rounded bg-accent/15 text-accent/80 hover:bg-accent/25 transition-colors"
@click="runCode"
>
Run
</button>
<button
v-if="consoleOutput.length > 0"
class="text-[10px] px-2 py-1 rounded bg-white/5 text-white/40 hover:text-white/60 hover:bg-white/10 transition-colors"
@click="clearOutput"
>
Clear
</button>
</div>
<!-- Code display -->
<pre class="px-3 py-2 text-xs text-white/70 overflow-x-auto max-h-48 bg-black/20"><code>{{ code }}</code></pre>
<!-- Output iframe (hidden, used for execution) -->
<iframe
v-if="showIframe"
ref="iframeRef"
:srcdoc="srcdoc"
sandbox="allow-scripts"
class="w-full border-t border-white/5"
:class="isHtml ? 'h-48' : 'h-0 invisible'"
title="Code output"
/>
<!-- Console output -->
<div
v-if="consoleOutput.length > 0"
class="border-t border-white/5 bg-black/30 px-3 py-2 max-h-32 overflow-y-auto"
>
<p class="text-[9px] text-white/20 uppercase tracking-wider mb-1">Console</p>
<div
v-for="(entry, i) in consoleOutput"
:key="i"
class="text-[11px] font-mono leading-relaxed"
:class="entry.type === 'error' ? 'text-red-400/80' : 'text-white/60'"
>
{{ entry.text }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
const props = defineProps<{
code: string
language: string
}>()
interface ConsoleEntry {
type: 'log' | 'error'
text: string
}
const consoleOutput = ref<ConsoleEntry[]>([])
const showIframe = ref(false)
const iframeRef = ref<HTMLIFrameElement | null>(null)
const isHtml = computed(() => props.language === 'html')
const srcdoc = computed(() => {
if (isHtml.value) {
// For HTML, wrap with console capture
return `<!DOCTYPE html>
<html><head><style>body{background:#0a0a0a;color:#e0e0e0;font-family:system-ui;margin:8px}</style>
<script>
const _post = (type, args) => parent.postMessage({type:'code-runner-console',level:type,text:args.map(a=>typeof a==='object'?JSON.stringify(a):String(a)).join(' ')},'*');
console.log = (...a) => _post('log', a);
console.error = (...a) => _post('error', a);
window.onerror = (m) => _post('error', [m]);
<\/script></head><body>${props.code}</body></html>`
}
// For JS/CSS, wrap in HTML
if (props.language === 'javascript' || props.language === 'js') {
return `<!DOCTYPE html><html><head>
<script>
const _post = (type, args) => parent.postMessage({type:'code-runner-console',level:type,text:args.map(a=>typeof a==='object'?JSON.stringify(a):String(a)).join(' ')},'*');
console.log = (...a) => _post('log', a);
console.error = (...a) => _post('error', a);
window.onerror = (m) => _post('error', [m]);
<\/script></head><body><script>${props.code}<\/script></body></html>`
}
if (props.language === 'css') {
return `<!DOCTYPE html><html><head><style>${props.code}</style></head><body style="background:#0a0a0a"><div style="padding:16px;color:#e0e0e0;font-family:system-ui">CSS Preview</div></body></html>`
}
return ''
})
function runCode() {
consoleOutput.value = []
showIframe.value = false
// Force re-mount iframe
requestAnimationFrame(() => {
showIframe.value = true
})
}
function clearOutput() {
consoleOutput.value = []
}
function handleMessage(e: MessageEvent) {
if (e.data?.type === 'code-runner-console') {
consoleOutput.value.push({
type: e.data.level === 'error' ? 'error' : 'log',
text: e.data.text,
})
}
}
onMounted(() => {
window.addEventListener('message', handleMessage)
})
onBeforeUnmount(() => {
window.removeEventListener('message', handleMessage)
})
</script>