Changeset 0.17.0 (#75)

This commit is contained in:
2026-02-27 16:25:39 +00:00
parent 8bb77710b9
commit c9141a6896
37 changed files with 2778 additions and 968 deletions

View File

@@ -38,7 +38,7 @@ async function summarizeAndContinue() {
siblingIndex: m.sibling_index || 0,
}));
UI.renderMessages(chat.messages);
updateContextWarning();
updateContextWarning(); updateChatTokenCount();
updateInputTokens();
}
} catch (err) {
@@ -96,6 +96,7 @@ async function loadChats() {
async function selectChat(chatId) {
clearStaged(); // Discard any staged attachments from previous chat
App.currentChatId = chatId;
try { sessionStorage.setItem('cs-active-chat', chatId); } catch (_) {}
UI.renderChatList();
if (window.innerWidth <= 768) {
document.getElementById('sidebar').classList.add('collapsed');
@@ -137,11 +138,32 @@ async function selectChat(chatId) {
UI.renderMessages(chat.messages);
UI.showRegenerate(chat.messages.some(m => m.role === 'assistant'));
Tokens._warningDismissed = false;
updateContextWarning();
updateContextWarning(); updateChatTokenCount();
updateInputTokens();
// Refresh KB toggle state for the new channel
if (typeof KnowledgeUI !== 'undefined') KnowledgeUI.onChatChanged();
updateChatTokenCount();
}
// ── Chat Header Token Count ──────────────────
function updateChatTokenCount() {
const el = document.getElementById('chatTokenCount');
if (!el) return;
const chat = App.chats.find(c => c.id === App.currentChatId);
if (!chat || !chat.messages.length) { el.textContent = ''; return; }
const tokens = Tokens.estimateConversation(chat.messages, App.settings?.systemPrompt);
const budget = Tokens.getContextBudget();
if (budget.maxContext > 0) {
const pct = tokens / budget.maxContext;
el.textContent = `${Tokens.format(tokens)} / ${Tokens.format(budget.maxContext)}`;
el.className = 'chat-token-count' + (pct > 0.9 ? ' danger' : pct > 0.75 ? ' warning' : '');
} else {
el.textContent = `~${Tokens.format(tokens)}`;
el.className = 'chat-token-count';
}
}
// ── Per-Chat Model/Preset Persistence ────────
@@ -242,7 +264,7 @@ async function newChat() {
UI.showEmptyState();
UI.showRegenerate(false);
Tokens._warningDismissed = false;
updateContextWarning();
updateContextWarning(); updateChatTokenCount();
updateInputTokens();
document.getElementById('messageInput').focus();
if (window.innerWidth <= 768) {
@@ -269,6 +291,89 @@ async function deleteChat(chatId) {
} catch (e) { UI.toast('Failed to delete: ' + e.message, 'error'); }
}
// ── Auto-Name Chat (utility model) ───────────
let _autoNamePending = new Set(); // debounce: one inflight per chat
async function autoNameChat(chatId, chat) {
if (!chatId || !chat) return;
if (_autoNamePending.has(chatId)) return;
// Only auto-name if title looks auto-generated (first 50 chars of user msg or "New Chat")
const firstUserMsg = chat.messages.find(m => m.role === 'user');
if (!firstUserMsg) return;
const truncated = firstUserMsg.content.slice(0, 50).trim();
const currentTitle = (chat.title || '').trim();
const isAutoGenerated = !currentTitle ||
currentTitle === 'New Chat' ||
currentTitle === truncated ||
truncated.startsWith(currentTitle);
if (!isAutoGenerated) return;
_autoNamePending.add(chatId);
try {
const resp = await API.generateTitle(chatId);
if (resp?.title && resp.title !== chat.title) {
chat.title = resp.title;
UI.renderChatList();
}
} catch (e) {
// Silently fail — keep truncated title
console.debug('Auto-name failed:', e.message);
} finally {
_autoNamePending.delete(chatId);
}
}
// ── Chat Rename (inline edit) ────────────────
function startRenameChat(chatId) {
const chat = App.chats.find(c => c.id === chatId);
if (!chat) return;
// Find the title span for this chat
const items = document.querySelectorAll('.chat-item');
let titleSpan = null;
for (const item of items) {
if (item.getAttribute('onclick')?.includes(chatId)) {
titleSpan = item.querySelector('.chat-item-title');
break;
}
}
if (!titleSpan) return;
// Replace span with input
const input = document.createElement('input');
input.type = 'text';
input.className = 'chat-rename-input';
input.value = chat.title || '';
input.onclick = (e) => e.stopPropagation();
input.ondblclick = (e) => e.stopPropagation();
const commit = async () => {
const newTitle = input.value.trim();
if (newTitle && newTitle !== chat.title) {
try {
await API.updateChannel(chatId, { title: newTitle });
chat.title = newTitle;
} catch (e) {
UI.toast('Rename failed: ' + e.message, 'error');
}
}
UI.renderChatList();
};
input.addEventListener('blur', commit);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); input.blur(); }
if (e.key === 'Escape') { input.value = chat.title || ''; input.blur(); }
});
titleSpan.replaceWith(input);
input.focus();
input.select();
}
// ── Send Message ─────────────────────────────
async function sendMessage() {
@@ -328,6 +433,11 @@ async function sendMessage() {
// Persist the model/preset selection for this chat
_saveChatModel(App.currentChatId);
// Auto-name: title the chat after first assistant response (v0.17.0)
if (chat.messages.length <= 3) {
autoNameChat(App.currentChatId, chat);
}
} catch (e) {
if (e.name === 'AbortError') {
UI.toast('Generation stopped', 'warning');
@@ -381,7 +491,7 @@ async function reloadActivePath() {
chat.messageCount = chat.messages.length;
await loadChannelAttachments(App.currentChatId);
UI.renderMessages(chat.messages);
updateContextWarning();
updateContextWarning(); updateChatTokenCount();
} catch (e) {
console.error('Failed to reload path:', e.message);
}