Changeset 0.23.0 (#153)

This commit is contained in:
2026-03-05 22:40:26 +00:00
parent 40d9834f64
commit 2fc620e1ac
62 changed files with 6214 additions and 362 deletions

View File

@@ -185,7 +185,7 @@ async function toggleChannelAutoCompact(enabled) {
async function loadChats() {
try {
const resp = await API.listChannels(1, 100, 'direct');
const resp = await API.listChannels(1, 100);
App.chats = (resp.data || []).map(c => ({
id: c.id,
title: c.title,
@@ -404,6 +404,123 @@ async function newChat() {
window.history.replaceState(null, '', `${window.__BASE__ || ''}/`);
}
// ── New Group Chat (v0.23.0) ─────────────────
// Creates a group channel and adds persona participants.
// Personas can @mention each other → AI-to-AI chaining.
async function newGroupChat() {
const personas = (App.models || []).filter(m => m.isPersona);
if (personas.length === 0) {
UI.toast('No personas available. Ask your admin to create one.', 'warning');
return;
}
return new Promise(resolve => {
const overlay = document.createElement('div');
overlay.className = 'confirm-overlay';
const personaCheckboxes = personas.map(p => {
const scope = p.personaScope || 'global';
const badge = scope === 'personal' ? ' <span class="dd-hint">personal</span>'
: scope === 'team' ? ` <span class="dd-hint">${esc(p.personaTeamName || 'team')}</span>` : '';
const handle = p.personaHandle ? ` <span class="group-persona-handle">@${esc(p.personaHandle)}</span>` : '';
return `<label class="group-persona-item">
<input type="checkbox" value="${esc(p.personaId)}" data-name="${esc(p.name)}" data-handle="${esc(p.personaHandle || '')}">
<span class="group-persona-name">${esc(p.name)}${handle}${badge}</span>
<span class="group-persona-model">${esc(p.baseModelId || '')}</span>
</label>`;
}).join('');
overlay.innerHTML = `
<div class="confirm-dialog" style="max-width:480px">
<div class="confirm-header">New Group Chat</div>
<div class="confirm-body">
<div style="margin-bottom:12px">
<label style="font-size:12px;color:var(--text-secondary);display:block;margin-bottom:4px">Channel Name</label>
<input id="groupChatTitle" type="text" class="settings-input" placeholder="e.g. Code Review Team" style="width:100%;box-sizing:border-box" autofocus>
</div>
<div>
<label style="font-size:12px;color:var(--text-secondary);display:block;margin-bottom:6px">Add Personas (select at least one)</label>
<div class="group-persona-list" style="max-height:200px;overflow-y:auto;display:flex;flex-direction:column;gap:4px">
${personaCheckboxes}
</div>
</div>
</div>
<div class="confirm-footer">
<button class="btn-small" data-action="cancel">Cancel</button>
<button class="btn-small btn-primary" data-action="ok">Create</button>
</div>
</div>`;
function close(result) {
overlay.remove();
document.removeEventListener('keydown', onKey);
resolve(result);
}
function onKey(e) {
if (e.key === 'Escape') close(false);
}
overlay.querySelector('[data-action="cancel"]').addEventListener('click', () => close(false));
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(false); });
document.addEventListener('keydown', onKey);
overlay.querySelector('[data-action="ok"]').addEventListener('click', async () => {
const title = overlay.querySelector('#groupChatTitle').value.trim() || 'Group Chat';
const checked = [...overlay.querySelectorAll('.group-persona-list input:checked')];
if (checked.length === 0) {
UI.toast('Select at least one persona', 'warning');
return;
}
try {
// Create group channel
const resp = await API.createChannel(title, null, null, 'group');
const chat = {
id: resp.id, title: resp.title, type: 'group', model: null,
messages: [], messageCount: 0, projectId: resp.project_id || null,
workspace_id: resp.workspace_id || null, updatedAt: resp.updated_at,
settings: resp.settings || {}
};
// Add persona participants
for (const cb of checked) {
try {
await API.addParticipant(chat.id, {
participant_type: 'persona',
participant_id: cb.value,
role: 'member'
});
} catch (e) {
console.warn('Failed to add persona:', cb.dataset.name, e.message);
}
}
App.chats.unshift(chat);
App.currentChatId = chat.id;
UI.renderChatList();
Events.emit('chat.created', { chatId: chat.id, projectId: null }, { localOnly: true });
window.history.replaceState(null, '', `${window.__BASE__ || ''}/chat/${chat.id}`);
// Load the channel model roster (populated by participant auto-add)
if (typeof ChannelModels !== 'undefined') ChannelModels.load(chat.id);
UI.showEmptyState();
const names = checked.map(cb => cb.dataset.name).join(', ');
UI.toast(`Group created with ${checked.length} persona${checked.length > 1 ? 's' : ''}: ${names}`, 'success');
ChatInput.focus();
} catch (e) {
UI.toast('Failed to create group: ' + e.message, 'error');
}
close(true);
});
document.body.appendChild(overlay);
overlay.querySelector('#groupChatTitle').focus();
});
}
async function deleteChat(chatId) {
if (!await showConfirm('Delete this chat?')) return;
try {
@@ -908,6 +1025,65 @@ function _initChatListeners() {
// Multi-model channel roster (v0.20.0)
if (typeof ChannelModels !== 'undefined') ChannelModels.init();
// AI-to-AI chain: handle messages arriving via WebSocket (v0.23.0)
// When a persona @mentions another persona, the follow-up completion
// runs server-side and delivers the result via WS, not SSE.
Events.on('message.created', (payload) => {
if (!payload || !payload.channel_id) return;
if (payload.channel_id !== App.currentChatId) return;
const chat = App.chats.find(c => c.id === payload.channel_id);
if (!chat) return;
// Append the chained message
chat.messages.push({
id: payload.id,
parent_id: null,
role: payload.role || 'assistant',
content: payload.content || '',
model: payload.model || '',
modelName: payload.display_name || payload.model || '',
timestamp: new Date().toISOString(),
tool_calls: null,
metadata: payload.chain_depth ? { chain_depth: payload.chain_depth } : null,
siblingCount: 1,
siblingIndex: 0,
participant_type: payload.participant_type,
participant_id: payload.participant_id,
});
// Remove typing indicator and re-render
document.getElementById('typingIndicator')?.remove();
UI.renderMessages(chat.messages);
});
// Typing indicators from persona chain activity
Events.on('typing.start', (payload) => {
if (!payload || payload.channel_id !== App.currentChatId) return;
if (payload.participant_type !== 'persona') return;
// Show typing indicator with persona name
document.getElementById('typingIndicator')?.remove();
const container = document.getElementById('chatMessages');
if (!container) return;
const typing = document.createElement('div');
typing.id = 'typingIndicator';
typing.className = 'message assistant';
const name = payload.display_name || 'AI';
typing.innerHTML = `
<div class="msg-row">
<div class="msg-avatar"><span class="avatar-emoji">⚡</span></div>
<div class="msg-body"><span class="chain-typing-name">${name}</span> <div class="typing-dots"><span></span><span></span><span></span></div></div>
</div>`;
container.appendChild(typing);
container.scrollTop = container.scrollHeight;
});
Events.on('typing.stop', (payload) => {
if (!payload || payload.channel_id !== App.currentChatId) return;
document.getElementById('typingIndicator')?.remove();
});
// Close modals on overlay click
document.querySelectorAll('.modal-overlay').forEach(overlay => {
overlay.addEventListener('click', (e) => {