feat: orchestrator Agent mode UI + claude_allow_dir tool + fix DDG search
- Add Agent mode toggle to web UI input row — routes through POST /orchestrate instead of /chat; polls for result with live tool-call count in thinking bubble - Add cortex/tools/system.py with claude_allow_dir tool; registers in tool registry - Fix web search: duckduckgo_search renamed to ddgs, update import + requirements.txt - Allow WebSearch and WebFetch in ~/.claude/settings.json for Claude CLI fallback - Add claude-allow-dir script docs and security note to CLAUDE.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,9 +9,10 @@
|
||||
const heightRow = document.getElementById('height-row');
|
||||
const heightSel = document.getElementById('height-sel');
|
||||
const enterToggle = document.getElementById('enter-toggle');
|
||||
const noteTypeBtnEl = document.getElementById('note-type-btn');
|
||||
const noteBtnEl = document.getElementById('note-btn');
|
||||
const stopBtn = document.getElementById('stop');
|
||||
const noteTypeBtnEl = document.getElementById('note-type-btn');
|
||||
const noteBtnEl = document.getElementById('note-btn');
|
||||
const agentModeBtnEl = document.getElementById('agent-mode-btn');
|
||||
const stopBtn = document.getElementById('stop');
|
||||
|
||||
let sessionId = null;
|
||||
let primaryBackend = 'claude';
|
||||
@@ -58,6 +59,23 @@
|
||||
syncHeight();
|
||||
});
|
||||
|
||||
// ── Agent mode ───────────────────────────────────────────────
|
||||
let agentMode = localStorage.getItem('agentMode') === 'true';
|
||||
|
||||
function updateAgentModeUI() {
|
||||
agentModeBtnEl.classList.toggle('active', agentMode);
|
||||
updateInputPlaceholder();
|
||||
if (agentMode) sendBtn.textContent = 'Run';
|
||||
else if (!noteMode) sendBtn.textContent = 'Send';
|
||||
}
|
||||
|
||||
agentModeBtnEl.addEventListener('click', () => {
|
||||
agentMode = !agentMode;
|
||||
localStorage.setItem('agentMode', agentMode);
|
||||
updateAgentModeUI();
|
||||
inputEl.focus();
|
||||
});
|
||||
|
||||
// ── Note mode ────────────────────────────────────────────────
|
||||
let noteMode = false;
|
||||
let notePublic = false;
|
||||
@@ -82,7 +100,7 @@
|
||||
} else {
|
||||
noteBtnEl.classList.remove('active', 'public');
|
||||
noteTypeBtnEl.style.display = 'none';
|
||||
sendBtn.textContent = 'Send';
|
||||
sendBtn.textContent = agentMode ? 'Run' : 'Send';
|
||||
inputEl.classList.remove('note-mode', 'public');
|
||||
}
|
||||
updateInputPlaceholder();
|
||||
@@ -93,6 +111,10 @@
|
||||
inputEl.placeholder = notePublic
|
||||
? 'Public note — LLM sees this next turn…'
|
||||
: 'Private note — only you see this…';
|
||||
} else if (agentMode) {
|
||||
inputEl.placeholder = ctrlEnterMode
|
||||
? 'Task for Inara… (Gemini tool loop — Ctrl+Enter to run)'
|
||||
: 'Task for Inara… (Gemini tool loop)';
|
||||
} else {
|
||||
inputEl.placeholder = ctrlEnterMode
|
||||
? 'Message Inara… (Ctrl+Enter to send)'
|
||||
@@ -617,16 +639,108 @@
|
||||
inputEl.focus();
|
||||
}
|
||||
|
||||
sendBtn.addEventListener('click', () => {
|
||||
if (noteMode) addNote(); else sendMessage();
|
||||
});
|
||||
async function sendOrchestrate() {
|
||||
const text = inputEl.value.trim();
|
||||
if (!text || activeController) return;
|
||||
|
||||
inputEl.value = '';
|
||||
syncHeight();
|
||||
sendBtn.style.display = 'none';
|
||||
stopBtn.style.display = 'block';
|
||||
headerEmoji.classList.add('processing');
|
||||
|
||||
activeController = new AbortController();
|
||||
|
||||
addMessage('user', text);
|
||||
scrollToBottom();
|
||||
|
||||
const thinkingDiv = addMessage('assistant thinking', '⚡ working…');
|
||||
|
||||
try {
|
||||
const res = await fetch('/orchestrate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
task: text,
|
||||
session_id: sessionId,
|
||||
tier: currentTier,
|
||||
include_long: memLong,
|
||||
include_mid: memMid,
|
||||
include_short: memShort,
|
||||
}),
|
||||
signal: activeController.signal,
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const { job_id } = await res.json();
|
||||
|
||||
// Poll until complete or stopped
|
||||
let job;
|
||||
while (true) {
|
||||
if (activeController.signal.aborted) throw new DOMException('Aborted', 'AbortError');
|
||||
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
if (activeController.signal.aborted) throw new DOMException('Aborted', 'AbortError');
|
||||
|
||||
const pollRes = await fetch(`/orchestrate/${job_id}`, {
|
||||
signal: activeController.signal,
|
||||
});
|
||||
if (!pollRes.ok) throw new Error(`Poll failed: HTTP ${pollRes.status}`);
|
||||
job = await pollRes.json();
|
||||
|
||||
const n = job.tool_calls?.length || 0;
|
||||
if (job.status === 'queued' || job.status === 'running') {
|
||||
thinkingDiv.textContent = n
|
||||
? `⚡ working… (${n} tool${n !== 1 ? 's' : ''} used)`
|
||||
: '⚡ working…';
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (job.status === 'error') throw new Error(job.error || 'Orchestrator failed');
|
||||
|
||||
thinkingDiv.className = 'message assistant';
|
||||
setMessageText(thinkingDiv, 'assistant', job.response || '(no response)');
|
||||
|
||||
const n = job.tool_calls?.length || 0;
|
||||
if (n) {
|
||||
const names = job.tool_calls.map(t => t.name).join(', ');
|
||||
addMessage('system', `⚡ ${n} tool call${n !== 1 ? 's' : ''}: ${names}`);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') {
|
||||
thinkingDiv.className = 'message system';
|
||||
thinkingDiv.textContent = 'Stopped.';
|
||||
} else {
|
||||
thinkingDiv.className = 'message error';
|
||||
thinkingDiv.textContent = `Error: ${err.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
activeController = null;
|
||||
headerEmoji.classList.remove('processing');
|
||||
sendBtn.style.display = 'block';
|
||||
stopBtn.style.display = 'none';
|
||||
inputEl.focus();
|
||||
}
|
||||
|
||||
function dispatchSend() {
|
||||
if (noteMode) addNote();
|
||||
else if (agentMode) sendOrchestrate();
|
||||
else sendMessage();
|
||||
}
|
||||
|
||||
sendBtn.addEventListener('click', dispatchSend);
|
||||
|
||||
inputEl.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const shouldSend = ctrlEnterMode ? (e.ctrlKey || e.metaKey) : !e.shiftKey;
|
||||
if (shouldSend) {
|
||||
e.preventDefault();
|
||||
if (noteMode) addNote(); else sendMessage();
|
||||
dispatchSend();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -923,6 +1037,7 @@
|
||||
|
||||
updateTierUI();
|
||||
updateMemUI();
|
||||
updateAgentModeUI();
|
||||
|
||||
// ── Init ─────────────────────────────────────────────────────
|
||||
updateEnterToggleUI();
|
||||
|
||||
Reference in New Issue
Block a user