v1.2.1 - Fix session data loss on switch, add terminal panel for coding/agentic modes

This commit is contained in:
admin
2026-05-19 15:28:15 +04:00
Unverified
parent d62a850ac5
commit 9f7e3b03ae
6 changed files with 456 additions and 12 deletions

View File

@@ -631,6 +631,16 @@ data: [DONE]
## Changelog ## Changelog
### v1.2.1 (2026-05-19)
- Fixed: messages lost when switching conversations during streaming generation
- Streaming responses auto-save every 20 tokens to prevent data loss
- Partial responses preserved when switching sessions mid-generation
- Added in-app terminal panel for Coding and Agentic modes
- Terminal parses code blocks, file operations, and tool calls from AI responses
- Terminal toggle button with collapsible panel (persists state)
- Conversation list now shows message count per session
- Improved conversation switch safety with flush-before-switch pattern
### v1.2.0 (2026-05-19) ### v1.2.0 (2026-05-19)
- Added light mode / dark mode toggle - Added light mode / dark mode toggle
- Theme persists across sessions via localStorage - Theme persists across sessions via localStorage

View File

@@ -7,8 +7,8 @@ android {
applicationId "ai.z.chat" applicationId "ai.z.chat"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2 versionCode 3
versionName "1.2.0" versionName "1.2.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'

View File

@@ -1,6 +1,6 @@
{ {
"name": "zai-chat", "name": "zai-chat",
"version": "1.2.0", "version": "1.2.1",
"description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan", "description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@@ -693,6 +693,155 @@ a:hover { text-decoration: underline; }
font-size: 12px; font-size: 12px;
} }
.terminal-panel {
display: none;
flex-direction: column;
background: var(--bg-code);
border-top: 1px solid var(--border);
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
flex-shrink: 0;
}
.terminal-panel.open {
max-height: 45vh;
}
.terminal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.terminal-title {
font-size: 12px;
font-weight: 700;
color: var(--success);
text-transform: uppercase;
letter-spacing: 1px;
font-family: 'Fira Code', 'JetBrains Mono', monospace;
}
.terminal-info {
font-size: 11px;
color: var(--text-muted);
font-family: monospace;
}
.terminal-body {
overflow-y: auto;
padding: 8px;
font-family: 'Fira Code', 'JetBrains Mono', 'Cascadia Code', monospace;
font-size: 12px;
line-height: 1.5;
flex: 1;
min-height: 60px;
max-height: calc(45vh - 40px);
}
.term-empty {
color: var(--text-muted);
text-align: center;
padding: 20px;
font-size: 12px;
}
.term-entry {
margin-bottom: 8px;
border-radius: 6px;
overflow: hidden;
border: 1px solid var(--border);
}
.term-tool {
background: var(--bg-secondary);
}
.term-tool-header {
padding: 6px 10px;
font-size: 11px;
font-weight: 700;
font-family: monospace;
background: var(--bg-tertiary);
}
.term-action {
font-weight: 800;
letter-spacing: 0.5px;
}
.term-target {
color: var(--text-secondary);
font-weight: 400;
margin-left: 4px;
word-break: break-all;
}
.term-code-block {
background: var(--bg-secondary);
}
.term-file-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 10px;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border);
}
.term-lang {
font-size: 11px;
color: var(--accent);
font-weight: 600;
}
.term-copy-btn {
background: var(--accent-dim);
border: none;
color: var(--accent);
padding: 2px 8px;
border-radius: 3px;
font-size: 10px;
cursor: pointer;
font-family: inherit;
}
.term-copy-btn:hover { background: var(--accent); color: white; }
.term-code {
padding: 8px 10px;
margin: 0;
font-size: 11px;
line-height: 1.4;
color: var(--text-primary);
white-space: pre-wrap;
word-break: break-all;
overflow-x: auto;
max-height: 200px;
overflow-y: auto;
background: transparent;
}
.terminal-toggle-btn {
display: none;
align-items: center;
justify-content: center;
gap: 6px;
width: 100%;
padding: 8px;
background: var(--bg-tertiary);
border: none;
border-top: 1px solid var(--border);
color: var(--text-secondary);
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all var(--transition);
flex-shrink: 0;
}
.terminal-toggle-btn:hover {
background: var(--accent-dim);
color: var(--accent);
}
.terminal-icon {
font-size: 10px;
}
.terminal-label {
font-family: 'Fira Code', monospace;
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 11px;
}
/* Responsive */ /* Responsive */
@media (max-width: 480px) { @media (max-width: 480px) {
.message { max-width: 92%; } .message { max-width: 92%; }

View File

@@ -96,6 +96,18 @@
<div id="messages" class="messages"></div> <div id="messages" class="messages"></div>
<div id="terminal-panel" class="terminal-panel">
<div class="terminal-header">
<span class="terminal-title">&#9632; Terminal</span>
<span class="terminal-info" id="terminal-info"></span>
</div>
<div id="terminal-body" class="terminal-body"></div>
</div>
<button id="terminal-toggle" class="terminal-toggle-btn" style="display:none">
<span class="terminal-icon">&#9654;</span>
<span class="terminal-label">Show Terminal</span>
</button>
<div class="chat-input-area"> <div class="chat-input-area">
<div id="mode-selector" class="mode-selector"> <div id="mode-selector" class="mode-selector">
<button class="mode-btn active" data-mode="chat">Chat</button> <button class="mode-btn active" data-mode="chat">Chat</button>
@@ -185,13 +197,28 @@
</div> </div>
<div class="settings-section"> <div class="settings-section">
<h3>About</h3> <h3>About</h3>
<p class="about-text">Z.AI Chat v1.2.0</p> <p class="about-text">Z.AI Chat v1.2.1</p>
<p class="about-text">Built with Z.AI SDK &amp; GLM-5.1</p> <p class="about-text">Built with Z.AI SDK &amp; GLM-5.1</p>
<p class="about-text">Compatible with Android 15/16</p> <p class="about-text">Compatible with Android 15/16</p>
</div> </div>
<div class="settings-section"> <div class="settings-section">
<h3>Changelog</h3> <h3>Changelog</h3>
<ul class="changelog-list"> <ul class="changelog-list">
<li>
<span class="changelog-version">v1.2.1</span>
<span class="changelog-date">2026-05-19</span>
<ul>
<li>Fixed: messages lost when switching conversations during streaming</li>
<li>Streaming responses now auto-save every 20 tokens to prevent data loss</li>
<li>Partial responses preserved when switching sessions mid-generation</li>
<li>Added in-app terminal panel for Coding and Agentic modes</li>
<li>Terminal parses code blocks, file ops, and tool calls from AI responses</li>
<li>Terminal toggle button with collapsible panel</li>
<li>Terminal state persists across sessions</li>
<li>Conversation list now shows message count</li>
<li>Improved conversation switch safety with flush-before-switch</li>
</ul>
</li>
<li> <li>
<span class="changelog-version">v1.2.0</span> <span class="changelog-version">v1.2.0</span>
<span class="changelog-date">2026-05-19</span> <span class="changelog-date">2026-05-19</span>

View File

@@ -10,7 +10,6 @@
brainstorm: 'You are a creative brainstorming partner. Generate diverse ideas, explore unconventional angles, build on concepts, and help evaluate trade-offs. Think freely and expansively. Present ideas in organized lists or tables when appropriate.', brainstorm: 'You are a creative brainstorming partner. Generate diverse ideas, explore unconventional angles, build on concepts, and help evaluate trade-offs. Think freely and expansively. Present ideas in organized lists or tables when appropriate.',
agentic: 'You are an autonomous coding agent. Break down complex tasks into clear steps. Write production-quality code with proper error handling, tests, and documentation. Think through the architecture before coding. Use tool-calling format when appropriate: [SEARCH], [CREATE_FILE], [EDIT_FILE], [RUN_COMMAND]. Always verify your work.' agentic: 'You are an autonomous coding agent. Break down complex tasks into clear steps. Write production-quality code with proper error handling, tests, and documentation. Think through the architecture before coding. Use tool-calling format when appropriate: [SEARCH], [CREATE_FILE], [EDIT_FILE], [RUN_COMMAND]. Always verify your work.'
}; };
var MODE_EMOJIS = { chat: '\u{1F4AC}', coding: '\u{1F4BB}', brainstorm: '\u{1F4A1}', agentic: '\u{1F916}' };
var state = { var state = {
apiKey: '', apiKey: '',
@@ -25,7 +24,11 @@
conversations: [], conversations: [],
activeConversationId: null, activeConversationId: null,
isGenerating: false, isGenerating: false,
abortController: null abortController: null,
streamingConvId: null,
streamingContent: '',
streamingResponseDiv: null,
terminalOpen: false
}; };
function $(sel) { return document.querySelector(sel); } function $(sel) { return document.querySelector(sel); }
@@ -42,6 +45,7 @@
state.webSearch = localStorage.getItem(STORAGE_KEY + 'webSearch') === 'true'; state.webSearch = localStorage.getItem(STORAGE_KEY + 'webSearch') === 'true';
state.currentMode = localStorage.getItem(STORAGE_KEY + 'currentMode') || 'chat'; state.currentMode = localStorage.getItem(STORAGE_KEY + 'currentMode') || 'chat';
state.theme = localStorage.getItem(STORAGE_KEY + 'theme') || 'dark'; state.theme = localStorage.getItem(STORAGE_KEY + 'theme') || 'dark';
state.terminalOpen = localStorage.getItem(STORAGE_KEY + 'terminalOpen') === 'true';
var convData = localStorage.getItem(STORAGE_KEY + 'conversations'); var convData = localStorage.getItem(STORAGE_KEY + 'conversations');
state.conversations = convData ? JSON.parse(convData) : []; state.conversations = convData ? JSON.parse(convData) : [];
state.activeConversationId = localStorage.getItem(STORAGE_KEY + 'activeConv') || null; state.activeConversationId = localStorage.getItem(STORAGE_KEY + 'activeConv') || null;
@@ -59,6 +63,7 @@
localStorage.setItem(STORAGE_KEY + 'webSearch', state.webSearch.toString()); localStorage.setItem(STORAGE_KEY + 'webSearch', state.webSearch.toString());
localStorage.setItem(STORAGE_KEY + 'currentMode', state.currentMode); localStorage.setItem(STORAGE_KEY + 'currentMode', state.currentMode);
localStorage.setItem(STORAGE_KEY + 'theme', state.theme); localStorage.setItem(STORAGE_KEY + 'theme', state.theme);
localStorage.setItem(STORAGE_KEY + 'terminalOpen', state.terminalOpen.toString());
localStorage.setItem(STORAGE_KEY + 'conversations', JSON.stringify(state.conversations)); localStorage.setItem(STORAGE_KEY + 'conversations', JSON.stringify(state.conversations));
localStorage.setItem(STORAGE_KEY + 'activeConv', state.activeConversationId || ''); localStorage.setItem(STORAGE_KEY + 'activeConv', state.activeConversationId || '');
} catch(e) { console.error('Save state error:', e); } } catch(e) { console.error('Save state error:', e); }
@@ -66,12 +71,36 @@
function genId() { return Date.now().toString(36) + Math.random().toString(36).substr(2, 9); } function genId() { return Date.now().toString(36) + Math.random().toString(36).substr(2, 9); }
function getConversation() { function getConversation(id) {
if (!state.activeConversationId) return null; var targetId = id || state.activeConversationId;
return state.conversations.find(function(c) { return c.id === state.activeConversationId; }); if (!targetId) return null;
return state.conversations.find(function(c) { return c.id === targetId; });
}
function flushStreamingToConversation() {
if (state.streamingConvId && state.streamingContent) {
var conv = getConversation(state.streamingConvId);
if (conv) {
var lastMsg = conv.messages[conv.messages.length - 1];
if (lastMsg && lastMsg.role === 'assistant' && lastMsg._streaming) {
lastMsg.content = state.streamingContent;
delete lastMsg._streaming;
} else {
conv.messages.push({ role: 'assistant', content: state.streamingContent });
}
saveState();
}
}
state.streamingConvId = null;
state.streamingContent = '';
state.streamingResponseDiv = null;
} }
function newConversation() { function newConversation() {
flushStreamingToConversation();
if (state.isGenerating) {
stopGeneration();
}
var conv = { var conv = {
id: genId(), id: genId(),
title: 'New Chat', title: 'New Chat',
@@ -81,13 +110,27 @@
}; };
state.conversations.unshift(conv); state.conversations.unshift(conv);
state.activeConversationId = conv.id; state.activeConversationId = conv.id;
state.isGenerating = false;
state.abortController = null;
saveState(); saveState();
renderConversationList(); renderConversationList();
renderMessages(); renderMessages();
updateHeader(); updateHeader();
updateSendButton();
updateTerminalVisibility();
} }
function switchConversation(id) { function switchConversation(id) {
if (id === state.activeConversationId) {
closeSidebar();
return;
}
flushStreamingToConversation();
if (state.isGenerating) {
stopGeneration();
state.isGenerating = false;
state.abortController = null;
}
state.activeConversationId = id; state.activeConversationId = id;
var conv = getConversation(); var conv = getConversation();
if (conv) { if (conv) {
@@ -98,10 +141,17 @@
renderConversationList(); renderConversationList();
renderMessages(); renderMessages();
updateHeader(); updateHeader();
updateSendButton();
updateTerminalVisibility();
closeSidebar(); closeSidebar();
} }
function deleteConversation(id) { function deleteConversation(id) {
if (state.streamingConvId === id) {
flushStreamingToConversation();
if (state.isGenerating) stopGeneration();
state.isGenerating = false;
}
state.conversations = state.conversations.filter(function(c) { return c.id !== id; }); state.conversations = state.conversations.filter(function(c) { return c.id !== id; });
if (state.activeConversationId === id) { if (state.activeConversationId === id) {
state.activeConversationId = state.conversations.length > 0 ? state.conversations[0].id : null; state.activeConversationId = state.conversations.length > 0 ? state.conversations[0].id : null;
@@ -110,6 +160,7 @@
renderConversationList(); renderConversationList();
renderMessages(); renderMessages();
updateHeader(); updateHeader();
updateTerminalVisibility();
} }
function updateHeader() { function updateHeader() {
@@ -135,7 +186,10 @@
state.conversations.forEach(function(conv) { state.conversations.forEach(function(conv) {
var div = document.createElement('div'); var div = document.createElement('div');
div.className = 'conv-item' + (conv.id === state.activeConversationId ? ' active' : ''); div.className = 'conv-item' + (conv.id === state.activeConversationId ? ' active' : '');
div.innerHTML = '<span class="conv-title">' + escapeHtml(conv.title) + '</span>' + var msgCount = conv.messages.length;
div.innerHTML = '<span class="conv-title">' + escapeHtml(conv.title) +
(msgCount > 0 ? ' <span style="color:var(--text-muted);font-size:11px">(' + msgCount + ')</span>' : '') +
'</span>' +
'<button class="conv-delete" data-id="' + conv.id + '">&times;</button>'; '<button class="conv-delete" data-id="' + conv.id + '">&times;</button>';
div.addEventListener('click', function(e) { div.addEventListener('click', function(e) {
if (e.target.classList.contains('conv-delete')) { if (e.target.classList.contains('conv-delete')) {
@@ -204,6 +258,7 @@
appendMessage(msg.role, msg.content, container, false); appendMessage(msg.role, msg.content, container, false);
}); });
container.scrollTop = container.scrollHeight; container.scrollTop = container.scrollHeight;
updateTerminalContent();
} }
function appendMessage(role, content, container, animate) { function appendMessage(role, content, container, animate) {
@@ -270,12 +325,15 @@
} }
conv.messages.push({ role: 'user', content: text }); conv.messages.push({ role: 'user', content: text });
saveState();
input.value = ''; input.value = '';
autoResize(input); autoResize(input);
updateSendButton(); updateSendButton();
appendMessage('user', text); appendMessage('user', text);
state.isGenerating = true; state.isGenerating = true;
state.streamingConvId = conv.id;
state.streamingContent = '';
updateSendButton(); updateSendButton();
showThinking(); showThinking();
@@ -283,7 +341,7 @@
var systemPrompt = MODE_PROMPTS[state.currentMode] || MODE_PROMPTS.chat; var systemPrompt = MODE_PROMPTS[state.currentMode] || MODE_PROMPTS.chat;
var apiMessages = [{ role: 'system', content: systemPrompt }]; var apiMessages = [{ role: 'system', content: systemPrompt }];
conv.messages.forEach(function(m) { conv.messages.forEach(function(m) {
if (m.role === 'user' || m.role === 'assistant') { if (m.role === 'user' || (m.role === 'assistant' && !m._streaming)) {
apiMessages.push({ role: m.role, content: m.content }); apiMessages.push({ role: m.role, content: m.content });
} }
}); });
@@ -305,6 +363,7 @@
removeThinking(); removeThinking();
var responseDiv = appendMessage('assistant', ''); var responseDiv = appendMessage('assistant', '');
state.streamingResponseDiv = responseDiv;
if (state.streaming) { if (state.streaming) {
await streamResponse(requestBody, responseDiv, conv); await streamResponse(requestBody, responseDiv, conv);
@@ -312,6 +371,7 @@
var result = await apiRequest(requestBody); var result = await apiRequest(requestBody);
var content = result.choices[0].message.content; var content = result.choices[0].message.content;
updateStreamingMessage(responseDiv, content); updateStreamingMessage(responseDiv, content);
state.streamingContent = content;
conv.messages.push({ role: 'assistant', content: content }); conv.messages.push({ role: 'assistant', content: content });
} }
} catch(err) { } catch(err) {
@@ -319,11 +379,17 @@
if (err.name !== 'AbortError') { if (err.name !== 'AbortError') {
appendMessage('system', 'Error: ' + (err.message || 'Request failed')); appendMessage('system', 'Error: ' + (err.message || 'Request failed'));
} }
if (state.streamingContent) {
conv.messages.push({ role: 'assistant', content: state.streamingContent, _streaming: false });
}
} finally { } finally {
state.isGenerating = false; state.isGenerating = false;
state.abortController = null; state.abortController = null;
state.streamingConvId = null;
state.streamingResponseDiv = null;
updateSendButton(); updateSendButton();
saveState(); saveState();
updateTerminalContent();
} }
} }
@@ -346,6 +412,8 @@
return await resp.json(); return await resp.json();
} }
var _streamAutoSaveCounter = 0;
async function streamResponse(body, responseDiv, conv) { async function streamResponse(body, responseDiv, conv) {
state.abortController = new AbortController(); state.abortController = new AbortController();
body.stream = true; body.stream = true;
@@ -372,6 +440,7 @@
var decoder = new TextDecoder(); var decoder = new TextDecoder();
var fullContent = ''; var fullContent = '';
var buffer = ''; var buffer = '';
_streamAutoSaveCounter = 0;
while (true) { while (true) {
var chunk = await reader.read(); var chunk = await reader.read();
@@ -392,19 +461,39 @@
var delta = parsed.choices && parsed.choices[0] && parsed.choices[0].delta; var delta = parsed.choices && parsed.choices[0] && parsed.choices[0].delta;
if (delta && delta.content) { if (delta && delta.content) {
fullContent += delta.content; fullContent += delta.content;
state.streamingContent = fullContent;
if (responseDiv && responseDiv.parentElement) {
updateStreamingMessage(responseDiv, fullContent); updateStreamingMessage(responseDiv, fullContent);
} }
_streamAutoSaveCounter++;
if (_streamAutoSaveCounter % 20 === 0) {
_saveStreamingProgress(conv, fullContent);
}
}
} catch(e) {} } catch(e) {}
} }
} }
conv.messages.push({ role: 'assistant', content: fullContent }); conv.messages.push({ role: 'assistant', content: fullContent });
state.streamingContent = '';
}
function _saveStreamingProgress(conv, content) {
if (!conv) return;
var last = conv.messages[conv.messages.length - 1];
if (last && last.role === 'assistant' && last._streaming) {
last.content = content;
} else {
conv.messages.push({ role: 'assistant', content: content, _streaming: true });
}
saveState();
} }
function stopGeneration() { function stopGeneration() {
if (state.abortController) { if (state.abortController) {
state.abortController.abort(); state.abortController.abort();
} }
flushStreamingToConversation();
} }
function updateSendButton() { function updateSendButton() {
@@ -457,6 +546,170 @@
applyTheme(state.theme === 'dark' ? 'light' : 'dark'); applyTheme(state.theme === 'dark' ? 'light' : 'dark');
} }
// ---- Terminal Panel ----
function parseTerminalEntries(content) {
if (!content) return [];
var entries = [];
var toolRegex = /\[(CREATE_FILE|EDIT_FILE|DELETE_FILE|RUN_COMMAND|SEARCH|READ_FILE|BUILD|TEST)\]\s*\(([^)]*)\)\s*\n?([\s\S]*?)(?=\n\[|$)/gi;
var match;
while ((match = toolRegex.exec(content)) !== null) {
entries.push({
type: 'tool',
action: match[1],
target: match[2].trim(),
body: match[3].trim()
});
}
var codeBlockRegex = /```(\w*)\n([\s\S]*?)```/gi;
var idx = 0;
while ((match = codeBlockRegex.exec(content)) !== null) {
var isTool = false;
for (var e = 0; e < entries.length; e++) {
if (entries[e].type === 'tool' && match.index >= content.indexOf(entries[e].body) - 20) {
isTool = true;
break;
}
}
if (!isTool) {
idx++;
var firstLine = match[2].trim().split('\n')[0];
var isFilePath = /^(\/|\.\/|\.\.\/|[A-Za-z]:\\|[a-zA-Z0-9_\-]+\.[a-zA-Z]{1,4}$)/.test(firstLine) && firstLine.length < 120 && firstLine.split('\n').length === 1;
entries.push({
type: 'code',
lang: match[1] || 'text',
code: match[2].trim(),
index: idx,
fileName: isFilePath ? firstLine : null
});
}
}
entries.sort(function(a, b) {
return content.indexOf(a.type === 'tool' ? '[' + a.action : '```' + (a.lang || '')) -
content.indexOf(b.type === 'tool' ? '[' + b.action : '```' + (b.lang || ''));
});
return entries;
}
function renderTerminalEntry(entry) {
if (entry.type === 'tool') {
var actionIcon = { CREATE_FILE: '+', EDIT_FILE: '~', DELETE_FILE: '-', RUN_COMMAND: '>', SEARCH: '?', READ_FILE: 'R', BUILD: 'B', TEST: 'T' };
var actionColor = { CREATE_FILE: 'var(--success)', EDIT_FILE: 'var(--warning)', DELETE_FILE: 'var(--danger)', RUN_COMMAND: 'var(--accent)', SEARCH: 'var(--text-secondary)', READ_FILE: 'var(--text-muted)', BUILD: 'var(--accent)', TEST: 'var(--success)' };
var icon = actionIcon[entry.action] || '>';
var color = actionColor[entry.action] || 'var(--accent)';
var html = '<div class="term-entry term-tool" style="border-left:3px solid ' + color + '">';
html += '<div class="term-tool-header"><span class="term-action" style="color:' + color + '">[' + icon + '] ' + escapeHtml(entry.action) + '</span>';
if (entry.target) html += ' <span class="term-target">' + escapeHtml(entry.target) + '</span>';
html += '</div>';
if (entry.body) {
html += '<pre class="term-code">' + escapeHtml(entry.body.substring(0, 2000)) + (entry.body.length > 2000 ? '\n... (truncated)' : '') + '</pre>';
}
html += '</div>';
return html;
}
if (entry.type === 'code') {
var label = entry.fileName ? escapeHtml(entry.fileName) : escapeHtml(entry.lang || 'code');
var displayCode = entry.fileName ? entry.code.split('\n').slice(1).join('\n') : entry.code;
if (!displayCode.trim()) displayCode = entry.code;
var html = '<div class="term-entry term-code-block">';
html += '<div class="term-file-header"><span class="term-lang">' + label + '</span>';
html += '<button class="term-copy-btn" data-code="' + escapeHtml(entry.code).replace(/"/g, '&quot;') + '">Copy</button></div>';
html += '<pre class="term-code">' + escapeHtml(displayCode.substring(0, 3000)) + (displayCode.length > 3000 ? '\n... (truncated)' : '') + '</pre>';
html += '</div>';
return html;
}
return '';
}
function updateTerminalContent() {
var termBody = $('#terminal-body');
if (!termBody) return;
termBody.innerHTML = '';
var conv = getConversation();
if (!conv) return;
var lastAssistant = null;
for (var i = conv.messages.length - 1; i >= 0; i--) {
if (conv.messages[i].role === 'assistant') {
lastAssistant = conv.messages[i];
break;
}
}
if (!lastAssistant && !state.streamingContent) {
termBody.innerHTML = '<div class="term-empty">No code output yet. Use Coding or Agentic mode to generate code.</div>';
return;
}
var content = state.streamingContent || (lastAssistant ? lastAssistant.content : '');
var entries = parseTerminalEntries(content);
if (entries.length === 0) {
termBody.innerHTML = '<div class="term-empty">No structured code blocks or tool calls detected in response.</div>';
return;
}
entries.forEach(function(entry) {
termBody.innerHTML += renderTerminalEntry(entry);
});
termBody.querySelectorAll('.term-copy-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var code = this.getAttribute('data-code').replace(/&quot;/g, '"').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>');
navigator.clipboard.writeText(code).then(function() {
this.textContent = 'Copied!';
setTimeout(function() { this.textContent = 'Copy'; }.bind(this), 2000);
}.bind(this));
});
});
if (state.terminalOpen) {
termBody.scrollTop = termBody.scrollHeight;
}
}
function updateTerminalVisibility() {
var panel = $('#terminal-panel');
var toggleBtn = $('#terminal-toggle');
if (!panel || !toggleBtn) return;
var isDevMode = (state.currentMode === 'coding' || state.currentMode === 'agentic');
if (isDevMode) {
panel.style.display = 'flex';
toggleBtn.style.display = 'flex';
} else {
panel.style.display = 'none';
toggleBtn.style.display = 'none';
state.terminalOpen = false;
}
if (state.terminalOpen && isDevMode) {
panel.classList.add('open');
} else {
panel.classList.remove('open');
}
var label = toggleBtn.querySelector('.terminal-label');
if (label) label.textContent = state.terminalOpen ? 'Hide Terminal' : 'Show Terminal';
}
function toggleTerminal() {
state.terminalOpen = !state.terminalOpen;
var panel = $('#terminal-panel');
if (panel) panel.classList.toggle('open', state.terminalOpen);
var label = $('#terminal-toggle .terminal-label');
if (label) label.textContent = state.terminalOpen ? 'Hide Terminal' : 'Show Terminal';
if (state.terminalOpen) updateTerminalContent();
saveState();
}
// ---- Rest of init ----
async function testConnection(apiKey, baseUrl) { async function testConnection(apiKey, baseUrl) {
var url = (baseUrl || state.baseUrl).replace(/\/+$/, '') + '/chat/completions'; var url = (baseUrl || state.baseUrl).replace(/\/+$/, '') + '/chat/completions';
var resp = await fetch(url, { var resp = await fetch(url, {
@@ -529,6 +782,7 @@
renderMessages(); renderMessages();
updateHeader(); updateHeader();
updateModeSelector(); updateModeSelector();
updateTerminalVisibility();
$('#api-token').value = state.apiKey; $('#api-token').value = state.apiKey;
$('#base-url').value = state.baseUrl; $('#base-url').value = state.baseUrl;
} }
@@ -591,6 +845,7 @@
state.currentMode = this.dataset.mode; state.currentMode = this.dataset.mode;
updateModeSelector(); updateModeSelector();
updateHeader(); updateHeader();
updateTerminalVisibility();
saveState(); saveState();
}); });
}); });
@@ -632,6 +887,8 @@
applyTheme(this.checked ? 'dark' : 'light'); applyTheme(this.checked ? 'dark' : 'light');
}); });
$('#terminal-toggle').addEventListener('click', toggleTerminal);
$('#export-btn').addEventListener('click', exportConversations); $('#export-btn').addEventListener('click', exportConversations);
$('#clear-btn').addEventListener('click', function() { $('#clear-btn').addEventListener('click', function() {
@@ -642,6 +899,7 @@
renderConversationList(); renderConversationList();
renderMessages(); renderMessages();
updateHeader(); updateHeader();
updateTerminalContent();
} }
}); });