Last active
June 17, 2026 15:01
-
-
Save artydev/e5271161177c266e07c05514db72ac68 to your computer and use it in GitHub Desktop.
OLLAMA TERMINAL - WTH - MCP SERVER
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { get, set, del } from 'https://cdn.jsdelivr.net/npm/idb-keyval@6/+esm'; | |
| /* ────────────────────────────────────────────────────────────── | |
| DOM REFS | |
| ────────────────────────────────────────────────────────────── */ | |
| const log = document.getElementById("log"); | |
| const input = document.getElementById("input"); | |
| const sessionBar = document.getElementById("sessionBar"); | |
| const terminalPrompt = document.getElementById("terminalPrompt"); | |
| const mcpDot = document.getElementById("mcpDot"); | |
| const mcpStatusEl = document.getElementById("mcpStatus"); | |
| const statusModelEl = document.getElementById("statusModel"); | |
| const toolCountEl = document.getElementById("toolCount"); | |
| const msgCountEl = document.getElementById("msgCount"); | |
| const stopBtn = document.getElementById("stopBtn"); | |
| /* ────────────────────────────────────────────────────────────── | |
| CONSTANTS & ENDPOINTS | |
| ────────────────────────────────────────────────────────────── */ | |
| const OLLAMA_URL = "http://localhost:11434/api/chat"; | |
| const ALBERT_URL = "http://localhost:3000/proxy/albert"; | |
| const MCP_SERVER_URL = "http://localhost:3000"; | |
| const REGISTRY_KEY = "multiplexer_registry_config"; | |
| const SESSION_PREFIX = "multiplexer_session_"; | |
| const AVAILABLE_MODELS = [ | |
| "gemma4:e4b", | |
| "gpt-oss:120b-cloud", | |
| "AgentPublic/llama3-instruct-8b", | |
| "albert-small", | |
| "albert-large" | |
| ]; | |
| const SYSTEM_PROMPT = { | |
| role: "system", | |
| content: "You are a helpful local terminal intelligence agent. You have real-time access to advanced local host platform systems tools. Invoke them whenever required to answer questions accurately." | |
| }; | |
| /* ────────────────────────────────────────────────────────────── | |
| STATE | |
| ────────────────────────────────────────────────────────────── */ | |
| let registry = { currentActiveId: null, activeModel: "gemma4:e4b", list: [] }; | |
| let activeMessages = [SYSTEM_PROMPT]; | |
| let fullPersistentHistory = []; | |
| let RUNTIME_AVAILABLE_TOOLS = []; | |
| let ALBERT_API_KEY = ""; | |
| const CMD_HISTORY_MAX = 50; | |
| let cmdHistory = []; | |
| let historyIndex = -1; | |
| let historyDraft = ""; | |
| let currentAbortController = null; | |
| // Smart Auto-Scroll State Tracking Indicator | |
| let shouldAutoScroll = true; | |
| function setGenerating(on) { | |
| if (on) { | |
| stopBtn.classList.add("visible"); | |
| input.disabled = true; | |
| input.placeholder = "generating… (Esc to stop)"; | |
| } else { | |
| stopBtn.classList.remove("visible"); | |
| input.disabled = false; | |
| input.placeholder = "enter a prompt or /help…"; | |
| input.focus(); | |
| currentAbortController = null; | |
| } | |
| } | |
| /* ────────────────────────────────────────────────────────────── | |
| SCROLL POSITION DETECTOR | |
| ────────────────────────────────────────────────────────────── */ | |
| log.addEventListener("scroll", () => { | |
| // A threshold buffer window of 15px handles elastic scroll bouncing on mobile/trackpads | |
| const threshold = 15; | |
| const isAtBottom = log.scrollHeight - log.scrollTop - log.clientHeight <= threshold; | |
| if (isAtBottom) { | |
| shouldAutoScroll = true; | |
| } else { | |
| // User explicitly scrolled away from the floor margin threshold | |
| shouldAutoScroll = false; | |
| } | |
| }); | |
| function executeAutoScroll() { | |
| if (shouldAutoScroll) { | |
| log.scrollTop = log.scrollHeight; | |
| } | |
| } | |
| /* ────────────────────────────────────────────────────────────── | |
| STATUS BAR HELPERS | |
| ────────────────────────────────────────────────────────────── */ | |
| function setMcpStatus(online, toolCount = 0) { | |
| if (online) { | |
| mcpDot.className = "status-dot online"; | |
| mcpStatusEl.className = ""; | |
| mcpStatusEl.style.color = "var(--green)"; | |
| mcpStatusEl.textContent = "MCP online"; | |
| } else { | |
| mcpDot.className = "status-dot offline"; | |
| mcpStatusEl.className = "offline"; | |
| mcpStatusEl.style.color = "var(--red)"; | |
| mcpStatusEl.textContent = "MCP offline"; | |
| } | |
| toolCountEl.textContent = `${toolCount} tool${toolCount !== 1 ? 's' : ''}`; | |
| } | |
| function updateStatusBar() { | |
| statusModelEl.textContent = registry.activeModel || "—"; | |
| const userMsgs = activeMessages.filter(m => m.role === "user").length; | |
| msgCountEl.textContent = `${userMsgs} msg${userMsgs !== 1 ? 's' : ''}`; | |
| } | |
| /* ────────────────────────────────────────────────────────────── | |
| MCP CLIENT | |
| ────────────────────────────────────────────────────────────── */ | |
| async function fetchToolsFromMcpServer() { | |
| try { | |
| print(`connecting to MCP gateway at ${MCP_SERVER_URL}…`, "sys"); | |
| const response = await fetch(MCP_SERVER_URL, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ jsonrpc: "2.0", method: "tools/list", params: {}, id: 1 }) | |
| }); | |
| if (!response.ok) throw new Error(`HTTP ${response.status}`); | |
| const data = await response.json(); | |
| if (data.result?.tools) { | |
| RUNTIME_AVAILABLE_TOOLS = data.result.tools.map(t => ({ | |
| type: "function", | |
| function: { | |
| name: t.name, | |
| description: t.description, | |
| parameters: { | |
| type: t.inputSchema?.type || "object", | |
| properties: t.inputSchema?.properties || {}, | |
| required: t.inputSchema?.required || [] | |
| } | |
| } | |
| })); | |
| setMcpStatus(true, RUNTIME_AVAILABLE_TOOLS.length); | |
| print(`🟢 MCP connected — ${RUNTIME_AVAILABLE_TOOLS.length} tools loaded`, "sys"); | |
| } | |
| } catch (err) { | |
| setMcpStatus(false, 0); | |
| print(`🔴 MCP offline (${err.message}) — text-only mode`, "sys"); | |
| RUNTIME_AVAILABLE_TOOLS = []; | |
| } | |
| } | |
| async function executeMcpToolCall(name, args, signal = null) { | |
| try { | |
| const response = await fetch(MCP_SERVER_URL, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ jsonrpc: "2.0", method: "tools/call", params: { name, arguments: args }, id: Date.now() }), | |
| signal | |
| }); | |
| if (!response.ok) { | |
| const errData = await response.json().catch(() => ({})); | |
| throw new Error(errData.error?.message || `HTTP ${response.status}`); | |
| } | |
| const rpcResult = await response.json(); | |
| if (rpcResult.error) throw new Error(rpcResult.error.message); | |
| const content = rpcResult.result; | |
| if (content?.content && Array.isArray(content.content)) { | |
| return content.content.filter(c => c.type === "text").map(c => c.text).join("\n") || "No output."; | |
| } | |
| return JSON.stringify(content); | |
| } catch (err) { | |
| if (err.name === "AbortError") return "Tool error: Execution aborted by user."; | |
| return `Tool error: ${err.message}`; | |
| } | |
| } | |
| /* ────────────────────────────────────────────────────────────── | |
| CONTEXT PRUNING | |
| ────────────────────────────────────────────────────────────── */ | |
| function pruneActiveMemory() { | |
| const sys = activeMessages[0]; | |
| let hist = activeMessages.slice(1); | |
| while (hist.length > 16) hist.shift(); | |
| while (hist.length > 0 && (hist[0].role === "tool" || hist[0].content === "")) hist.shift(); | |
| activeMessages = [sys, ...hist]; | |
| updateStatusBar(); | |
| } | |
| /* ────────────────────────────────────────────────────────────── | |
| PERSISTENCE | |
| ────────────────────────────────────────────────────────────── */ | |
| async function saveAllToBrowser() { | |
| try { | |
| await set(REGISTRY_KEY, registry); | |
| if (registry.currentActiveId) await set(SESSION_PREFIX + registry.currentActiveId, fullPersistentHistory); | |
| } catch (err) { console.error("Storage write error:", err); } | |
| } | |
| async function loadSessionData(sessionId) { | |
| try { | |
| const saved = await get(SESSION_PREFIX + sessionId); | |
| fullPersistentHistory = saved || []; | |
| activeMessages = [SYSTEM_PROMPT, ...fullPersistentHistory.filter(m => !m.isToolActivityLog)]; | |
| pruneActiveMemory(); | |
| renderTerminalScreen(); | |
| renderTopMultiplexerBar(); | |
| } catch (err) { print("DB error: " + err.message, "err"); } | |
| } | |
| /* ────────────────────────────────────────────────────────────── | |
| RENDERING | |
| ────────────────────────────────────────────────────────────── */ | |
| function renderTopMultiplexerBar() { | |
| sessionBar.innerHTML = ""; | |
| registry.list.forEach((session, index) => { | |
| const wrapper = document.createElement("div"); | |
| wrapper.className = "session-wrapper"; | |
| const tab = document.createElement("span"); | |
| const isActive = session.id === registry.currentActiveId; | |
| tab.className = `session-tab ${isActive ? 'active' : ''}`; | |
| tab.textContent = `${index}: ${session.name}`; | |
| tab.addEventListener("click", () => { if (!isActive) switchSession(index); }); | |
| tab.addEventListener("dblclick", (e) => { | |
| e.stopPropagation(); | |
| const editorInput = document.createElement("input"); | |
| editorInput.type = "text"; | |
| editorInput.className = "rename-input"; | |
| editorInput.value = session.name; | |
| const finishRename = async () => { | |
| const fresh = editorInput.value.trim(); | |
| if (fresh && fresh !== session.name) { session.name = fresh; await saveAllToBrowser(); renderTerminalScreen(); } | |
| renderTopMultiplexerBar(); | |
| }; | |
| editorInput.addEventListener("keydown", (ke) => { | |
| if (ke.key === "Enter") finishRename(); | |
| if (ke.key === "Escape") renderTopMultiplexerBar(); | |
| }); | |
| editorInput.addEventListener("blur", finishRename); | |
| wrapper.replaceChild(editorInput, tab); | |
| editorInput.focus(); editorInput.select(); | |
| }); | |
| wrapper.appendChild(tab); | |
| const deleteBtn = document.createElement("span"); | |
| deleteBtn.className = "delete-btn"; | |
| deleteBtn.textContent = "✕"; | |
| deleteBtn.title = "Delete session"; | |
| deleteBtn.addEventListener("click", (e) => { e.stopPropagation(); handleDeleteSessionConfirmation(index); }); | |
| wrapper.appendChild(deleteBtn); | |
| sessionBar.appendChild(wrapper); | |
| }); | |
| const actions = document.createElement("div"); | |
| actions.className = "bar-actions-group"; | |
| const modelWrap = document.createElement("div"); | |
| modelWrap.className = "model-selector-wrapper"; | |
| modelWrap.innerHTML = `<span>⚙ model:</span>`; | |
| const sel = document.createElement("select"); | |
| sel.className = "model-select-native"; | |
| const models = [...new Set([registry.activeModel, ...AVAILABLE_MODELS])]; | |
| models.forEach(m => { | |
| const opt = document.createElement("option"); | |
| opt.value = m; | |
| const isAlbert = isAlbertModel(m); | |
| opt.textContent = isAlbert ? `🇫🇷 Albert — ${m}` : `🐋 Ollama — ${m}`; | |
| if (m === registry.activeModel) opt.selected = true; | |
| sel.appendChild(opt); | |
| }); | |
| sel.addEventListener("change", e => switchActiveModel(e.target.value)); | |
| modelWrap.appendChild(sel); | |
| actions.appendChild(modelWrap); | |
| const newBtn = document.createElement("span"); | |
| newBtn.className = "bar-btn new-session-btn"; | |
| newBtn.textContent = "+ New"; | |
| newBtn.title = "Create new session"; | |
| newBtn.addEventListener("click", () => handleCreateSession()); | |
| actions.appendChild(newBtn); | |
| const wipeBtn = document.createElement("span"); | |
| wipeBtn.className = "bar-btn wipe-workspace-btn"; | |
| wipeBtn.textContent = "Wipe All"; | |
| wipeBtn.title = "Erase all sessions"; | |
| wipeBtn.addEventListener("click", () => handleWipeAllSessionsConfirmation()); | |
| actions.appendChild(wipeBtn); | |
| sessionBar.appendChild(actions); | |
| updateStatusBar(); | |
| } | |
| function renderTerminalScreen() { | |
| log.innerHTML = ""; | |
| const cur = registry.list.find(s => s.id === registry.currentActiveId); | |
| const name = cur ? cur.name : "none"; | |
| const promptEngine = isAlbertModel(registry.activeModel) ? "albert-proxy" : "ollama"; | |
| terminalPrompt.textContent = `user@${promptEngine}:[${name}]~$`; | |
| if (!cur) { | |
| printBanner(); | |
| print("No active session. Click '+ New' or type /new to begin.", "sys"); | |
| return; | |
| } | |
| if (fullPersistentHistory.length === 0) { | |
| printBanner(); | |
| print(`Session [${name}] ready — model: ${registry.activeModel}`, "sys"); | |
| print("Type /help for available commands.", "sys"); | |
| return; | |
| } | |
| let i = 0; | |
| while (i < fullPersistentHistory.length) { | |
| const msg = fullPersistentHistory[i]; | |
| if (msg.role === "user") { | |
| print(`user@${promptEngine}:[${name}]~$ ${msg.content}`, "user"); | |
| i++; continue; | |
| } | |
| if (msg.role === "assistant" && !msg.isInternalToolCall && msg.content?.trim()) { | |
| print(`assistant: ${msg.content}`, "ai"); | |
| i++; continue; | |
| } | |
| if (msg.isToolActivityLog && msg.content?.startsWith("🔧")) { | |
| const execMsg = msg.content; | |
| const resultMsg = fullPersistentHistory[i + 1]; | |
| const tName = execMsg.replace("🔧 [MCP] executing → ", "").trim(); | |
| const outcome = resultMsg?.content?.replace(/^📦 \[MCP\] result: /, "") ?? ""; | |
| const isError = outcome.startsWith("Tool error:"); | |
| const block = document.createElement("div"); | |
| block.className = "tool-block" + (isError ? " tool-error" : ""); | |
| const header = document.createElement("div"); | |
| header.className = "tool-block-header"; | |
| header.innerHTML = ` | |
| <span class="tool-chevron">▶</span> | |
| <span class="tool-block-label">🔧 ${tName}</span> | |
| <span class="tool-block-badge">${isError ? "error" : "done"}</span>`; | |
| block.appendChild(header); | |
| const body = document.createElement("div"); | |
| body.className = "tool-block-body"; | |
| body.innerHTML = ` | |
| <div class="tool-block-section"> | |
| <div class="tool-block-section-label">result</div> | |
| <div class="tool-block-code result-text"></div> | |
| </div>`; | |
| body.querySelector(".result-text").textContent = outcome; | |
| block.appendChild(body); | |
| header.addEventListener("click", () => block.classList.toggle("open")); | |
| if (isError) block.classList.add("open"); | |
| log.appendChild(block); | |
| i += 2; | |
| if (fullPersistentHistory[i]?.isInternalToolCall) i++; | |
| continue; | |
| } | |
| i++; | |
| } | |
| // Guarantee view snaps on full context history initialization loads | |
| log.scrollTop = log.scrollHeight; | |
| } | |
| function printBanner() { | |
| const lines = [ | |
| "╔══════════════════════════════════════════╗", | |
| "║ 🧛 MULTIPLEXER TERMINAL CORE ║", | |
| "║ MCP local tools & Albert Proxy ║", | |
| "╚══════════════════════════════════════════╝", | |
| ]; | |
| lines.forEach(l => print(l, "banner-line")); | |
| const hr = document.createElement("hr"); | |
| hr.className = "log-divider"; | |
| log.appendChild(hr); | |
| } | |
| function print(text, cls = "sys") { | |
| const div = document.createElement("div"); | |
| div.className = "line " + cls; | |
| div.textContent = text; | |
| log.appendChild(div); | |
| executeAutoScroll(); | |
| return div; | |
| } | |
| /* ────────────────────────────────────────────────────────────── | |
| MULTIPLEXER OPERATIONS | |
| ────────────────────────────────────────────────────────────── */ | |
| async function switchActiveModel(name) { | |
| if (!name?.trim()) return; | |
| registry.activeModel = name.trim(); | |
| if (isAlbertModel(registry.activeModel) && !ALBERT_API_KEY) { | |
| verifyAlbertAuth(); | |
| } | |
| await saveAllToBrowser(); | |
| updateStatusBar(); | |
| print(`⚙ Model switched → ${registry.activeModel}`, "sys"); | |
| renderTopMultiplexerBar(); | |
| renderTerminalScreen(); | |
| } | |
| function isAlbertModel(modelName) { | |
| return modelName.startsWith("AgentPublic/") || modelName.startsWith("albert-"); | |
| } | |
| function verifyAlbertAuth() { | |
| if (!ALBERT_API_KEY) { | |
| const key = prompt("🔒 Enter your ALBERT_API_KEY (from albert.api.etalab.gouv.fr):"); | |
| if (key?.trim()) { | |
| ALBERT_API_KEY = key.trim(); | |
| print("🔑 Albert API verification token accepted in temporary memory context.", "sys"); | |
| } else { | |
| print("⚠ Warning: No Albert API key supplied. Proxy requests will drop.", "err"); | |
| } | |
| } | |
| } | |
| async function handleCreateSession(customName = null) { | |
| if (registry.currentActiveId) { | |
| try { | |
| await set(SESSION_PREFIX + registry.currentActiveId, fullPersistentHistory); | |
| } catch (e) { console.error("Failed to save current session before switching:", e); } | |
| } | |
| const id = "s_" + Date.now(); | |
| const name = customName?.trim() || `session-${registry.list.length}`; | |
| registry.list.push({ id, name }); | |
| registry.currentActiveId = id; | |
| fullPersistentHistory = []; | |
| activeMessages = [SYSTEM_PROMPT]; | |
| await saveAllToBrowser(); | |
| renderTopMultiplexerBar(); | |
| renderTerminalScreen(); | |
| } | |
| async function switchSession(index) { | |
| const t = registry.list[index]; | |
| if (!t || t.id === registry.currentActiveId) return; | |
| if (registry.currentActiveId) { | |
| try { | |
| await set(SESSION_PREFIX + registry.currentActiveId, fullPersistentHistory); | |
| } catch (e) { console.error("Failed to save session before switch:", e); } | |
| } | |
| registry.currentActiveId = t.id; | |
| await set(REGISTRY_KEY, registry); | |
| await loadSessionData(t.id); | |
| } | |
| async function handleDeleteSessionConfirmation(index) { | |
| const s = registry.list[index]; | |
| if (!s) return; | |
| if (confirm(`Delete session "${s.name}"?`)) { | |
| await del(SESSION_PREFIX + s.id); | |
| registry.list.splice(index, 1); | |
| if (registry.currentActiveId === s.id) { | |
| if (registry.list.length > 0) { | |
| registry.currentActiveId = registry.list[Math.max(0, index - 1)].id; | |
| await set(REGISTRY_KEY, registry); | |
| await loadSessionData(registry.currentActiveId); | |
| } else { | |
| registry.currentActiveId = null; | |
| fullPersistentHistory = []; | |
| activeMessages = [SYSTEM_PROMPT]; | |
| await set(REGISTRY_KEY, registry); | |
| renderTopMultiplexerBar(); | |
| renderTerminalScreen(); | |
| } | |
| } else { | |
| await set(REGISTRY_KEY, registry); | |
| renderTopMultiplexerBar(); | |
| } | |
| } | |
| } | |
| async function handleWipeAllSessionsConfirmation() { | |
| if (prompt("Type WIPE to erase all sessions:") === "WIPE") { | |
| for (const s of registry.list) await del(SESSION_PREFIX + s.id); | |
| await del(REGISTRY_KEY); | |
| registry.list = []; | |
| registry.currentActiveId = null; | |
| fullPersistentHistory = []; | |
| activeMessages = [SYSTEM_PROMPT]; | |
| await handleCreateSession("general"); | |
| } | |
| } | |
| async function handleSlashCommand(raw) { | |
| const parts = raw.trim().split(" "); | |
| const cmd = parts[0].toLowerCase(); | |
| const args = parts.slice(1).join(" "); | |
| if (cmd === "/new") { | |
| await handleCreateSession(args || null); | |
| } else if (cmd === "/clear") { | |
| log.innerHTML = ""; | |
| printBanner(); | |
| } else if (cmd === "/key") { | |
| ALBERT_API_KEY = args.trim(); | |
| print("🔑 ALBERT_API_KEY updated successfully.", "sys"); | |
| } else if (cmd === "/help") { | |
| const cmds = [ | |
| [" /new [name]", "create a new session tab"], | |
| [" /clear", "clear the terminal output"], | |
| [" /key [token]", "set/update your Albert API Key context value"], | |
| [" /help", "show this help message"], | |
| ]; | |
| print("── commands ──────────────────────────────────", "sys"); | |
| cmds.forEach(([c, d]) => print(`${c.padEnd(18)} — ${d}`, "sys")); | |
| print("──────────────────────────────────────────────", "sys"); | |
| } else { | |
| print(`unknown command: ${cmd} (try /help)`, "err"); | |
| } | |
| } | |
| /* ────────────────────────────────────────────────────────────── | |
| STREAM / AGENTIC CORE (MULTIPLEXED ROUTING) | |
| ────────────────────────────────────────────────────────────── */ | |
| async function callModel() { | |
| currentAbortController = new AbortController(); | |
| const targetAlbert = isAlbertModel(registry.activeModel); | |
| const url = targetAlbert ? ALBERT_URL : OLLAMA_URL; | |
| const headers = { "Content-Type": "application/json" }; | |
| if (targetAlbert) { | |
| if (!ALBERT_API_KEY) verifyAlbertAuth(); | |
| headers["Authorization"] = `Bearer ${ALBERT_API_KEY}`; | |
| } | |
| const payload = { | |
| model: registry.activeModel, | |
| stream: true, | |
| messages: activeMessages.map(({role, content, tool_calls}) => ({role, content, ...(tool_calls && {tool_calls})})) | |
| }; | |
| if (RUNTIME_AVAILABLE_TOOLS.length > 0) payload.tools = RUNTIME_AVAILABLE_TOOLS; | |
| return await fetch(url, { | |
| method: "POST", | |
| headers, | |
| body: JSON.stringify(payload), | |
| signal: currentAbortController.signal | |
| }); | |
| } | |
| async function askAI(promptText) { | |
| const cur = registry.list.find(s => s.id === registry.currentActiveId); | |
| const label = cur ? cur.name : "~"; | |
| const promptEngine = isAlbertModel(registry.activeModel) ? "albert-proxy" : "ollama"; | |
| // New prompt input forcing snap to floor ensures tracking alignment triggers properly | |
| shouldAutoScroll = true; | |
| print(`user@${promptEngine}:[${label}]~$ ${promptText}`, "user"); | |
| const userMsg = { role: "user", content: promptText }; | |
| activeMessages.push(userMsg); | |
| fullPersistentHistory.push(userMsg); | |
| updateStatusBar(); | |
| setGenerating(true); | |
| let guard = 6; | |
| let isProcessing = true; | |
| let aborted = false; | |
| const targetAlbert = isAlbertModel(registry.activeModel); | |
| try { | |
| while (isProcessing && guard-- > 0) { | |
| const thinkLine = document.createElement("div"); | |
| thinkLine.className = "line sys"; | |
| thinkLine.innerHTML = `<span style="color:var(--purple)">assistant</span> is thinking<div class="thinking-dots"><span></span><span></span><span></span></div>`; | |
| log.appendChild(thinkLine); | |
| executeAutoScroll(); | |
| let stream; | |
| try { | |
| stream = await callModel(); | |
| } catch (fetchErr) { | |
| thinkLine.remove(); | |
| if (fetchErr.name === "AbortError") { aborted = true; break; } | |
| throw fetchErr; | |
| } | |
| const reader = stream.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let displayEl = null; | |
| let textNode = null; | |
| let buf = ""; | |
| let fullText = ""; | |
| let rawToolCallsMap = {}; | |
| let thinkRemoved = false; | |
| const removeThink = () => { if (!thinkRemoved) { thinkLine.remove(); thinkRemoved = true; } }; | |
| try { | |
| while (true) { | |
| const { value, done } = await reader.read(); | |
| buf += decoder.decode(value || new Uint8Array(), { stream: !done }); | |
| const lines = buf.split("\n"); | |
| buf = lines.pop(); | |
| for (const line of lines) { | |
| let processedLine = line.trim(); | |
| if (!processedLine) continue; | |
| if (targetAlbert && processedLine.startsWith("data: ")) { | |
| processedLine = processedLine.replace(/^data:\s*/, ""); | |
| } | |
| if (processedLine === "[DONE]") continue; | |
| try { | |
| const json = JSON.parse(processedLine); | |
| const choice = targetAlbert ? json.choices?.[0] : json; | |
| const msg = targetAlbert ? choice?.delta : choice?.message; | |
| if (msg?.tool_calls?.length) { | |
| removeThink(); | |
| msg.tool_calls.forEach(tc => { | |
| const idx = tc.index ?? 0; | |
| // FIX 1: Initialize accumulator dynamically based on incoming type | |
| if (!rawToolCallsMap[idx]) { | |
| const initialArgsType = typeof tc.function?.arguments === "object" ? {} : ""; | |
| rawToolCallsMap[idx] = { | |
| id: tc.id || "", | |
| type: "function", | |
| function: { name: "", arguments: initialArgsType } | |
| }; | |
| } | |
| if (tc.id) rawToolCallsMap[idx].id = tc.id; | |
| if (tc.function?.name) rawToolCallsMap[idx].function.name += tc.function.name; | |
| // FIX 2: Prevent [object Object] concatenation strings | |
| if (tc.function?.arguments) { | |
| const incomingArgs = tc.function.arguments; | |
| if (typeof incomingArgs === "object" && incomingArgs !== null) { | |
| // Ollama format: Merge properties natively | |
| if (typeof rawToolCallsMap[idx].function.arguments !== "object") { | |
| rawToolCallsMap[idx].function.arguments = {}; | |
| } | |
| rawToolCallsMap[idx].function.arguments = { | |
| ...rawToolCallsMap[idx].function.arguments, | |
| ...incomingArgs | |
| }; | |
| } else { | |
| // OpenAI/Albert Proxy format: Append string text chunks | |
| if (typeof rawToolCallsMap[idx].function.arguments !== "string") { | |
| rawToolCallsMap[idx].function.arguments = ""; | |
| } | |
| rawToolCallsMap[idx].function.arguments += incomingArgs; | |
| } | |
| } | |
| }); | |
| } else if (msg?.content) { | |
| removeThink(); | |
| fullText += msg.content; | |
| if (!displayEl) { | |
| displayEl = document.createElement("div"); | |
| displayEl.className = "line ai"; | |
| displayEl.innerHTML = `<span>assistant: </span>`; | |
| textNode = document.createTextNode(""); | |
| displayEl.appendChild(textNode); | |
| log.appendChild(displayEl); | |
| } | |
| textNode.nodeValue += msg.content; | |
| executeAutoScroll(); | |
| } | |
| } catch { | |
| // Safe evaluation pass | |
| } | |
| } | |
| if (done) break; | |
| } | |
| } catch (readErr) { | |
| removeThink(); | |
| if (readErr.name === "AbortError") { aborted = true; break; } | |
| throw readErr; | |
| } | |
| removeThink(); | |
| if (aborted) break; | |
| const toolCalls = Object.values(rawToolCallsMap); | |
| if (toolCalls.length > 0) { | |
| if (displayEl) displayEl.remove(); | |
| displayEl = null; | |
| fullText = ""; | |
| activeMessages.push({ role: "assistant", content: null, tool_calls: toolCalls }); | |
| for (const tc of toolCalls) { | |
| const tName = tc.function.name; | |
| // FIX 3: Safe adaptive evaluation pass (Handles object context vs payload strings) | |
| let tArgs = {}; | |
| try { | |
| tArgs = typeof tc.function.arguments === "string" | |
| ? JSON.parse(tc.function.arguments) | |
| : (tc.function.arguments || {}); | |
| } catch (pe) { | |
| console.error("Args evaluation error:", pe); | |
| tArgs = {}; | |
| } | |
| const block = document.createElement("div"); | |
| block.className = "tool-block"; | |
| const header = document.createElement("div"); | |
| header.className = "tool-block-header"; | |
| header.innerHTML = ` | |
| <span class="tool-chevron">▶</span> | |
| <span class="tool-block-label">🔧 ${tName}</span> | |
| <span class="tool-block-badge">executing…</span>`; | |
| block.appendChild(header); | |
| const body = document.createElement("div"); | |
| body.className = "tool-block-body"; | |
| const argsSection = document.createElement("div"); | |
| argsSection.className = "tool-block-section"; | |
| argsSection.innerHTML = `<div class="tool-block-section-label">arguments</div> | |
| <div class="tool-block-code"></div>`; | |
| argsSection.querySelector(".tool-block-code").textContent = JSON.stringify(tArgs, null, 2); | |
| body.appendChild(argsSection); | |
| const resultSection = document.createElement("div"); | |
| resultSection.className = "tool-block-section"; | |
| resultSection.innerHTML = `<div class="tool-block-section-label">result</div> | |
| <div class="tool-block-code result-text">running…</div>`; | |
| body.appendChild(resultSection); | |
| block.appendChild(body); | |
| log.appendChild(block); | |
| executeAutoScroll(); | |
| header.addEventListener("click", () => { | |
| block.classList.toggle("open"); | |
| executeAutoScroll(); | |
| }); | |
| const logMsg = `🔧 [MCP] executing → ${tName}`; | |
| fullPersistentHistory.push({ role: "system", content: logMsg, isToolActivityLog: true }); | |
| fullPersistentHistory.push({ role: "assistant", content: JSON.stringify(tc), isInternalToolCall: true }); | |
| const outcome = await executeMcpToolCall(tName, tArgs, currentAbortController?.signal); | |
| const isError = outcome.startsWith("Tool error:"); | |
| const resultCode = resultSection.querySelector(".result-text"); | |
| resultCode.textContent = outcome; | |
| const badge = header.querySelector(".tool-block-badge"); | |
| badge.textContent = isError ? "error" : "done"; | |
| if (isError) { | |
| block.classList.add("tool-error"); | |
| block.classList.add("open"); | |
| } | |
| const resultMsg = `📦 [MCP] result: ${outcome.substring(0, 400)}${outcome.length > 400 ? '…' : ''}`; | |
| fullPersistentHistory.push({ role: "system", content: resultMsg, isToolActivityLog: true }); | |
| activeMessages.push({ role: "tool", name: tName, tool_call_id: tc.id, content: outcome }); | |
| executeAutoScroll(); | |
| } | |
| } else { | |
| isProcessing = false; | |
| const finalMsg = { role: "assistant", content: fullText }; | |
| activeMessages.push(finalMsg); | |
| if (fullText.trim()) { | |
| fullPersistentHistory.push(finalMsg); | |
| } | |
| } | |
| } | |
| if (aborted) { | |
| print("⚠ Generation stopped by user.", "sys"); | |
| } | |
| await saveAllToBrowser(); | |
| pruneActiveMemory(); | |
| } catch (err) { | |
| print(`runtime error: ${err.message}`, "err"); | |
| } finally { | |
| setGenerating(false); | |
| } | |
| } | |
| /* ────────────────────────────────────────────────────────────── | |
| STOP BUTTON | |
| ────────────────────────────────────────────────────────────── */ | |
| stopBtn.addEventListener("click", () => { | |
| if (currentAbortController) currentAbortController.abort(); | |
| }); | |
| /* ────────────────────────────────────────────────────────────── | |
| INPUT HANDLER | |
| ────────────────────────────────────────────────────────────── */ | |
| input.addEventListener("keydown", async (e) => { | |
| if (e.key === "Escape") { | |
| if (currentAbortController) currentAbortController.abort(); | |
| return; | |
| } | |
| if (e.key === "ArrowUp") { | |
| e.preventDefault(); | |
| if (cmdHistory.length === 0) return; | |
| if (historyIndex === -1) { | |
| historyDraft = input.value; | |
| historyIndex = cmdHistory.length - 1; | |
| } else if (historyIndex > 0) { | |
| historyIndex--; | |
| } | |
| input.value = cmdHistory[historyIndex]; | |
| requestAnimationFrame(() => { input.selectionStart = input.selectionEnd = input.value.length; }); | |
| return; | |
| } | |
| if (e.key === "ArrowDown") { | |
| e.preventDefault(); | |
| if (historyIndex === -1) return; | |
| if (historyIndex < cmdHistory.length - 1) { | |
| historyIndex++; | |
| input.value = cmdHistory[historyIndex]; | |
| } else { | |
| historyIndex = -1; | |
| input.value = historyDraft; | |
| } | |
| requestAnimationFrame(() => { input.selectionStart = input.selectionEnd = input.value.length; }); | |
| return; | |
| } | |
| if (e.key !== "Enter") { | |
| if (historyIndex !== -1) historyIndex = -1; | |
| return; | |
| } | |
| const value = input.value.trim(); | |
| if (!value) return; | |
| input.value = ""; | |
| historyIndex = -1; | |
| historyDraft = ""; | |
| if (cmdHistory[cmdHistory.length - 1] !== value) { | |
| cmdHistory.push(value); | |
| if (cmdHistory.length > CMD_HISTORY_MAX) cmdHistory.shift(); | |
| } | |
| if (value.startsWith("/")) { | |
| shouldAutoScroll = true; | |
| print(`client:[cmd]~$ ${value}`, "user"); | |
| await handleSlashCommand(value); | |
| } else { | |
| if (!registry.currentActiveId) { | |
| shouldAutoScroll = true; | |
| print("No active session. Type /new to start one.", "err"); | |
| return; | |
| } | |
| await askAI(value); | |
| } | |
| }); | |
| /* ────────────────────────────────────────────────────────────── | |
| BOOT | |
| ────────────────────────────────────────────────────────────── */ | |
| async function boot() { | |
| try { | |
| const saved = await get(REGISTRY_KEY); | |
| if (saved?.list?.length) { | |
| registry = saved; | |
| } else { | |
| const id = "s_" + Date.now(); | |
| registry.list.push({ id, name: "general" }); | |
| registry.currentActiveId = id; | |
| } | |
| } catch (err) { | |
| console.warn("Registry recovery failover window context invoked:", err); | |
| const id = "s_" + Date.now(); | |
| registry.list.push({ id, name: "general" }); | |
| registry.currentActiveId = id; | |
| } | |
| renderTopMultiplexerBar(); | |
| await loadSessionData(registry.currentActiveId); | |
| updateStatusBar(); | |
| await fetchToolsFromMcpServer(); | |
| if (isAlbertModel(registry.activeModel) && !ALBERT_API_KEY) { | |
| verifyAlbertAuth(); | |
| } | |
| input.focus(); | |
| } | |
| boot(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System.Diagnostics; | |
| using System.Runtime.InteropServices; | |
| using System.Text.Json; | |
| using System.Text.Json.Nodes; | |
| var builder = WebApplication.CreateBuilder(args); | |
| builder.Services.AddCors(options => | |
| { | |
| options.AddDefaultPolicy(policy => policy | |
| .AllowAnyOrigin() | |
| .AllowAnyHeader() | |
| .AllowAnyMethod()); | |
| }); | |
| var app = builder.Build(); | |
| app.UseCors(); | |
| app.Urls.Add("http://localhost:3000"); | |
| app.MapPost("/", async (HttpContext context) => | |
| { | |
| using var reader = new StreamReader(context.Request.Body); | |
| var body = await reader.ReadToEndAsync(); | |
| var json = JsonNode.Parse(body); | |
| if (json == null) return Results.BadRequest("Invalid JSON-RPC payload format."); | |
| string? method = json["method"]?.GetValue<string>(); | |
| var id = json["id"]; | |
| // 1. Handshake Lifecycle | |
| if (method == "initialize") | |
| { | |
| return Results.Json(new | |
| { | |
| jsonrpc = "2.0", | |
| id = id, | |
| result = new { protocolVersion = "2025-11-25" } | |
| }); | |
| } | |
| // 2. Return tools declaration array to your frontend | |
| if (method == "tools/list") | |
| { | |
| return Results.Json(new | |
| { | |
| jsonrpc = "2.0", | |
| id = id, | |
| result = new | |
| { | |
| tools = new[] | |
| { | |
| new | |
| { | |
| name = "list_files", | |
| description = "Lists files and directories in a given local path.", | |
| inputSchema = new | |
| { | |
| type = "object", | |
| properties = new | |
| { | |
| path = new { type = "string", description = "The target directory absolute path (e.g., C:\\temp)" } | |
| }, | |
| required = new[] { "path" } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // 3. Fix: Handle the execution invocation routing target! | |
| if (method == "tools/call") | |
| { | |
| var paramsNode = json["params"]; | |
| string? toolName = paramsNode?["name"]?.GetValue<string>(); | |
| var arguments = paramsNode?["arguments"]; | |
| if (toolName == "list_files") | |
| { | |
| // Safely pull the path argument sent from the frontend | |
| string? targetPath = arguments?["path"]?.GetValue<string>() ?? arguments?["directory"]?.GetValue<string>(); | |
| // Fallback strategy if your model keeps insisting on sending parameter field as "command" | |
| if (string.IsNullOrEmpty(targetPath) && arguments?["command"] != null) | |
| { | |
| string cmd = arguments["command"].GetValue<string>(); | |
| // Extract clean target strings if model sent raw "dir C:\temp" phrases | |
| targetPath = cmd.Replace("dir ", "").Replace("cmd /c", "").Replace("\"", "").Trim(); | |
| } | |
| if (string.IsNullOrEmpty(targetPath)) | |
| { | |
| return Results.Json(CreateRpcError(id, "Missing required argument property: path")); | |
| } | |
| try | |
| { | |
| if (!Directory.Exists(targetPath)) | |
| { | |
| return Results.Json(CreateRpcResult(id, $"Error: Directory '{targetPath}' does not exist.")); | |
| } | |
| // Gather directory content cleanly using standard C# runtime IO definitions | |
| var entries = Directory.GetFileSystemEntries(targetPath) | |
| .Select(e => Path.GetFileName(e)); | |
| string formattedTextResult = $"Contents of {targetPath}:\n" + string.Join("\n", entries); | |
| return Results.Json(CreateRpcResult(id, formattedTextResult)); | |
| } | |
| catch (Exception ex) | |
| { | |
| return Results.Json(CreateRpcResult(id, $"Tool error: {ex.Message}")); | |
| } | |
| } | |
| return Results.Json(CreateRpcError(id, $"Unknown tool execution target location requested: {toolName}")); | |
| } | |
| return Results.Json(CreateRpcError(id, "Method execution handler not established on this target destination.")); | |
| }); | |
| // Structural helper wrappers for clean MCP response formats | |
| object CreateRpcResult(JsonNode? id, string textContent) => new | |
| { | |
| jsonrpc = "2.0", | |
| id = id, | |
| result = new | |
| { | |
| content = new[] { new { type = "text", text = textContent } } | |
| } | |
| }; | |
| object CreateRpcError(JsonNode? id, string errorMessage) => new | |
| { | |
| jsonrpc = "2.0", | |
| id = id, | |
| error = new { code = -32601, message = errorMessage } | |
| }; | |
| app.Run(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>OllamaTerminal — Dracula</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> | |
| <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Fira+Code:wght@300;400;500;600&display=swap" rel="stylesheet" /> | |
| <link rel="stylesheet" href="style.css" /> | |
| </head> | |
| <body> | |
| <div id="app"> | |
| <!-- Title Bar --> | |
| <div id="titleBar"> | |
| <div class="traffic-lights"> | |
| <div class="tl tl-red" title="Close"></div> | |
| <div class="tl tl-yellow" title="Minimize"></div> | |
| <div class="tl tl-green" title="Maximize"></div> | |
| </div> | |
| <span id="titleText">ollama-terminal — dracula edition</span> | |
| <span class="title-badge">MCP</span> | |
| </div> | |
| <!-- Session / Multiplexer Bar --> | |
| <div id="sessionBar"></div> | |
| <!-- Status Bar --> | |
| <div id="statusBar"> | |
| <span class="status-item"> | |
| <span class="status-dot offline" id="mcpDot"></span> | |
| <span id="mcpStatus" class="offline">MCP offline</span> | |
| </span> | |
| <span class="status-sep">│</span> | |
| <span class="status-item">model: <span id="statusModel" style="color:var(--cyan);margin-left:4px;">—</span></span> | |
| <span class="status-sep">│</span> | |
| <span class="status-item" id="toolCount" style="color:var(--orange);">0 tools</span> | |
| <span class="status-sep">│</span> | |
| <span class="status-item" id="msgCount" style="color:var(--comment);">0 msgs</span> | |
| </div> | |
| <!-- Output Log --> | |
| <div id="log"></div> | |
| <!-- Input Row --> | |
| <div id="inputRow"> | |
| <span id="terminalPrompt">user@ollama:~$</span> | |
| <input id="input" type="text" autocomplete="off" spellcheck="false" placeholder="enter a prompt or /help…" /> | |
| <span class="hist-hint" title="↑↓ history">↑↓</span> | |
| <span id="stopBtn" title="Stop generation (Esc)"> | |
| <span class="stop-icon"></span>Stop | |
| </span> | |
| <div class="input-cursor-hint"></div> | |
| </div> | |
| </div> | |
| <script type="module" src="./app.js"> | |
| </script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <Project Sdk="Microsoft.NET.Sdk.Web"> | |
| <PropertyGroup> | |
| <TargetFramework>net10.0</TargetFramework> | |
| <Nullable>enable</Nullable> | |
| <ImplicitUsings>enable</ImplicitUsings> | |
| </PropertyGroup> | |
| </Project> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment