Offer to drop an assistant's code block straight into the Scheduler editor

This commit is contained in:
Gmer4Lfe
2026-08-10 19:59:28 -04:00
parent 6054f68754
commit 9c744b34f8
2 changed files with 85 additions and 3 deletions
+4 -1
View File
@@ -660,7 +660,10 @@ vv_ai_profiles_script();
body: new URLSearchParams({ action: 'clear', token }) }).catch(() => {});
finish();
save();
onTurn();
// The answer is passed so a page can react to what was said, not just that a turn
// happened — the Scheduler uses it to spot a code block worth offering to the editor.
// Handlers that take no argument are unaffected.
onTurn(j.answer);
return;
}
if (j.status === 'error') { addError(j.error || 'Unknown error'); finish(); return; }
+81 -2
View File
@@ -2437,11 +2437,14 @@ if (document.getElementById('vv-sched-ai-chat')) {
vvAiLastQ = q;
return false;
},
onTurn: () => { vvAiFixArm(); requestAnimationFrame(vvFitRight); },
onTurn: (answer) => { vvAiFixArm(); vvAiCodeArm(answer); requestAnimationFrame(vvFitRight); },
// Expanding changes how much of the panel is left for the views above it. Without this the
// chat grows downward past the end of the panel instead of upward into it.
onResize: () => requestAnimationFrame(vvFitRight),
onOffer: (kind, yes) => { if (kind === 'fix') vvAiFixAnswer(yes); },
onOffer: (kind, yes) => {
if (kind === 'fix') vvAiFixAnswer(yes);
if (kind === 'code') vvAiCodeAnswer(yes);
},
});
}
@@ -2499,6 +2502,82 @@ let vvAiFixAsk = null; // { scope, symptom } while the offer is open and awa
// Armed when a troubleshoot turn finishes. The question is the symptom; the scope is whatever was
// being diagnosed. `since` guards against a run that had already completed before the diagnosis
// started being read as proof that the diagnosis worked.
// ── AI → editor handoff ──────────────────────────────────────────────────────────────────────
// Asking the assistant for a script and then hand-selecting it out of the transcript is the one
// step in this tab that is pure friction — and a transcript selection loses leading whitespace
// the moment a line wraps, which for bash is not cosmetic. When the editor is open and an answer
// carries a real code block, offer to place it instead.
//
// It is an offer, never automatic: the editor usually holds work in progress, and a reply that
// silently rewrote it would be far worse than copy/paste.
let vvAiCodePending = null;
// Largest fenced block wins when an answer carries several — an explanation typically quotes a
// line or two before giving the whole thing, and the whole thing is what was asked for. A shebang
// beats size outright, since that is unambiguously a script rather than an excerpt.
function vvAiCodeExtract(text) {
if (!text) return '';
const blocks = [];
const re = /```[a-zA-Z0-9_+-]*\n([\s\S]*?)```/g;
let m;
while ((m = re.exec(text)) !== null) blocks.push(m[1].replace(/\s+$/, ''));
if (!blocks.length) return '';
const shebang = blocks.find(b => b.startsWith('#!'));
return shebang || blocks.sort((a, b) => b.length - a.length)[0];
}
// display:'' vs 'none' is how this panel switches views — see vvShowEditor and friends.
function vvAiEditorOpen() {
const ed = document.getElementById('vv-editor');
return !!ed && ed.style.display !== 'none';
}
function vvAiCodeArm(answer) {
vvAiCodePending = null;
if (!vvSchedChat || !vvAiEditorOpen()) return;
const code = vvAiCodeExtract(answer);
// A single short line is a mention, not a script. Offering on those turns every answer into a
// question and trains the operator to ignore the buttons.
if (!code || (code.length < 40 && code.indexOf('\n') === -1)) return;
vvAiCodePending = code;
const lines = code.split('\n').length;
vvSchedChat.offer('code', 'Insert that ' + lines + '-line block into the editor at your cursor?');
}
function vvAiCodeAnswer(yes) {
const code = vvAiCodePending;
vvAiCodePending = null;
if (!yes || !code) return;
const ta = document.getElementById('vv-editor-body');
// They may have switched views between the answer and the click. Inserting into a hidden
// textarea would look like nothing happened and be discovered much later.
if (!ta || !vvAiEditorOpen()) {
vvSchedChat.note('The editor is no longer open — nothing was inserted.');
return;
}
// Commit the pre-insert state immediately. Without the flag, the 600ms coalescing window can
// merge this with whatever was typed just before it, and one Ctrl+Z would take back both.
vvUndoCapture(true);
const s = ta.selectionStart, e = ta.selectionEnd;
ta.value = ta.value.slice(0, s) + code + ta.value.slice(e);
ta.selectionStart = ta.selectionEnd = s + code.length;
// A programmatic value change fires no input event, so the highlight overlay, the line gutter
// and the current-line marker all keep rendering the old text until these are called by hand.
vvUndoCapture(true);
vvSyncHlOverlay();
vvEditorCursorMoved();
ta.focus();
vvSchedChat.note('Inserted at the cursor. Ctrl+Z in the editor takes it back.');
}
function vvAiFixArm() {
if (vvAiProfile !== 'troubleshoot' || !vvAiScope) return;
vvAiFixWatch = {