agent terminal: slash commands /help and /clear, tab-completion
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.
This commit is contained in:
parent
85e1f1a8f4
commit
8d3df656de
1 changed files with 63 additions and 2 deletions
|
|
@ -155,6 +155,47 @@
|
||||||
let lastOutputLen = -1;
|
let lastOutputLen = -1;
|
||||||
let pollTimer = null;
|
let pollTimer = null;
|
||||||
let termInputRendered = false;
|
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) {
|
function renderTermInput(label, online) {
|
||||||
const slot = $('term-input');
|
const slot = $('term-input');
|
||||||
|
|
@ -178,9 +219,24 @@
|
||||||
};
|
};
|
||||||
ta.addEventListener('input', grow);
|
ta.addEventListener('input', grow);
|
||||||
ta.addEventListener('keydown', (e) => {
|
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) {
|
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
|
||||||
e.preventDefault();
|
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.
|
// Reset height after async submit clears the value.
|
||||||
|
|
@ -188,7 +244,7 @@
|
||||||
form.append(
|
form.append(
|
||||||
el('span', { class: 'prompt' }, 'operator@' + label + ' ▸'),
|
el('span', { class: 'prompt' }, 'operator@' + label + ' ▸'),
|
||||||
ta,
|
ta,
|
||||||
el('span', { class: 'submit-hint' }, '↵ send · ⇧↵ newline'),
|
el('span', { class: 'submit-hint' }, '↵ send · ⇧↵ newline · /help'),
|
||||||
);
|
);
|
||||||
slot.append(form);
|
slot.append(form);
|
||||||
termInputRendered = true;
|
termInputRendered = true;
|
||||||
|
|
@ -270,6 +326,11 @@
|
||||||
// Backfill replays mark rows .no-anim so we don't stagger 100 fade-ins
|
// 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.
|
// on page load. Set via `currentNoAnim` before the row helpers fire.
|
||||||
let currentNoAnim = false;
|
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) {
|
function row(cls, text) {
|
||||||
clearPlaceholder();
|
clearPlaceholder();
|
||||||
const e = document.createElement('div');
|
const e = document.createElement('div');
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue