Offer to drop an assistant's code block straight into the Scheduler editor
This commit is contained in:
@@ -660,7 +660,10 @@ vv_ai_profiles_script();
|
|||||||
body: new URLSearchParams({ action: 'clear', token }) }).catch(() => {});
|
body: new URLSearchParams({ action: 'clear', token }) }).catch(() => {});
|
||||||
finish();
|
finish();
|
||||||
save();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (j.status === 'error') { addError(j.error || 'Unknown error'); finish(); return; }
|
if (j.status === 'error') { addError(j.error || 'Unknown error'); finish(); return; }
|
||||||
|
|||||||
@@ -2437,11 +2437,14 @@ if (document.getElementById('vv-sched-ai-chat')) {
|
|||||||
vvAiLastQ = q;
|
vvAiLastQ = q;
|
||||||
return false;
|
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
|
// 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.
|
// chat grows downward past the end of the panel instead of upward into it.
|
||||||
onResize: () => requestAnimationFrame(vvFitRight),
|
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
|
// 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
|
// being diagnosed. `since` guards against a run that had already completed before the diagnosis
|
||||||
// started being read as proof that the diagnosis worked.
|
// 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() {
|
function vvAiFixArm() {
|
||||||
if (vvAiProfile !== 'troubleshoot' || !vvAiScope) return;
|
if (vvAiProfile !== 'troubleshoot' || !vvAiScope) return;
|
||||||
vvAiFixWatch = {
|
vvAiFixWatch = {
|
||||||
|
|||||||
Reference in New Issue
Block a user