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.js97
1 files changed, 87 insertions, 10 deletions
diff --git a/modules/core/terminal.js b/modules/core/terminal.js
index de2f4a7..d53ab70 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -419,6 +419,7 @@
return fragments.length > 0 ? fragments : null;
}
printAIResponse(text) {
+ this._flushStreamChunks();
const fragments = this._renderHTMLBlock(text);
if (fragments) {
for (const frag of fragments) {
@@ -443,6 +444,7 @@
this._trimOutput();
}
printStreamingResponse(text, append) {
+ if (!this._streamChunks) this._streamChunks = [];
if (!append) {
const safeText = text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
const wrapper = document.createElement('div');
@@ -452,16 +454,30 @@
<div class="ai-content">${safeText}</div>
`;
this.outputContainer.appendChild(wrapper);
+ this._streamChunks = [];
} else {
const contentDiv = this.outputContainer.querySelector('.output-line.ollama:last-child .ai-content');
if (contentDiv) {
- const safeText = text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
- contentDiv.textContent += safeText;
+ this._streamChunks.push(text);
+ if (this._streamChunks.length >= 5) {
+ contentDiv.textContent += this._streamChunks.join('');
+ this._streamChunks = [];
+ if (contentDiv.textContent.length > 50000) {
+ contentDiv.textContent = contentDiv.textContent.slice(-30000);
+ }
+ }
}
}
this.scrollToBottom();
this._trimOutput();
}
+ _flushStreamChunks() {
+ const contentDiv = this.outputContainer.querySelector('.output-line.ollama:last-child .ai-content');
+ if (contentDiv && this._streamChunks && this._streamChunks.length > 0) {
+ contentDiv.textContent += this._streamChunks.join('');
+ this._streamChunks = [];
+ }
+ }
copyToClipboard(btn) {
const contentDiv = btn.nextElementSibling;
const text = contentDiv.innerText;
@@ -576,8 +592,9 @@
}
if (toRender.length > batchSize) {
const remaining = toRender.slice(batchSize);
- const doBatch = (index) => {
- if (index >= remaining.length) {
+ const MAX_BATCHES = 50;
+ const doBatch = (index, batchCount) => {
+ if (index >= remaining.length || batchCount >= MAX_BATCHES) {
this.scrollToBottom();
return;
}
@@ -589,10 +606,10 @@
line.textContent = remaining[i].l || '';
this.outputContainer.appendChild(line);
}
- doBatch(end);
+ doBatch(end, (batchCount || 0) + 1);
});
};
- doBatch(0);
+ doBatch(0, 0);
} else {
this.scrollToBottom();
}
@@ -652,12 +669,29 @@
this._autoSaveSession();
this._dirty = false;
}, 180000);
+ window.addEventListener('beforeunload', () => {
+ if (this._autoSaveTimer) {
+ clearInterval(this._autoSaveTimer);
+ this._autoSaveTimer = null;
+ }
+ });
}
_autoSaveSession() {
try {
this.fs.mkdirRecursive('_sessions');
- // Clone fsRoot and strip _sessions to prevent recursive nesting
- var rootClone = JSON.parse(JSON.stringify(this.fs.root));
+ var rootClone;
+ try {
+ const json = JSON.stringify(this.fs.root);
+ if (json.length > 5242880) { // 5MB cap
+ rootClone = { type: 'directory', name: '/', content: {}, _truncated: true };
+ this.print('⚠ Auto-save skipped (VFS too large)', 'warning');
+ return;
+ }
+ rootClone = JSON.parse(json);
+ } catch (e) {
+ rootClone = { type: 'directory', name: '/', content: {}, _error: true };
+ return;
+ }
if (rootClone && rootClone.content && rootClone.content['_sessions']) {
delete rootClone.content['_sessions'];
}
@@ -669,8 +703,12 @@
version: 1,
terminalLines: this._historyBuffer.slice(-50)
};
- this.fs.writeFile('_sessions/_auto.json', JSON.stringify(snapshot, null, 2));
- // Also persist AI context concurrently
+ const snapshotJson = JSON.stringify(snapshot);
+ if (snapshotJson.length > 5242880) {
+ this.print('⚠ Auto-save snapshot too large, skipping', 'warning');
+ return;
+ }
+ this.fs.writeFile('_sessions/_auto.json', snapshotJson);
try {
const ctx = localStorage.getItem('webx11_ai_context');
if (ctx) this.fs.writeFile('_sessions/_auto-context.json', ctx);
@@ -1451,6 +1489,28 @@
}
this.ollamaManager.saveSettings();
}
+ toggleStream() {
+ const cb = document.getElementById('ollama-stream-toggle');
+ const label = document.getElementById('ollama-stream-label');
+ const slider = document.getElementById('ollama-stream-slider');
+ const enabled = cb.checked;
+ this.ollamaManager.setStreamEnabled(enabled);
+ if (label) {
+ label.textContent = enabled ? 'Enabled' : 'Disabled';
+ label.style.color = enabled ? 'var(--success-color)' : 'var(--text-muted)';
+ }
+ if (slider) {
+ slider.style.background = enabled ? 'var(--accent-color)' : 'var(--bg-tertiary)';
+ slider.style.borderColor = enabled ? 'var(--success-color)' : 'var(--border-color)';
+ }
+ }
+ updateStreamIndicator() {
+ const el = document.getElementById('stream-status');
+ if (!el) return;
+ const enabled = this.ollamaManager?.streamEnabled !== false;
+ el.textContent = enabled ? '▶ Stream' : '🔇 No-Stream';
+ el.style.color = enabled ? 'var(--success-color)' : 'var(--text-muted)';
+ }
saveValidatorConfig() {
const select = document.getElementById('validator-model-select');
const endpoint = document.getElementById('validator-endpoint-input')?.value?.trim() || 'http://localhost:11434';
@@ -1529,6 +1589,23 @@
valSlider.style.borderColor = enabled ? 'var(--info-color)' : 'var(--border-color)';
}
}
+ // Initialize stream toggle from stored state
+ const strCb = document.getElementById('ollama-stream-toggle');
+ const strLabel = document.getElementById('ollama-stream-label');
+ const strSlider = document.getElementById('ollama-stream-slider');
+ if (strCb && this.ollamaManager) {
+ const enabled = this.ollamaManager.streamEnabled !== false;
+ strCb.checked = enabled;
+ if (strLabel) {
+ strLabel.textContent = enabled ? 'Enabled' : 'Disabled';
+ strLabel.style.color = enabled ? 'var(--success-color)' : 'var(--text-muted)';
+ }
+ if (strSlider) {
+ strSlider.style.background = enabled ? 'var(--accent-color)' : 'var(--bg-tertiary)';
+ strSlider.style.borderColor = enabled ? 'var(--success-color)' : 'var(--border-color)';
+ }
+ }
+ this.updateStreamIndicator();
// Populate validator model dropdown from stored config
const valSelect = document.getElementById('validator-model-select');
const valEndpoint = document.getElementById('validator-endpoint-input');