v2.6.0
This commit is contained in:
+348
-7
@@ -14,6 +14,10 @@
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/htmlmixed/htmlmixed.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/python/python.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/markdown/markdown.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.js"></script>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=IBM+Plex+Mono:wght@400;500;600&display=swap');
|
||||
|
||||
@@ -2608,6 +2612,10 @@
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
|
||||
저장
|
||||
</button>
|
||||
<button id="canvas-run-btn" onclick="canvasRun()" title="코드 실행 (터미널)" style="display:flex;align-items:center;gap:5px;border:1px solid #333;background:#1a1a1a;color:#28c840;border-radius:8px;padding:4px 10px;font-size:11px;font-weight:700;cursor:pointer;font-family:'IBM Plex Mono',monospace">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="none"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||||
▶ 실행
|
||||
</button>
|
||||
<button onclick="canvasAddToContext()" title="채팅 입력에 추가" style="display:flex;align-items:center;gap:5px;border:1px solid var(--line);background:#fff;color:var(--text);border-radius:8px;padding:4px 10px;font-size:11px;font-weight:700;cursor:pointer;font-family:'Manrope',sans-serif">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||
Add to Chat
|
||||
@@ -2619,13 +2627,28 @@
|
||||
</div>
|
||||
<!-- Canvas body -->
|
||||
<div style="flex:1;min-height:0;display:flex;flex-direction:column;position:relative">
|
||||
<div id="canvas-empty-state" style="position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;color:var(--muted);text-align:center;padding:24px;pointer-events:none">
|
||||
<div id="canvas-empty-state" style="position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;color:var(--muted);text-align:center;padding:24px;pointer-events:none;z-index:1">
|
||||
<svg width="42" height="42" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.25"><rect x="2" y="3" width="20" height="18" rx="2"/><path d="M8 9l-4 3 4 3"/><path d="M16 9l4 3-4 3"/><path d="M12 6v12"/></svg>
|
||||
<div style="font-size:13px;font-weight:700">열린 파일 없음</div>
|
||||
<div style="font-size:11px;line-height:1.7">Drop a file here or click <strong>+</strong> to create one</div>
|
||||
</div>
|
||||
<div id="canvas-editor-wrap" style="display:none;flex:1;min-height:0;overflow:hidden"></div>
|
||||
<iframe id="canvas-preview-frame" style="display:none;flex:1;border:none;width:100%;min-height:0"></iframe>
|
||||
<!-- Terminal panel (VSCode-style, bottom) -->
|
||||
<div id="canvas-terminal" style="display:flex;flex-direction:column;border-top:1px solid #333;background:rgba(12,12,12,0.9);height:50%;min-height:32px;flex-shrink:0;margin-top:auto">
|
||||
<!-- Drag handle -->
|
||||
<div id="canvas-term-drag" style="height:3px;cursor:ns-resize;background:#333;flex-shrink:0;transition:background .15s"></div>
|
||||
<!-- Shell tab bar -->
|
||||
<div style="display:flex;align-items:center;height:30px;background:#0C0C0C;font-size:13px;flex-shrink:0;overflow-x:auto">
|
||||
<div id="canvas-shell-tabs" style="display:flex;align-items:stretch;height:100%"></div>
|
||||
<button onclick="addTermShell()" title="새 쉘" style="background:0;border:0;color:#767676;cursor:pointer;font-size:16px;line-height:1;padding:0 8px;flex-shrink:0">+</button>
|
||||
<div style="flex:1"></div>
|
||||
<span id="canvas-run-status" style="font-size:10px;color:#767676;margin-right:4px"></span>
|
||||
<button onclick="document.getElementById('canvas-terminal').style.display='none'" style="background:0;border:0;color:#767676;cursor:pointer;font-size:14px;line-height:1;padding:2px 4px;flex-shrink:0" title="패널 닫기">×</button>
|
||||
</div>
|
||||
<!-- xterm.js terminal container -->
|
||||
<div id="canvas-term-container" style="flex:1;min-height:0;padding:0 4px;background:#0C0C0C;position:relative"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Canvas Overlay -->
|
||||
@@ -2751,7 +2774,7 @@
|
||||
</div>
|
||||
|
||||
<!-- AGENT EXECUTION PANEL -->
|
||||
<div style="display:flex;flex-direction:column;max-height:200px;overflow:hidden;border:1px solid var(--border);border-radius:10px;background:var(--panel-2);margin-bottom:10px">
|
||||
<div id="agent-panel" style="display:flex;flex-direction:column;max-height:200px;overflow:hidden;border:1px solid var(--border);border-radius:10px;background:var(--panel-2);margin-bottom:10px">
|
||||
<div style="padding:8px 12px;border-bottom:1px solid var(--border);flex-shrink:0">
|
||||
<div style="font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:var(--muted);margin-bottom:8px">활성 에이전트</div>
|
||||
</div>
|
||||
@@ -2761,7 +2784,7 @@
|
||||
</div>
|
||||
|
||||
<!-- PROCESS LOG -->
|
||||
<div style="display:flex;flex-direction:column;flex:1 1 320px;overflow:hidden;min-height:280px">
|
||||
<div id="process-section" style="display:flex;flex-direction:column;flex:1 1 320px;overflow:hidden;min-height:280px">
|
||||
<div style="padding:8px 16px 6px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border);flex-shrink:0">
|
||||
<a href="javascript:void(0)" onclick="openProcessLogFile()" style="font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:var(--muted);text-decoration:none;cursor:pointer;hover:color:var(--brand)">프로세스 로그</a>
|
||||
<button onclick="clearProcessLog()" style="background:none;border:none;color:var(--muted);cursor:pointer;font-size:10px;font-family:inherit">✕ clear</button>
|
||||
@@ -4628,7 +4651,6 @@ function renderChatMessages() {
|
||||
return;
|
||||
}
|
||||
|
||||
_dlLinkCache = {}; // reset on full re-render so moved/deleted files recheck
|
||||
container.innerHTML = chatHistory.map((msg, idx) => {
|
||||
const pinClass = contextPinMode ? ' pin-selectable' : '';
|
||||
const selectedClass = (contextPinMode && pinnedMessageIndices.has(idx)) ? ' pin-selected' : '';
|
||||
@@ -8469,13 +8491,12 @@ function validateDownloadLinks(container) {
|
||||
var url = btn.getAttribute('href');
|
||||
if (!url || url.indexOf('/api/files/') === -1) return;
|
||||
if (_dlLinkCache[url] === false) { markDownloadLinkError(btn); return; }
|
||||
if (_dlLinkCache[url] === true) return;
|
||||
if (_dlLinkCache[url] === true || _dlLinkCache[url] === 'pending') return;
|
||||
_dlLinkCache[url] = 'pending';
|
||||
fetch(url, { method: 'HEAD', headers: authHeaders() })
|
||||
.then(function(r) {
|
||||
_dlLinkCache[url] = r.ok;
|
||||
if (!r.ok) {
|
||||
// btn may still be in DOM; re-query in case renderMessages rebuilt it
|
||||
var found = (container || document).querySelector('a.download-btn[href="' + url.replace(/"/g, '\\"') + '"]');
|
||||
if (found) markDownloadLinkError(found);
|
||||
}
|
||||
@@ -9209,15 +9230,34 @@ function toggleCanvas() {
|
||||
canvasOpen = !canvasOpen;
|
||||
const panel = document.getElementById('canvas-panel');
|
||||
const btn = document.getElementById('canvas-toggle-btn');
|
||||
const hideEls = ['system-stats-section', 'process-section', 'agent-panel'];
|
||||
if (!panel) return;
|
||||
if (canvasOpen) {
|
||||
panel.style.display = 'flex';
|
||||
if (btn) { btn.style.background='#eaf2ff'; btn.style.borderColor='#bdd3f6'; btn.style.color='#0d4faf'; }
|
||||
if (!canvasEditorInitialized) initCanvasEditor();
|
||||
canvasRenderTabs();
|
||||
hideEls.forEach(id => { const el = document.getElementById(id); if (el) el.style.display = 'none'; });
|
||||
setTimeout(() => {
|
||||
const shell = getActiveShell();
|
||||
if (shell && !shell.term) {
|
||||
const container = document.getElementById('canvas-term-container');
|
||||
if (container) {
|
||||
const result = createXtermShell(container, shell.id);
|
||||
shell.term = result.term;
|
||||
shell.fitAddon = result.fitAddon;
|
||||
shell.ptyReady = result.inst.ptyReady;
|
||||
}
|
||||
}
|
||||
if (shell && shell.term) {
|
||||
shell.fitAddon.fit();
|
||||
shell.term.focus();
|
||||
}
|
||||
}, 150);
|
||||
} else {
|
||||
panel.style.display = 'none';
|
||||
if (btn) { btn.style.background=''; btn.style.borderColor=''; btn.style.color=''; }
|
||||
hideEls.forEach(id => { const el = document.getElementById(id); if (el) el.style.display = ''; });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9313,7 +9353,7 @@ function applyCanvasViewMode(mode, tab) {
|
||||
if (previewModeBtn) previewModeBtn.classList.add('active');
|
||||
canvasUpdatePreview();
|
||||
} else {
|
||||
if (editorWrap) { editorWrap.style.display = 'flex'; editorWrap.style.flex = '1'; }
|
||||
if (editorWrap) { editorWrap.style.display = 'block'; editorWrap.style.flex = '1'; }
|
||||
if (previewFrame) previewFrame.style.display = 'none';
|
||||
if (codeModeBtn) codeModeBtn.classList.add('active');
|
||||
if (previewModeBtn) previewModeBtn.classList.remove('active');
|
||||
@@ -9393,6 +9433,307 @@ function canvasAddToContext() {
|
||||
addProcessEntry('info', `Canvas: sent ${tab.name} to LLM as context.`);
|
||||
}
|
||||
|
||||
// ── xterm.js Terminal ────────────────────────────────────────────────────────
|
||||
let termWs = null;
|
||||
let termWsReconnectTimer = null;
|
||||
let termWsConnected = false;
|
||||
const xtermInstances = new Map(); // shellId → { term, fitAddon, ptyReady }
|
||||
|
||||
function getCookie(name) {
|
||||
const m = document.cookie.match(new RegExp('(?:^|;\\s*)' + name + '=([^;]*)'));
|
||||
return m ? decodeURIComponent(m[1]) : '';
|
||||
}
|
||||
|
||||
function termWsConnect() {
|
||||
if (termWs && (termWs.readyState === WebSocket.OPEN || termWs.readyState === WebSocket.CONNECTING)) return;
|
||||
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
let url = `${proto}//${location.host}/ws`;
|
||||
const token = sessionStorage.getItem('smallclaw_token') || getCookie('smallclaw_session');
|
||||
if (token) url += `?token=${encodeURIComponent(token)}`;
|
||||
try { termWs = new WebSocket(url); } catch { return; }
|
||||
termWs.onopen = () => {
|
||||
termWsConnected = true;
|
||||
// Spawn PTY for each existing shell
|
||||
for (const [shellId, inst] of xtermInstances) {
|
||||
if (!inst.ptyReady) {
|
||||
inst.ptyReady = true;
|
||||
const term = inst.term;
|
||||
termWsSend({ type: 'pty_spawn', shellId, cols: term.cols, rows: term.rows });
|
||||
}
|
||||
}
|
||||
};
|
||||
termWs.onmessage = (ev) => {
|
||||
let msg; try { msg = JSON.parse(ev.data); } catch { return; }
|
||||
if (msg.type === 'pty_output') {
|
||||
const inst = xtermInstances.get(msg.shellId);
|
||||
if (inst) inst.term.write(msg.data);
|
||||
} else if (msg.type === 'pty_exit') {
|
||||
const inst = xtermInstances.get(msg.shellId);
|
||||
if (inst) {
|
||||
inst.ptyReady = false;
|
||||
inst.term.write('\r\n\x1b[90m[Process exited]\x1b[0m\r\n');
|
||||
}
|
||||
} else if (msg.type === 'pty_error') {
|
||||
const inst = xtermInstances.get(msg.shellId);
|
||||
if (inst) inst.term.write('\r\n\x1b[31mPTY error: ' + (msg.error || 'unknown') + '\x1b[0m\r\n');
|
||||
}
|
||||
};
|
||||
termWs.onclose = () => {
|
||||
termWsConnected = false;
|
||||
termWs = null;
|
||||
for (const [id, inst] of xtermInstances) inst.ptyReady = false;
|
||||
termWsReconnectTimer = setTimeout(termWsConnect, 3000);
|
||||
};
|
||||
termWs.onerror = () => { termWsConnected = false; };
|
||||
}
|
||||
|
||||
function termWsSend(msg) {
|
||||
if (termWs && termWs.readyState === WebSocket.OPEN) {
|
||||
termWs.send(JSON.stringify(msg));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function createXtermShell(container, shellId) {
|
||||
const id = shellId || 'sh_' + Math.random().toString(36).slice(2, 8);
|
||||
// Create a sub-container for this shell's xterm instance
|
||||
const sub = document.createElement('div');
|
||||
sub.id = 'xterm-' + id;
|
||||
sub.style.cssText = 'position:absolute;inset:0';
|
||||
container.appendChild(sub);
|
||||
const term = new Terminal({
|
||||
theme: {
|
||||
background: '#0C0C0C',
|
||||
foreground: '#CCCCCC',
|
||||
cursor: '#CCCCCC',
|
||||
cursorAccent: '#0C0C0C',
|
||||
selectionBackground: 'rgba(59,120,255,0.4)',
|
||||
black: '#1e1e1e',
|
||||
red: '#E74856',
|
||||
green: '#16C60C',
|
||||
yellow: '#C19C00',
|
||||
blue: '#3B78FF',
|
||||
magenta: '#B4009E',
|
||||
cyan: '#61D6D6',
|
||||
white: '#CCCCCC',
|
||||
brightBlack: '#767676',
|
||||
brightRed: '#FF6E6E',
|
||||
brightGreen: '#69FF69',
|
||||
brightYellow: '#FFFF55',
|
||||
brightBlue: '#96CBFE',
|
||||
brightMagenta: '#FF76F6',
|
||||
brightCyan: '#8FFFFF',
|
||||
brightWhite: '#FFFFFF',
|
||||
},
|
||||
fontFamily: "'IBM Plex Mono', 'Courier New', monospace",
|
||||
fontSize: 14,
|
||||
lineHeight: 1.4,
|
||||
cursorBlink: true,
|
||||
scrollback: 5000,
|
||||
allowProposedApi: true,
|
||||
});
|
||||
const fitAddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
const webLinksAddon = new WebLinksAddon.WebLinksAddon();
|
||||
term.loadAddon(webLinksAddon);
|
||||
term.open(sub);
|
||||
fitAddon.fit();
|
||||
|
||||
const inst = { term, fitAddon, ptyReady: false };
|
||||
xtermInstances.set(id, inst);
|
||||
|
||||
// Send keystrokes to PTY
|
||||
term.onData((data) => {
|
||||
if (termWsConnected && inst.ptyReady) {
|
||||
termWsSend({ type: 'pty_input', shellId: id, data });
|
||||
}
|
||||
});
|
||||
|
||||
// Resize PTY when terminal resizes
|
||||
term.onResize(({ cols, rows }) => {
|
||||
if (termWsConnected && inst.ptyReady) {
|
||||
termWsSend({ type: 'pty_resize', shellId: id, cols, rows });
|
||||
}
|
||||
});
|
||||
|
||||
// Spawn PTY if WS is already connected
|
||||
if (termWsConnected) {
|
||||
inst.ptyReady = true;
|
||||
termWsSend({ type: 'pty_spawn', shellId: id, cols: term.cols, rows: term.rows });
|
||||
}
|
||||
|
||||
return { id, term, fitAddon, inst };
|
||||
}
|
||||
|
||||
// ── Canvas interactive shell (multi-shell tabs) ─────────────────────────────
|
||||
let termShells = [];
|
||||
let activeTermShellId = null;
|
||||
|
||||
function createShell() {
|
||||
const id = 'sh_' + Math.random().toString(36).slice(2, 8);
|
||||
const shell = { id, name: 'bash', cwd: null, ptyReady: false, term: null, fitAddon: null };
|
||||
termShells.push(shell);
|
||||
return shell;
|
||||
}
|
||||
|
||||
function getActiveShell() {
|
||||
return termShells.find(s => s.id === activeTermShellId);
|
||||
}
|
||||
|
||||
function renderShellTabs() {
|
||||
const bar = document.getElementById('canvas-shell-tabs');
|
||||
if (!bar) return;
|
||||
bar.innerHTML = termShells.map(s => {
|
||||
const active = s.id === activeTermShellId;
|
||||
return `<div onclick="switchTermShell('${s.id}')" style="display:flex;align-items:center;gap:5px;padding:0 10px;cursor:pointer;font-family:'IBM Plex Mono',monospace;font-size:13px;color:${active ? '#CCCCCC' : '#767676'};background:${active ? '#0C0C0C' : 'transparent'};border-top:${active ? '2px solid #3B78FF' : '2px solid transparent'};white-space:nowrap;user-select:none">
|
||||
<svg width="11" height="11" viewBox="0 0 16 16" fill="currentColor" style="opacity:.6;flex-shrink:0"><path d="M14 3v10H2V3h12zM1 2v12h14V2H1zm5 9l3-2.5L6 6v5z"/></svg>
|
||||
<span>${escHtml(s.name)}</span>
|
||||
${termShells.length > 1 ? `<span onclick="closeTermShell('${s.id}',event)" style="margin-left:3px;color:#767676;font-size:13px;line-height:1">×</span>` : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function switchTermShell(id) {
|
||||
// Hide current terminal sub-container
|
||||
const cur = getActiveShell();
|
||||
if (cur) {
|
||||
const curSub = document.getElementById('xterm-' + cur.id);
|
||||
if (curSub) curSub.style.display = 'none';
|
||||
}
|
||||
activeTermShellId = id;
|
||||
const shell = getActiveShell();
|
||||
if (!shell) return;
|
||||
// Show or create xterm for this shell
|
||||
if (shell.term) {
|
||||
const sub = document.getElementById('xterm-' + shell.id);
|
||||
if (sub) sub.style.display = '';
|
||||
shell.term.focus();
|
||||
if (shell.fitAddon) shell.fitAddon.fit();
|
||||
} else {
|
||||
const container = document.getElementById('canvas-term-container');
|
||||
if (container) {
|
||||
const result = createXtermShell(container, shell.id);
|
||||
shell.term = result.term;
|
||||
shell.fitAddon = result.fitAddon;
|
||||
shell.ptyReady = result.inst.ptyReady;
|
||||
}
|
||||
}
|
||||
renderShellTabs();
|
||||
}
|
||||
|
||||
function addTermShell() {
|
||||
const shell = createShell();
|
||||
switchTermShell(shell.id);
|
||||
}
|
||||
|
||||
function closeTermShell(id, ev) {
|
||||
if (ev) { ev.stopPropagation(); }
|
||||
const idx = termShells.findIndex(s => s.id === id);
|
||||
if (idx === -1 || termShells.length <= 1) return;
|
||||
termWsSend({ type: 'pty_kill', shellId: id });
|
||||
const shell = termShells[idx];
|
||||
if (shell.term) shell.term.dispose();
|
||||
xtermInstances.delete(id);
|
||||
// Remove sub-container
|
||||
const sub = document.getElementById('xterm-' + id);
|
||||
if (sub) sub.remove();
|
||||
termShells.splice(idx, 1);
|
||||
if (activeTermShellId === id) {
|
||||
switchTermShell(termShells[Math.min(idx, termShells.length - 1)].id);
|
||||
} else {
|
||||
renderShellTabs();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: HTTP POST for canvasRun and non-PTY scenarios
|
||||
async function termExec(command) {
|
||||
const shell = getActiveShell();
|
||||
if (!shell || !command.trim()) return;
|
||||
if (termWsConnected && shell.ptyReady && shell.term) {
|
||||
termWsSend({ type: 'pty_input', shellId: shell.id, data: command + '\r' });
|
||||
return;
|
||||
}
|
||||
const status = document.getElementById('canvas-run-status');
|
||||
if (status) { status.textContent = '●'; status.style.color = '#C19C00'; }
|
||||
try {
|
||||
const resp = await fetch('/api/canvas/shell', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ command, cwd: shell.cwd }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.cwd) shell.cwd = data.cwd;
|
||||
if (shell.term) {
|
||||
if (data.stdout) shell.term.write(data.stdout);
|
||||
if (data.stderr) shell.term.write('\x1b[31m' + data.stderr + '\x1b[0m');
|
||||
}
|
||||
if (status) { status.textContent = data.exitCode === 0 ? '✓' : '✗'; status.style.color = data.exitCode === 0 ? '#16C60C' : '#E74856'; }
|
||||
} catch (err) {
|
||||
if (shell.term) shell.term.write('\x1b[31mError: ' + String(err) + '\x1b[0m\r\n');
|
||||
if (status) { status.textContent = '✗'; status.style.color = '#E74856'; }
|
||||
}
|
||||
}
|
||||
|
||||
function canvasRun() {
|
||||
const tab = canvasTabs.find(t => t.id === activeCanvasTabId);
|
||||
if (!tab) return;
|
||||
if (canvasEditor && tab.mode !== 'preview') tab.content = canvasEditor.getValue();
|
||||
const ext = (tab.name.split('.').pop() || '').toLowerCase();
|
||||
if (!['py','js','ts','mjs'].includes(ext)) return;
|
||||
const runner = ext === 'py' ? 'python3' : (ext === 'ts' ? 'npx tsx' : 'node');
|
||||
const tmpName = '.canvas-tmp/' + tab.name.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
termExec(`cat > "${tmpName}" << 'CANVAS_EOF'\n${tab.content}\nCANVAS_EOF && ${runner} "${tmpName}" && rm -f "${tmpName}"`);
|
||||
}
|
||||
|
||||
// Init first shell + WS PTY connection
|
||||
(function() {
|
||||
const shell = createShell();
|
||||
activeTermShellId = shell.id;
|
||||
renderShellTabs();
|
||||
fetch('/api/status').then(r => r.json()).then(d => {
|
||||
if (d.workspace) shell.cwd = d.workspace;
|
||||
termWsConnect();
|
||||
}).catch(() => { termWsConnect(); });
|
||||
})();
|
||||
|
||||
// Terminal drag-resize
|
||||
(function() {
|
||||
const drag = document.getElementById('canvas-term-drag');
|
||||
const term = document.getElementById('canvas-terminal');
|
||||
if (!drag || !term) return;
|
||||
let startY, startH;
|
||||
drag.addEventListener('mousedown', e => {
|
||||
e.preventDefault();
|
||||
startY = e.clientY;
|
||||
startH = term.offsetHeight;
|
||||
const onMove = ev => {
|
||||
const dy = startY - ev.clientY;
|
||||
const next = Math.max(32, Math.min(startH + dy, term.parentElement.offsetHeight - 38));
|
||||
term.style.height = next + 'px';
|
||||
const shell = getActiveShell();
|
||||
if (shell && shell.fitAddon) shell.fitAddon.fit();
|
||||
if (canvasEditor) canvasEditor.refresh();
|
||||
};
|
||||
const onUp = () => {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
drag.style.background = '';
|
||||
const shell = getActiveShell();
|
||||
if (shell && shell.fitAddon) shell.fitAddon.fit();
|
||||
};
|
||||
drag.style.background = '#007acc';
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
});
|
||||
})();
|
||||
|
||||
// Resize xterm when window resizes
|
||||
window.addEventListener('resize', () => {
|
||||
const shell = getActiveShell();
|
||||
if (shell && shell.fitAddon) shell.fitAddon.fit();
|
||||
});
|
||||
|
||||
function canvasInitDrop() {
|
||||
const panel = document.getElementById('canvas-panel');
|
||||
if (!panel) return;
|
||||
|
||||
Reference in New Issue
Block a user