feat: replace Agent mode with independent Tools toggle

- Remove 'agent' from mode dropdown; Chat/Note/OTR remain
- Add  tools toggle button in input bar (persisted in localStorage)
  When on: routes to POST /orchestrate (Gemini tool loop); send btn → "Run"
  When off: routes to POST /chat (direct to active role); no change
- Role selector and tools toggle are now fully independent:
  active chat_role sent in orchestrate payload → used for final response
- orchestrator_engine.run() accepts response_role param; passes it to
  complete(role=...) instead of hardcoded model="claude"
- OrchestrateRequest gains chat_role field (default "chat")
- Migrate stored 'agent' mode/MRU entries to 'chat' on load

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-04-28 20:36:15 -04:00
parent 1cc7988953
commit 2b9dd53566
4 changed files with 55 additions and 23 deletions

View File

@@ -58,6 +58,7 @@ async def run(
respond_with_claude: bool = True,
gemini_api_key: str | None = None,
model_name: str | None = None,
response_role: str = "chat",
) -> OrchestratorResult:
"""
Run the full orchestration loop for a task.
@@ -176,7 +177,7 @@ async def run(
response_text, backend = await complete(
system_prompt=system_prompt,
messages=messages,
model="claude",
role=response_role,
)
else:
# Cron/background tasks: return Gemini's summary directly, no Claude call

View File

@@ -52,6 +52,7 @@ class OrchestrateRequest(BaseModel):
include_short: bool = True
user: str = "scott"
persona: str = "inara"
chat_role: str = "chat" # role used for the final response (decoupled from tool-loop model)
class OrchestrateResponse(BaseModel):
@@ -184,6 +185,7 @@ async def _run_job(job_id: str, req: OrchestrateRequest, user: str) -> None:
respond_with_claude=req.respond_with_claude,
gemini_api_key=gemini_key,
model_name=orch_model.get("model_name") if orch_model else None,
response_role=req.chat_role,
)
# Save the turn to the session store so it survives a page refresh

View File

@@ -14,6 +14,7 @@
const mode_icon_el = document.getElementById('mode-icon');
const mode_label_el = document.getElementById('mode-label');
const note_vis_btn_el = document.getElementById('note-vis-btn');
const tools_toggle_el = document.getElementById('tools-toggle');
const settings_btn_el = document.getElementById('settings-btn');
const settings_dd_el = document.getElementById('settings-dropdown');
const sessionsBackdrop = document.getElementById('sessions-backdrop');
@@ -151,22 +152,22 @@
// ── Input mode — dropdown select with MRU ordering ──────────
const MODES = {
chat: { icon: 'message-circle', label: 'Chat' },
note: { icon: 'pencil', label: 'Note' },
otr: { icon: 'lock', label: 'OTR' },
agent: { icon: 'bot', label: 'Agent' },
chat: { icon: 'message-circle', label: 'Chat' },
note: { icon: 'pencil', label: 'Note' },
otr: { icon: 'lock', label: 'OTR' },
};
const send_defs = {
chat: { icon: 'arrow-up', label: 'Send' },
note: { icon: 'pencil', label: 'Note' },
otr: { icon: 'arrow-up', label: 'Send' },
agent: { icon: 'zap', label: 'Run' },
chat: { icon: 'arrow-up', label: 'Send' },
note: { icon: 'pencil', label: 'Note' },
otr: { icon: 'arrow-up', label: 'Send' },
};
let current_mode = localStorage.getItem('current_mode') || 'chat';
if (!(current_mode in MODES)) current_mode = 'chat'; // migrate stored 'agent'
let note_public = false;
// MRU list — most recent first; used to sort dropdown options
let mode_mru = JSON.parse(localStorage.getItem('mode_mru') || '["chat","note","otr","agent"]');
let mode_mru = JSON.parse(localStorage.getItem('mode_mru') || '["chat","note","otr"]');
mode_mru = mode_mru.filter(m => m in MODES); // strip stale 'agent' entries
function push_mru(mode) {
mode_mru = [mode, ...mode_mru.filter(m => m !== mode)];
@@ -219,7 +220,7 @@
});
function update_mode_ui() {
const m = MODES[current_mode];
const m = MODES[current_mode] || MODES.chat;
const sd = send_defs[current_mode] || send_defs.chat;
// Update trigger button
@@ -235,13 +236,15 @@
note_vis_btn_el.classList.toggle('pub', note_public);
// Textarea mode classes
inputEl.classList.toggle('mode-note', current_mode === 'note');
inputEl.classList.toggle('public', current_mode === 'note' && note_public);
inputEl.classList.toggle('mode-otr', current_mode === 'otr');
inputEl.classList.toggle('mode-agent', current_mode === 'agent');
inputEl.classList.toggle('mode-note', current_mode === 'note');
inputEl.classList.toggle('public', current_mode === 'note' && note_public);
inputEl.classList.toggle('mode-otr', current_mode === 'otr');
// Send button label + icon
sendBtn.innerHTML = icon_html(sd.icon) + ' ' + sd.label;
// Send button label + icon (tools active → "Run", otherwise per-mode)
const effectiveSd = toolsEnabled && current_mode !== 'note'
? { icon: 'zap', label: 'Run' }
: sd;
sendBtn.innerHTML = icon_html(effectiveSd.icon) + ' ' + effectiveSd.label;
render_icons();
updateInputPlaceholder();
@@ -252,12 +255,14 @@
inputEl.placeholder = note_public
? 'Public note — LLM sees this next turn…'
: 'Private note — only you see this…';
} else if (current_mode === 'agent') {
inputEl.placeholder = ctrlEnterMode
? `Task for ${personaLabel}… (orchestrator — Ctrl+Enter to run)`
: `Task for ${personaLabel}… (orchestrator)`;
} else if (current_mode === 'otr') {
inputEl.placeholder = 'Off the record — not logged or distilled…';
inputEl.placeholder = toolsEnabled
? `Task for ${personaLabel}… ⚡ tools + off the record`
: 'Off the record — not logged or distilled…';
} else if (toolsEnabled) {
inputEl.placeholder = ctrlEnterMode
? `Task for ${personaLabel}… ⚡ tools (Ctrl+Enter to run)`
: `Task for ${personaLabel}… ⚡ tools`;
} else {
inputEl.placeholder = ctrlEnterMode
? `Message ${personaLabel}… (Ctrl+Enter to send)`
@@ -272,6 +277,26 @@
update_mode_ui();
});
// ── Tools toggle ─────────────────────────────────────────────
// When on: submit goes to POST /orchestrate (Gemini tool loop → active role responds).
// When off: submit goes to POST /chat (direct to active role, no tools).
let toolsEnabled = localStorage.getItem('tools-enabled') === 'true';
function updateToolsToggleUI() {
tools_toggle_el.classList.toggle('local-on', toolsEnabled);
tools_toggle_el.title = toolsEnabled
? '⚡ Tools enabled — click to disable'
: 'Tools disabled — click to enable';
update_mode_ui();
}
tools_toggle_el.addEventListener('click', (e) => {
e.stopPropagation();
toolsEnabled = !toolsEnabled;
localStorage.setItem('tools-enabled', toolsEnabled);
updateToolsToggleUI();
});
// ── Settings dropdown ─────────────────────────────────────────
settings_btn_el.addEventListener('click', (e) => {
e.stopPropagation();
@@ -1098,6 +1123,7 @@
include_long: memLong,
include_mid: memMid,
include_short: memShort,
chat_role: activeRole()?.role || 'chat',
user: CORTEX_USER,
persona: CORTEX_PERSONA,
}),
@@ -1171,7 +1197,7 @@
function dispatchSend() {
if (current_mode === 'note') addNote();
else if (current_mode === 'agent') sendOrchestrate();
else if (toolsEnabled) sendOrchestrate();
else sendMessage();
}
@@ -1636,6 +1662,7 @@
updateTierUI();
updateMemUI();
updateToolsToggleUI();
update_mode_ui();
// ── Init ─────────────────────────────────────────────────────

View File

@@ -176,6 +176,8 @@
<div id="mode-dropdown"></div>
<!-- Note visibility sub-toggle — only shown when note mode is active -->
<button id="note-vis-btn" title="Toggle note visibility (private / public)">prv</button>
<!-- Tools toggle — routes through the orchestrator tool loop when active -->
<button id="tools-toggle" title="Tools disabled — click to enable"></button>
</div>
<textarea id="input" rows="1" placeholder="Message…" autofocus></textarea>
<div id="send-col">