Soft keyboards have no reliable Enter-to-send, so chat was unsendable on mobile browsers. Add an optional themed Send button wired through wireAgentChat (Enter-to-send kept for desktop), applied to the Companion rail, Yerin, and Little Blue composers. Blackflame-styled, flex-row layout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
60 lines
2.5 KiB
JavaScript
60 lines
2.5 KiB
JavaScript
// 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 sendBtn = el('button', { class: 'rail-send', type: 'button', title: 'Send', 'aria-label': 'Send' }, '➤');
|
|
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, sendBtn)));
|
|
|
|
// (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, sendBtnEl: sendBtn,
|
|
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);
|
|
});
|
|
}
|