aboutsummaryrefslogtreecommitdiff
path: root/modules/core/terminal.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/core/terminal.js')
-rw-r--r--modules/core/terminal.js117
1 files changed, 71 insertions, 46 deletions
diff --git a/modules/core/terminal.js b/modules/core/terminal.js
index 607c51a..f0ef036 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -7,7 +7,7 @@
this.lastRouteTime = 0;
this.lastRouteCmd = '';
this.blockedCount = 0;
- this.destructiveCmds = new Set(['rm','delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot']);
+ this.destructiveCmds = new Set(['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot']);
this.systemCmds = new Set(['reboot','shutdown','format','kill','halt','poweroff']);
this.suspiciousFlags = new Set(['--dump','--execute','--eval','--exec','--shell','--script']);
this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history']);
@@ -374,8 +374,16 @@
}
print(text, type = '') {
const line = document.createElement('div');
- line.className = `output-line ${type}`;
- line.textContent = text;
+ // Use card layout for typed messages (error/warning/info/success/text-muted), plain for bare text
+ const isTyped = ['error','warning','success'].includes(type);
+ if (isTyped) {
+ line.className = 'output-line msg-card system ' + type;
+ const safeText = String(text).replace(/</g, "&lt;").replace(/>/g, "&gt;");
+ line.innerHTML = '<div class="msg-card-header"><span class="msg-role">⚙ System</span><span class="msg-buttons"><button class="card-btn copy-btn" onclick="webx11.copyToClipboard(this)">Copy</button></span></div><div class="msg-card-body">' + safeText + '</div>';
+ } else {
+ line.className = 'output-line';
+ line.textContent = text;
+ }
this.outputContainer.appendChild(line);
this._historyBuffer.push({ l: text, t: type });
if (this._historyBuffer.length > this._historyMaxLines) {
@@ -411,7 +419,7 @@
const before = text.slice(lastIndex, match.index);
if (before.trim()) {
const safeBefore = before.replace(/</g, "&lt;").replace(/>/g, "&gt;");
- fragments.push({ type: 'text', html: `<button class="copy-btn" onclick="webx11.copyToClipboard(this)">Copy</button><div class="ai-content">${safeBefore}</div>` });
+ fragments.push({ type: 'text', html: '<div class="ai-content">' + safeBefore + '</div>' });
}
fragments.push({ type: 'render', html: this._renderHTMLPreview(match[1].trim()) });
lastIndex = match.index + match[0].length;
@@ -419,7 +427,7 @@
const after = text.slice(lastIndex);
if (after.trim()) {
const safeAfter = after.replace(/</g, "&lt;").replace(/>/g, "&gt;");
- fragments.push({ type: 'text', html: `<button class="copy-btn" onclick="webx11.copyToClipboard(this)">Copy</button><div class="ai-content">${safeAfter}</div>` });
+ fragments.push({ type: 'text', html: '<div class="ai-content">' + safeAfter + '</div>' });
}
return fragments.length > 0 ? fragments : null;
}
@@ -427,23 +435,24 @@
this._flushStreamChunks();
const fragments = this._renderHTMLBlock(text);
if (fragments) {
+ const wrapper = document.createElement('div');
+ wrapper.className = 'output-line msg-card ai';
+ wrapper.innerHTML = '<div class="msg-card-header"><span class="msg-role">🤖 AI</span><span class="msg-buttons"><button class="card-btn copy-btn" onclick="webx11.copyToClipboard(this)">Copy</button></span></div><div class="msg-card-body"></div>';
+ const body = wrapper.querySelector('.msg-card-body');
for (const frag of fragments) {
- const wrapper = document.createElement('div');
- wrapper.className = 'output-line' + (frag.type === 'text' ? ' ollama' : '');
- wrapper.innerHTML = frag.html;
- this.outputContainer.appendChild(wrapper);
+ const div = document.createElement('div');
+ div.innerHTML = frag.html;
+ body.appendChild(div);
}
+ this.outputContainer.appendChild(wrapper);
this.scrollToBottom();
this._trimOutput();
return;
}
const wrapper = document.createElement('div');
- wrapper.className = 'output-line ollama';
+ wrapper.className = 'output-line msg-card ai';
const safeText = text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
- wrapper.innerHTML = `
- <button class="copy-btn" onclick="webx11.copyToClipboard(this)">Copy</button>
- <div class="ai-content">${safeText}</div>
- `;
+ wrapper.innerHTML = '<div class="msg-card-header"><span class="msg-role">🤖 AI</span><span class="msg-buttons"><button class="card-btn copy-btn" onclick="webx11.copyToClipboard(this)">Copy</button></span></div><div class="msg-card-body">' + safeText + '</div>';
this.outputContainer.appendChild(wrapper);
this.scrollToBottom();
this._trimOutput();
@@ -453,15 +462,12 @@
if (!append) {
const safeText = text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
const wrapper = document.createElement('div');
- wrapper.className = 'output-line ollama';
- wrapper.innerHTML = `
- <button class="copy-btn" onclick="webx11.copyToClipboard(this)">Copy</button>
- <div class="ai-content">${safeText}</div>
- `;
+ wrapper.className = 'output-line msg-card ai';
+ wrapper.innerHTML = '<div class="msg-card-header"><span class="msg-role">🤖 AI</span><span class="msg-buttons"><button class="card-btn copy-btn" onclick="webx11.copyToClipboard(this)">Copy</button></span></div><div class="msg-card-body">' + safeText + '</div>';
this.outputContainer.appendChild(wrapper);
this._streamChunks = [];
} else {
- const contentDiv = this.outputContainer.querySelector('.output-line.ollama:last-child .ai-content');
+ const contentDiv = this.outputContainer.querySelector('.msg-card.ai:last-child .msg-card-body');
if (contentDiv) {
this._streamChunks.push(text);
if (this._streamChunks.length >= 5) {
@@ -477,7 +483,7 @@
this._trimOutput();
}
_flushStreamChunks() {
- const contentDiv = this.outputContainer.querySelector('.output-line.ollama:last-child .ai-content');
+ const contentDiv = this.outputContainer.querySelector('.msg-card.ai:last-child .msg-card-body');
if (contentDiv && this._streamChunks && this._streamChunks.length > 0) {
contentDiv.textContent += this._streamChunks.join('');
this._streamChunks = [];
@@ -485,17 +491,13 @@
}
printUserPrompt(rawInput) {
const wrapper = document.createElement('div');
- wrapper.className = 'output-line user-prompt-line';
+ wrapper.className = 'output-line msg-card user';
const safe = rawInput.replace(/</g, '&lt;').replace(/>/g, '&gt;');
const hasHtml = /<[a-z][\s\S]*>/i.test(rawInput.trim());
- const previewBtn = hasHtml ? `<button class="copy-btn" onclick="webx11.previewHtmlFromPrompt(this)" data-content="${safe.replace(/"/g, '&quot;')}" title="Preview HTML">👁 Preview</button>` : '';
- wrapper.innerHTML = `
- <span class="prompt-prefix">webx11@module:~$ </span>
- <span class="prompt-text">${safe}</span>
- <button class="copy-btn user-copy-btn" onclick="webx11.copyToClipboard(this)" title="Copy prompt">Copy</button>
- ${previewBtn}
- <button class="edit-btn" onclick="webx11.editPrompt('${safe.replace(/'/g, "\\'")}')" title="Edit prompt">Edit</button>
- `;
+ let buttons = '<button class="card-btn copy-btn" onclick="webx11.copyToClipboard(this)">Copy</button>';
+ if (hasHtml) buttons += '<button class="card-btn preview-btn" onclick="webx11.previewHtmlFromPrompt(this)" data-content="' + safe.replace(/"/g, '&quot;') + '">Preview</button>';
+ buttons += '<button class="card-btn edit-btn" onclick="webx11.editPrompt(\'' + safe.replace(/'/g, "\\'") + '\')">Edit</button>';
+ wrapper.innerHTML = '<div class="msg-card-header"><span class="msg-role">👤 User</span><span class="msg-buttons">' + buttons + '</span></div><div class="msg-card-body">' + safe + '</div>';
this.outputContainer.appendChild(wrapper);
this._historyBuffer.push({ l: rawInput, t: 'command-prompt' });
if (this._historyBuffer.length > this._historyMaxLines) this._historyBuffer.shift();
@@ -508,22 +510,37 @@
this.inputField.setSelectionRange(text.length, text.length);
}
copyToClipboard(btn) {
- let text;
- if (btn.classList.contains('user-copy-btn')) {
- // User prompt: text is in .prompt-text sibling
- const pSpan = btn.parentElement.querySelector('.prompt-text');
- if (pSpan) text = pSpan.textContent;
+ let text = '';
+ const card = btn.closest('.msg-card');
+ if (card) {
+ const cardBody = card.querySelector('.msg-card-body');
+ if (cardBody) text = cardBody.textContent;
} else {
- // AI response: text is in next sibling .ai-content
- const contentDiv = btn.nextElementSibling;
- if (contentDiv) text = contentDiv.innerText;
+ const nd = btn.nextElementSibling;
+ if (nd) text = nd.innerText || nd.textContent;
}
if (!text) return;
- navigator.clipboard.writeText(text).then(() => {
- const originalText = btn.textContent;
- btn.textContent = 'Copied!';
- setTimeout(() => { btn.textContent = originalText; }, 2000);
- }).catch(err => { console.error('Failed to copy: ', err); });
+ btn.textContent = 'Copied!';
+ setTimeout(function(){ btn.textContent = 'Copy'; }, 2000);
+ // Use navigator.clipboard on secure contexts (iOS 14+, modern browsers).
+ // This is the preferred API — NOTHING blocks it on HTTPS pages.
+ if (navigator.clipboard) {
+ navigator.clipboard.writeText(text).catch(function(){});
+ }
+ // Also try textarea+execCommand synchronously for older browsers.
+ // DO NOT wrap in setTimeout — iOS Safari requires execCommand to run
+ // in the same microtask as the user's click event.
+ try {
+ var ta = document.createElement('textarea');
+ ta.value = text;
+ ta.style.cssText = 'position:fixed;left:10px;top:10px;width:200px;height:40px;font-size:16px;padding:4px;z-index:-1;opacity:0.01';
+ document.body.appendChild(ta);
+ ta.focus();
+ ta.select();
+ ta.setSelectionRange(0, text.length);
+ document.execCommand('copy');
+ document.body.removeChild(ta);
+ } catch(e) {}
}
printClickable(items) {
const line = document.createElement('div');
@@ -1853,7 +1870,13 @@
const saved = localStorage.getItem('webx11_font_size');
if (saved) {
const s = parseInt(saved);
- if (s >= 8 && s <= 32) document.documentElement.style.fontSize = s + 'px';
+ if (s >= 8 && s <= 32) {
+ document.body.style.fontSize = s + 'px';
+ const tc = document.getElementById('terminal-container');
+ if (tc) tc.style.fontSize = s + 'px';
+ const inp = document.getElementById('command-input');
+ if (inp) inp.style.fontSize = s + 'px';
+ }
}
} catch (e) {}
}
@@ -2004,8 +2027,10 @@
this.print('Usage: font-size <8-32>', 'warning');
return;
}
- document.documentElement.style.fontSize = s + 'px';
+ document.body.style.fontSize = s + 'px';
+ document.getElementById('terminal-container').style.fontSize = s + 'px';
+ document.getElementById('command-input').style.fontSize = s + 'px';
try { localStorage.setItem('webx11_font_size', String(s)); } catch (e) {}
- this.print('Font size set to ' + s + 'px', 'success');
+ this.print('Font size set to ' + s + 'px 👆', 'success');
}
}