Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_ai_gate.js
T
DemonGatanjieuandClaude Opus 5.5 59916d1ea3 Offer an on-demand tool set: a few tools, and the directory for the rest
A third choice for "Tools for the AI" in the Gates window. With "On demand",
AI apps are offered only action_catalog.CORE plus two tools of
mcp_directory.py:
- CORE: begin_task, get_situation, run_workflow, run_status, report, ask_user,
  get_answers.
- tool_help: the description and arguments of up to three tools named in the
  directory.
- use_tool: calls any tool of the directory.

tools/list drops to 11 tools and about 9,000 characters (daily 22,200, full
40,700). With this set the directory begin_task brings names every tool and
says how to reach the ones not listed.

use_tool has no run of its own. mcp_server._call makes the call the tool it
names before anything else, so the user's gates, the timeline's step, the run
watch and the answer are all that tool's; the timeline never shows use_tool.
An unknown name, the card's own tools or wrong arguments come back as tool
errors that point to tool_help.

The default stays Full. Switching back to Daily or Full in the Gates window
undoes this without a revert, and this commit stands alone if it has to go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 16:11:41 +08:00

140 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Gates on the AI workbench (api/ai_gates.py): the card under a step that waits for the user's OK
* (what the AI wants to do; Allow, No with a reason for the AI, and "from now on, go ahead"), what
* the user answered, and the Gates window where each kind of action is set to go ahead, ask first
* or never (changing the canvas, using the GPU, downloading, going online, moving model files), whether a new
* task works in a canvas tab of its own (api/ai_task_canvas.py), which tools AI apps are offered
* (all, the daily set for a small model, or on demand: a few and the rest through a directory), and whether the AI waits for a run or the user tells it
* when the run is done (ui_ai_call.js).
*/
import { translate as t } from './locales.js';
import { anomalousAlert } from './ui_dialog.js';
import { aiWindow } from './ui_ai_connect.js';
export const GATES = ['canvas', 'gpu', 'download', 'network', 'files'];
const POLICIES = ['auto', 'ask', 'never'];
function el(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}
function button(className, label, onClick, title) {
const node = el('button', className, label);
node.type = 'button';
node.onclick = onClick;
if (title) node.title = title;
return node;
}
async function post(url, body) {
const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(data.message || data.error || `HTTP ${response.status}`);
return data;
}
/** The user's answer to a step waiting at a gate: {decision: allow | deny, text?, remember?}. */
export function decideGate(taskId, stepId, answer) {
return post(`/anomalous/ai_tasks/${encodeURIComponent(taskId)}/gate`, { step: stepId, ...answer });
}
/** What the user answered, or the card to answer with, under a step that has a gate; null otherwise. */
export function gateCard(step, taskId, changed) {
const asked = step.gate;
if (!asked || (!asked.lines && !asked.decision)) return null;
if (step.state !== 'asking') {
if (!asked.decision || asked.earlier) return null;
const words = asked.decision === 'allow'
? t(step.state === 'allowed' ? 'aiGateAllowedLater' : 'aiGateAllowed')
: t('aiGateDenied') + (asked.text ? `:${asked.text}` : '');
return el('div', `anomalous-ai-gate-answer is-${asked.decision}`, words);
}
const card = el('div', 'anomalous-ai-gate');
card.append(el('div', 'anomalous-ai-gate-title', t(`aiGateQ_${asked.gate}`)),
el('div', 'anomalous-ai-gate-why', t(`aiGateWhy_${asked.gate}`)));
if (asked.lines?.length) {
const lines = el('dl', 'anomalous-ai-gate-lines');
for (const line of asked.lines) {
const label = t(`aiGateArg_${line.label}`);
lines.append(el('dt', '', label === `aiGateArg_${line.label}` ? line.label : label), el('dd', '', line.value));
}
card.append(lines);
}
const reason = el('input', 'anomalous-ai-gate-reason');
reason.placeholder = t('aiGateReason');
reason.addEventListener('keydown', event => event.stopPropagation());
const remember = el('label', 'anomalous-ai-gate-remember');
const box = el('input');
box.type = 'checkbox';
remember.append(box, el('span', '', t('aiGateRemember', { gate: t(`aiGate_${asked.gate}`) })));
const answer = async (decision) => {
card.querySelectorAll('button').forEach((item) => { item.disabled = true; });
try {
await decideGate(taskId, step.id, { decision, text: reason.value.trim(), remember: box.checked });
} catch (error) {
await anomalousAlert(error.message);
}
changed();
};
const actions = el('div', 'anomalous-ai-gate-actions');
actions.append(button('anomalous-ai-gate-allow', t('aiGateAllow'), () => answer('allow')),
button('anomalous-ai-gate-deny', t('aiGateDeny'), () => answer('deny')), remember);
card.append(reason, actions);
if (asked.timed_out) card.append(el('div', 'anomalous-ai-gate-note', t('aiGateLate')));
return card;
}
/** A row of the Gates window: a setting, its words and its choices; `save(choice)` posts it and gives what is kept. */
function choiceRow(key, choices, current, labelOf, save) {
const row = el('div', 'anomalous-ai-gates-row');
const words = el('div', 'anomalous-ai-gates-words');
words.append(el('div', 'anomalous-ai-gates-name', t(`aiGate_${key}`)), el('div', 'anomalous-ai-gates-tools', t(`aiGateWhy_${key}`)));
const choice = el('div', 'anomalous-ai-gates-choice');
choice.setAttribute('role', 'radiogroup');
let kept = current;
const draw = () => choice.querySelectorAll('button').forEach((item, at) => item.classList.toggle('is-on', choices[at] === kept));
for (const value of choices) {
const option = button('anomalous-ai-gates-option', labelOf(value), async () => {
try {
kept = await save(value);
} catch (error) {
await anomalousAlert(error.message);
return;
}
draw();
});
option.setAttribute('role', 'radio');
choice.append(option);
}
draw();
row.append(words, choice);
return row;
}
/** The Gates window: each kind of action the AI may do (goes ahead, asks or never), where it works, which tools it gets. */
export async function openGates() {
const body = el('div', 'anomalous-ai-gates');
body.append(el('p', 'anomalous-ai-gates-intro', t('aiGatesIntro')));
let known = {};
try {
known = await (await fetch('/anomalous/ai_gates')).json();
} catch (_) { /* drawn with nothing chosen; a change still saves */ }
const save = async change => post('/anomalous/ai_gates', change);
for (const gate of GATES) {
body.append(choiceRow(gate, POLICIES, known.policies?.[gate], policy => t(`aiPolicy_${policy}`),
async policy => (await save({ gate, policy })).policies[gate]));
}
body.append(choiceRow('canvasTab', ['new', 'current'], known.canvas_tab || 'new', mode => t(`aiCanvasTab_${mode}`),
async mode => (await save({ canvas_tab: mode })).canvas_tab),
choiceRow('tools', ['lazy', 'daily', 'full'], known.tools || 'full', set => t(`aiToolSet_${set}`),
async set => (await save({ tools: set })).tools),
choiceRow('runs', ['call', 'wait'], known.runs || 'call', mode => t(`aiRunWait_${mode}`),
async mode => (await save({ runs: mode })).runs),
el('p', 'anomalous-ai-gates-note', t('aiGatesNote')));
aiWindow(t('aiGates'), body);
}