// Plan 5: per-Space companion chat. Chat mechanics live in agent_chat.js; this // file owns only the collapsible rail chrome + Space-change re-init. import { el, mount } from '../dom.js'; import { state, on } from '../state.js'; import { wireAgentChat } from './agent_chat.js'; const COLLAPSE_KEY = 'void_rail_collapsed'; const COMPANION_LABELS = { search: '🔍 searching', read: '📄 reading', context: '🧭 looking at this view', propose_change: '📝 drafting a change' }; export async function renderRightrail(root) { const shell = document.getElementById('shell'); let collapsed = localStorage.getItem(COLLAPSE_KEY) === 'true'; if (collapsed) shell.classList.add('rail-collapsed'); const toggle = () => { collapsed = !collapsed; localStorage.setItem(COLLAPSE_KEY, String(collapsed)); shell.classList.toggle('rail-collapsed', collapsed); }; const log = el('div', { class: 'rail-log' }); const input = el('textarea', { class: 'rail-input', rows: 1, placeholder: 'Ask the companion…' }); const header = el('div', { class: 'rail-hd' }, el('span', { class: 'who' }, '◆ Companion'), el('span', { class: 'chev', onclick: toggle, title: 'Collapse' }, '⟩')); mount(root, el('div', { class: 'rail-toggle', onclick: toggle, title: 'Companion' }, 'CRADLE'), el('div', { class: 'rail-chat' }, header, log, el('div', { class: 'rail-inputwrap' }, input))); // (Re)wire the chat whenever the active Space changes. async function initChat(spaceId) { if (!spaceId) { mount(log, el('p', { class: 'muted' }, 'Open a Space to chat with its companion.')); return; } const chat = wireAgentChat({ logEl: log, inputEl: input, historyUrl: `/api/spaces/${spaceId}/companion`, turnUrl: `/api/spaces/${spaceId}/companion/turn`, agentName: 'Companion', showDrafts: true, toolLabels: COMPANION_LABELS, turnBody: (text) => ({ text, view: state.view || null }) }); await chat.load(); } // The state bus replays its last value on subscribe, so this fires for the // initial route() call and every later navigation. Only re-init when the id // actually changes so navigating within a Space doesn't wipe the conversation. let lastSpaceId; let inited = false; on('space-active', (spaceId) => { if (inited && spaceId === lastSpaceId) return; inited = true; lastSpaceId = spaceId; initChat(spaceId); }); }