feat(chat): add Send button to agent composers (mobile fix)
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>
This commit is contained in:
@@ -38,9 +38,10 @@ function draftCardEl(d, onResolve) {
|
||||
* @param {boolean} [o.showDrafts] render propose_change draft cards (Dross only)
|
||||
* @param {object} [o.toolLabels] tool-name → display string
|
||||
* @param {(text:string)=>object} [o.turnBody] POST body builder
|
||||
* @returns {{ load: () => Promise<void> }}
|
||||
* @param {HTMLElement} [o.sendBtnEl] optional Send button (needed on touch/mobile where there's no Enter key)
|
||||
* @returns {{ load: () => Promise<void>, send: () => Promise<void> }}
|
||||
*/
|
||||
export function wireAgentChat({ logEl, inputEl, historyUrl, turnUrl, agentName, showDrafts = false, toolLabels = {}, turnBody = (text) => ({ text }) }) {
|
||||
export function wireAgentChat({ logEl, inputEl, historyUrl, turnUrl, agentName, showDrafts = false, toolLabels = {}, turnBody = (text) => ({ text }), sendBtnEl = null }) {
|
||||
async function resolveDraft(id, status, cardNode) {
|
||||
try {
|
||||
await api.post(`/api/pending-changes/${id}/${status === 'approved' ? 'approve' : 'reject'}`);
|
||||
@@ -93,10 +94,19 @@ export function wireAgentChat({ logEl, inputEl, historyUrl, turnUrl, agentName,
|
||||
}
|
||||
}
|
||||
|
||||
// Desktop: Enter sends (Shift+Enter = newline). Mobile soft keyboards have no
|
||||
// reliable Enter-to-send, so callers also pass a tappable Send button.
|
||||
if (inputEl._sendHandler) inputEl.removeEventListener('keydown', inputEl._sendHandler);
|
||||
const handler = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } };
|
||||
inputEl._sendHandler = handler;
|
||||
inputEl.addEventListener('keydown', handler);
|
||||
|
||||
return { load };
|
||||
if (sendBtnEl) {
|
||||
if (sendBtnEl._sendHandler) sendBtnEl.removeEventListener('click', sendBtnEl._sendHandler);
|
||||
const click = () => { send(); inputEl.focus(); };
|
||||
sendBtnEl._sendHandler = click;
|
||||
sendBtnEl.addEventListener('click', click);
|
||||
}
|
||||
|
||||
return { load, send };
|
||||
}
|
||||
|
||||
@@ -22,12 +22,13 @@ export async function renderRightrail(root) {
|
||||
|
||||
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)));
|
||||
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) {
|
||||
@@ -36,7 +37,7 @@ export async function renderRightrail(root) {
|
||||
return;
|
||||
}
|
||||
const chat = wireAgentChat({
|
||||
logEl: log, inputEl: input,
|
||||
logEl: log, inputEl: input, sendBtnEl: sendBtn,
|
||||
historyUrl: `/api/spaces/${spaceId}/companion`,
|
||||
turnUrl: `/api/spaces/${spaceId}/companion/turn`,
|
||||
agentName: 'Companion', showDrafts: true, toolLabels: COMPANION_LABELS,
|
||||
|
||||
Reference in New Issue
Block a user