From 8d3df656deafba6812e30c9508fc3257405ed156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Fri, 15 May 2026 19:22:14 +0200 Subject: [PATCH] agent terminal: slash commands /help and /clear, tab-completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit intercept any line starting with / before sending it to the agent inbox. two commands today: - /help — render command list locally - /clear — wipe the local terminal view (server-side event history kept, so a page reload restores it) unknown /xxx surfaces an error row instead of being silently sent. tab on a /prefix cycles through matching command names. submit-hint mentions /help so the operator can discover it. scaffolding for the bigger commands (/compact /cancel /model) is in place — adding them later is a switch arm plus harness work. --- hive-ag3nt/assets/app.js | 65 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/hive-ag3nt/assets/app.js b/hive-ag3nt/assets/app.js index 54e107b..45c1bdc 100644 --- a/hive-ag3nt/assets/app.js +++ b/hive-ag3nt/assets/app.js @@ -155,6 +155,47 @@ let lastOutputLen = -1; let pollTimer = null; let termInputRendered = false; + // Filled in by the live-event IIFE below. Used by the slash-command + // dispatcher to print local-only rows ('help', errors) and to clear + // the terminal on `/clear`. + let termAPI = null; + + const SLASH_COMMANDS = [ + { name: '/help', desc: 'list slash commands' }, + { name: '/clear', desc: 'wipe the terminal panel (local-only)' }, + ]; + + function handleSlashCommand(line) { + if (!termAPI) return false; + const trimmed = line.trim(); + if (!trimmed.startsWith('/')) return false; + const [cmd] = trimmed.split(/\s+/); + switch (cmd) { + case '/help': + termAPI.row('note', '· /help'); + for (const c of SLASH_COMMANDS) { + termAPI.row('note', ' ' + c.name.padEnd(10) + ' — ' + c.desc); + } + return true; + case '/clear': + termAPI.clear(); + termAPI.row('note', '· terminal cleared (local view only — server history kept)'); + return true; + default: + termAPI.row('turn-end-fail', '✗ unknown slash command: ' + cmd + ' — try /help'); + return true; + } + } + + // Cycle through commands when operator hits Tab on a `/…` prefix. + function completeSlash(prefix) { + const matches = SLASH_COMMANDS.filter((c) => c.name.startsWith(prefix)); + if (!matches.length) return null; + // Cycle: when the current prefix already equals a command name, + // advance to the next match. + const idx = matches.findIndex((c) => c.name === prefix); + return matches[(idx + 1) % matches.length].name; + } function renderTermInput(label, online) { const slot = $('term-input'); @@ -178,9 +219,24 @@ }; ta.addEventListener('input', grow); ta.addEventListener('keydown', (e) => { + // Tab-complete slash commands when the buffer starts with `/`. + if (e.key === 'Tab' && ta.value.startsWith('/') && !ta.value.includes(' ')) { + const next = completeSlash(ta.value); + if (next) { e.preventDefault(); ta.value = next; return; } + } if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); - if (ta.value.trim()) form.requestSubmit(); + const line = ta.value; + if (!line.trim()) return; + // Intercept slash commands locally; never send them to the agent. + if (line.trim().startsWith('/')) { + if (handleSlashCommand(line)) { + ta.value = ''; + grow(); + return; + } + } + form.requestSubmit(); } }); // Reset height after async submit clears the value. @@ -188,7 +244,7 @@ form.append( el('span', { class: 'prompt' }, 'operator@' + label + ' ▸'), ta, - el('span', { class: 'submit-hint' }, '↵ send · ⇧↵ newline'), + el('span', { class: 'submit-hint' }, '↵ send · ⇧↵ newline · /help'), ); slot.append(form); termInputRendered = true; @@ -270,6 +326,11 @@ // Backfill replays mark rows .no-anim so we don't stagger 100 fade-ins // on page load. Set via `currentNoAnim` before the row helpers fire. let currentNoAnim = false; + // Expose the panel API for slash commands (`/help`, `/clear`). + termAPI = { + row: (cls, text) => row(cls, text), + clear: () => { log.innerHTML = ''; placeholder = null; }, + }; function row(cls, text) { clearPlaceholder(); const e = document.createElement('div');