Add slash commands, and stop Escape and recall from leaving a stale draft

This commit is contained in:
Gmer4Lfe
2026-08-10 21:25:04 -04:00
parent b9b2a59915
commit 775bdce6e1
+81 -1
View File
@@ -792,6 +792,69 @@ vv_ai_profiles_script();
} }
function clearDraft() { try { localStorage.removeItem(DRAFT_KEY); } catch (_) {} } function clearDraft() { try { localStorage.removeItem(DRAFT_KEY); } catch (_) {} }
// ── Slash commands ───────────────────────────────────────────────────
// Typed rather than clicked, for the panels that live on a screen with no pointer near them.
// Everything here is also reachable by mouse or Alt-key; this is a third route to the same
// controls, never the only route to any of them.
function runCommand(raw) {
const parts = raw.slice(1).trim().split(/\s+/);
const cmd = (parts.shift() || '').toLowerCase();
const arg = parts.join(' ');
const clear = () => { $('input').value = ''; clearDraft(); };
// Toggles a checkbox the page owns, if it gave us one. Reports the resulting state rather
// than the requested one, so a command against a control this profile does not have says so.
const flip = (elId, label) => {
const c = elId ? document.getElementById(elId) : null;
if (!c) { noteLine(label + ' is not available on this profile.'); return; }
c.checked = !c.checked;
noteLine(label + (c.checked ? ' on' : ' off') + ' for the next question.');
};
switch (cmd) {
case 'new': clear(); newChat(); $('input').focus(); return;
case 'saved': { clear(); const hb = $('hist-b'); if (hb) hb.click(); return; }
case 'web': clear(); flip(o.webEl, 'Web search'); return;
case 'think': clear(); flip(o.thinkEl, 'Reasoning'); return;
case 'retry': clear(); lastAction('retry'); return;
case 'edit': clear(); lastAction('edit'); return;
case 'profile': {
clear();
// Read from the rendered menu rather than a second list in JS — one source of truth for
// which profiles exist, and it cannot drift from what the picker offers.
const opts = Array.from(document.querySelectorAll(`#${P}-menu [data-prof]`))
.map(b => b.dataset.prof);
if (!arg) { noteLine('Profiles: ' + opts.join(', ')); return; }
const want = opts.find(x => x.toLowerCase() === arg.toLowerCase())
|| opts.find(x => x.toLowerCase().startsWith(arg.toLowerCase()));
if (!want) { noteLine('No profile matching "' + arg + '". Try: ' + opts.join(', ')); return; }
setProfile(want);
noteLine('Profile is now ' + want + '.');
return;
}
case 'scope': {
clear();
const s = (typeof o.scope === 'function' ? o.scope() : (o.scope || ''));
noteLine(s ? ('Scoped to ' + s) : 'Not scoped to anything in particular.');
return;
}
case 'help': case '?': {
clear();
noteLine('/new /saved /retry /edit /profile [name] /scope /web /think /help');
return;
}
}
// Unknown commands are not sent to the model. A typo'd command is a typo, and answering it
// as a question wastes 40 seconds to say it does not understand.
noteLine('No such command: /' + cmd + ' — try /help');
$('input').value = '';
clearDraft();
}
// ── Ask / poll ─────────────────────────────────────────────────────── // ── Ask / poll ───────────────────────────────────────────────────────
function send() { function send() {
// Never fail silently on a stuck flag. A turn that ends without finish() — a throw, a poll // Never fail silently on a stuck flag. A turn that ends without finish() — a throw, a poll
@@ -806,6 +869,15 @@ vv_ai_profiles_script();
const q = $('input').value.trim(); const q = $('input').value.trim();
if (!q) return; if (!q) return;
// Slash commands are handled before anything else, so one is never sent to the model as a
// question. Checked ahead of beforeSend too: a page claiming typed text wants questions,
// not "/new".
//
// The pattern is deliberately strict — a bare word of letters, then whitespace or the end.
// "/mnt/user/appdata is filling up" is a perfectly ordinary question on this machine, and a
// looser "starts with a slash" test swallowed it as a command.
if (/^\/[a-z?]+(\s|$)/i.test(q)) { runCommand(q); return; }
// A page may claim what was typed instead of asking it. The Scheduler does this once, after // A page may claim what was typed instead of asking it. The Scheduler does this once, after
// an offer is accepted, when the next thing typed is the fix being recorded rather than a // an offer is accepted, when the next thing typed is the fix being recorded rather than a
// question. Checked here rather than in a wrapper the page calls, because Ask and Ctrl+Enter // question. Checked here rather than in a wrapper the page calls, because Ask and Ctrl+Enter
@@ -1400,6 +1472,9 @@ vv_ai_profiles_script();
e.preventDefault(); e.preventDefault();
inp.value = messages[i].content; inp.value = messages[i].content;
inp.setSelectionRange(inp.value.length, inp.value.length); inp.setSelectionRange(inp.value.length, inp.value.length);
// Setting .value in script fires no input event, so the draft would still hold
// whatever was there before the recall.
saveDraft();
break; break;
} }
} }
@@ -1411,7 +1486,11 @@ vv_ai_profiles_script();
|| (histWrap && histWrap.classList.contains('open')); || (histWrap && histWrap.classList.contains('open'));
closeMenus(); closeMenus();
// Only clears once there is no menu left to close, so one Escape never does two things. // Only clears once there is no menu left to close, so one Escape never does two things.
if (!wasOpen && e.target === inp && inp.value !== '') { e.preventDefault(); inp.value = ''; } // clearDraft too, or Escape would empty the box and the text would reappear on the next
// tab swap — an Escape that does not actually discard anything.
if (!wasOpen && e.target === inp && inp.value !== '') {
e.preventDefault(); inp.value = ''; clearDraft();
}
return; return;
} }
@@ -1599,6 +1678,7 @@ function vv_ai_chat_keys(): array {
return [ return [
['keys' => ['Ctrl', 'Enter'], 'what' => 'Ask', 'id' => 'send'], ['keys' => ['Ctrl', 'Enter'], 'what' => 'Ask', 'id' => 'send'],
['keys' => ['↑'], 'what' => 'Your last question, to edit', 'id' => 'recall'], ['keys' => ['↑'], 'what' => 'Your last question, to edit', 'id' => 'recall'],
['keys' => ['/'], 'what' => 'Command — /help lists them', 'id' => 'slash'],
['keys' => ['Esc'], 'what' => 'Close a menu, or clear the box', 'id' => 'esc'], ['keys' => ['Esc'], 'what' => 'Close a menu, or clear the box', 'id' => 'esc'],
['keys' => ['Alt', 'N'], 'what' => 'New conversation', 'id' => 'new'], ['keys' => ['Alt', 'N'], 'what' => 'New conversation', 'id' => 'new'],
['keys' => ['Alt', 'E'], 'what' => 'Expand or collapse', 'id' => 'grow'], ['keys' => ['Alt', 'E'], 'what' => 'Expand or collapse', 'id' => 'grow'],