diff options
Diffstat (limited to 'modules/core')
| -rw-r--r-- | modules/core/core-bundle.js | 92 | ||||
| -rw-r--r-- | modules/core/module-aiassist.js | 54 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/core/ollama-manager.js | 46 |
4 files changed, 112 insertions, 82 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index d40988e..0d3a783 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -1604,11 +1604,36 @@ class OllamaManager { } } + // ─── Lazy connect ──────────────────────────────────────────────────────── + /** + * Every request path calls this first. Nothing connects at boot, so the user's first message + * used to die with "Use ollamaconnect first" even when the endpoint was healthy + * (reported 2026-10-26/27). One attempt, then the caller prints the real reason. + * Concurrent callers share the same in-flight promise. + */ + async _ensureConnected() { + if (this.connected) return true; + if (this._connecting) { + try { await this._connecting; } catch (e) { /* fall through to the state check */ } + return this.connected; + } + this.terminal.print('Connecting to Ollama…', 'info'); + this._connecting = this.connect(); + try { await this._connecting; } catch (e) { + this.terminal.log('WARN', 'Ollama', 'Auto-connect failed: ' + e.message); + } + this._connecting = null; + if (!this.connected) { + this.terminal.print('Auto-connect failed — check the endpoint in the Ollama tab (or run ollamaconnect).', 'warning'); + } + return this.connected; + } + // ─── Chat (non-streaming) ──────────────────────────────────────────────── async chat(prompt, model = null) { - if (!this.connected) { - this.terminal.print('Ollama not connected. Use ollamaconnect first.', 'error'); + if (!this.connected && !(await this._ensureConnected())) { + this.terminal.print('Ollama not connected: the endpoint is unreachable. Check the Ollama tab or run ollamaconnect.', 'error'); return null; } const useModel = model || this.currentModel; @@ -1775,7 +1800,10 @@ class OllamaManager { // ─── Chat Stream ───────────────────────────────────────────────────────── async chatStream(prompt, onToken, onComplete, model = null) { - if (!this.connected) { this.terminal.print('Ollama not connected.', 'error'); onComplete && onComplete(null); return; } + if (!this.connected && !(await this._ensureConnected())) { + this.terminal.print('Ollama not connected: the endpoint is unreachable.', 'error'); + onComplete && onComplete(null); return; + } const useModel = model || this.currentModel; if (!useModel) { this.terminal.print('No model selected.', 'error'); onComplete && onComplete(null); return; } @@ -1946,8 +1974,8 @@ class OllamaManager { } async chatWithTools(messages, tools, model = null) { - if (!this.connected) { - this.terminal.print('Ollama not connected. Use ollamaconnect first.', 'error'); + if (!this.connected && !(await this._ensureConnected())) { + this.terminal.print('Ollama not connected: the endpoint is unreachable. Check the Ollama tab or run ollamaconnect.', 'error'); return { message: null, tool_calls: null, error: 'Not connected' }; } const useModel = model || this.currentModel; @@ -10814,41 +10842,19 @@ buildSystemPrompt() { switch (name) { case 'list_files': { const originalPath = a.path || ''; - const p = originalPath.replace(/^\//, ''); - const saved = [...this.terminal.fs.currentPath]; - if (originalPath.startsWith('/')) { - this.terminal.fs.currentPath = []; + const p = originalPath.replace(/^\//, '').replace(/\/+$/, ''); + const fsapi = this.terminal.fs; + const saved = [...fsapi.currentPath]; + if (originalPath.startsWith('/')) fsapi.currentPath = []; + // MAINT-011: readDir() resolves the virtual mounts and returns null for a + // path that does not exist. The old hand-rolled walk over root.content could + // not see /sys, so a directory the UI shows answered "Directory not found". + const entries = fsapi.readDir(p || '/'); + fsapi.currentPath = saved; + if (entries === null || entries === undefined) { + return JSON.stringify({ error: `Directory not found: ${p || '/'}` }); } - if (p) { - const parts = p.split('/'); - const dir = this.terminal.fs.root; - let cur = dir; - let found = true; - for (const part of parts) { - if (part === '..') { saved.pop(); continue; } - if (part && cur.content[part] && cur.content[part].type === 'dir') cur = cur.content[part]; - else { found = false; break; } - } - if (!found) { - // Retry from root — cwd may be stale - this.terminal.fs.currentPath = []; - cur = this.terminal.fs.root; - found = true; - for (const part of parts) { - if (part === '..') continue; - if (part && cur.content[part] && cur.content[part].type === 'dir') cur = cur.content[part]; - else { found = false; break; } - } - if (!found) { - this.terminal.fs.currentPath = saved; - return JSON.stringify({ error: `Directory not found: ${p}` }); - } - } - this.terminal.fs.currentPath = saved.concat(parts.filter(x => x !== '..')); - } - const files = this.terminal.fs.ls(); - this.terminal.fs.currentPath = saved; - return JSON.stringify(files.map(f => ({ name: f.name, type: f.type, size: f.size }))); + return JSON.stringify(entries.map(f => ({ name: f.name, type: f.type, size: f.size }))); } case 'read_file': { const rp = a.path || ''; @@ -10888,8 +10894,14 @@ buildSystemPrompt() { if (wp.startsWith('/')) this.terminal.fs.currentPath = []; const path = wp.replace(/^\//, ''); const content = a.content || ''; - this.terminal.fs.writeFile(path, content); + // MAINT-012: writeFile() returns false when the parent directory is missing + // or the target is a directory node. Ignoring it told the model the write + // succeeded, and its follow-up read then found nothing. + const wrote = this.terminal.fs.writeFile(path, content); this.terminal.fs.currentPath = savedW; + if (!wrote) { + return `Write FAILED: ${path} — parent directory missing or target is a directory; nothing was written`; + } return `File written: ${path} (${content.length} chars)`; } case 'make_directory': { diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index 43cbb11..303f9c2 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -2372,41 +2372,19 @@ buildSystemPrompt() { switch (name) { case 'list_files': { const originalPath = a.path || ''; - const p = originalPath.replace(/^\//, ''); - const saved = [...this.terminal.fs.currentPath]; - if (originalPath.startsWith('/')) { - this.terminal.fs.currentPath = []; + const p = originalPath.replace(/^\//, '').replace(/\/+$/, ''); + const fsapi = this.terminal.fs; + const saved = [...fsapi.currentPath]; + if (originalPath.startsWith('/')) fsapi.currentPath = []; + // MAINT-011: readDir() resolves the virtual mounts and returns null for a + // path that does not exist. The old hand-rolled walk over root.content could + // not see /sys, so a directory the UI shows answered "Directory not found". + const entries = fsapi.readDir(p || '/'); + fsapi.currentPath = saved; + if (entries === null || entries === undefined) { + return JSON.stringify({ error: `Directory not found: ${p || '/'}` }); } - if (p) { - const parts = p.split('/'); - const dir = this.terminal.fs.root; - let cur = dir; - let found = true; - for (const part of parts) { - if (part === '..') { saved.pop(); continue; } - if (part && cur.content[part] && cur.content[part].type === 'dir') cur = cur.content[part]; - else { found = false; break; } - } - if (!found) { - // Retry from root — cwd may be stale - this.terminal.fs.currentPath = []; - cur = this.terminal.fs.root; - found = true; - for (const part of parts) { - if (part === '..') continue; - if (part && cur.content[part] && cur.content[part].type === 'dir') cur = cur.content[part]; - else { found = false; break; } - } - if (!found) { - this.terminal.fs.currentPath = saved; - return JSON.stringify({ error: `Directory not found: ${p}` }); - } - } - this.terminal.fs.currentPath = saved.concat(parts.filter(x => x !== '..')); - } - const files = this.terminal.fs.ls(); - this.terminal.fs.currentPath = saved; - return JSON.stringify(files.map(f => ({ name: f.name, type: f.type, size: f.size }))); + return JSON.stringify(entries.map(f => ({ name: f.name, type: f.type, size: f.size }))); } case 'read_file': { const rp = a.path || ''; @@ -2446,8 +2424,14 @@ buildSystemPrompt() { if (wp.startsWith('/')) this.terminal.fs.currentPath = []; const path = wp.replace(/^\//, ''); const content = a.content || ''; - this.terminal.fs.writeFile(path, content); + // MAINT-012: writeFile() returns false when the parent directory is missing + // or the target is a directory node. Ignoring it told the model the write + // succeeded, and its follow-up read then found nothing. + const wrote = this.terminal.fs.writeFile(path, content); this.terminal.fs.currentPath = savedW; + if (!wrote) { + return `Write FAILED: ${path} — parent directory missing or target is a directory; nothing was written`; + } return `File written: ${path} (${content.length} chars)`; } case 'make_directory': { diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index 0d61710..1f790e3 100644 --- a/modules/core/module-loader.js +++ b/modules/core/module-loader.js @@ -4,7 +4,7 @@ // Core module bundle loaded synchronously in main.html. // This loader only handles version cache-busting, splash, terminal init, and optional ext modules. - const LATEST_VER = 'v4.6.104'; + const LATEST_VER = 'v4.6.105'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { diff --git a/modules/core/ollama-manager.js b/modules/core/ollama-manager.js index 4f47db2..3d157ba 100644 --- a/modules/core/ollama-manager.js +++ b/modules/core/ollama-manager.js @@ -369,11 +369,36 @@ class OllamaManager { } } + // ─── Lazy connect ──────────────────────────────────────────────────────── + /** + * Every request path calls this first. Nothing connects at boot, so the user's first message + * used to die with "Use ollamaconnect first" even when the endpoint was healthy + * (reported 2026-10-26/27). One attempt, then the caller prints the real reason. + * Concurrent callers share the same in-flight promise. + */ + async _ensureConnected() { + if (this.connected) return true; + if (this._connecting) { + try { await this._connecting; } catch (e) { /* fall through to the state check */ } + return this.connected; + } + this.terminal.print('Connecting to Ollama…', 'info'); + this._connecting = this.connect(); + try { await this._connecting; } catch (e) { + this.terminal.log('WARN', 'Ollama', 'Auto-connect failed: ' + e.message); + } + this._connecting = null; + if (!this.connected) { + this.terminal.print('Auto-connect failed — check the endpoint in the Ollama tab (or run ollamaconnect).', 'warning'); + } + return this.connected; + } + // ─── Chat (non-streaming) ──────────────────────────────────────────────── async chat(prompt, model = null) { - if (!this.connected) { - this.terminal.print('Ollama not connected. Use ollamaconnect first.', 'error'); + if (!this.connected && !(await this._ensureConnected())) { + this.terminal.print('Ollama not connected: the endpoint is unreachable. Check the Ollama tab or run ollamaconnect.', 'error'); return null; } const useModel = model || this.currentModel; @@ -540,7 +565,10 @@ class OllamaManager { // ─── Chat Stream ───────────────────────────────────────────────────────── async chatStream(prompt, onToken, onComplete, model = null) { - if (!this.connected) { this.terminal.print('Ollama not connected.', 'error'); onComplete && onComplete(null); return; } + if (!this.connected && !(await this._ensureConnected())) { + this.terminal.print('Ollama not connected: the endpoint is unreachable.', 'error'); + onComplete && onComplete(null); return; + } const useModel = model || this.currentModel; if (!useModel) { this.terminal.print('No model selected.', 'error'); onComplete && onComplete(null); return; } @@ -711,8 +739,8 @@ class OllamaManager { } async chatWithTools(messages, tools, model = null) { - if (!this.connected) { - this.terminal.print('Ollama not connected. Use ollamaconnect first.', 'error'); + if (!this.connected && !(await this._ensureConnected())) { + this.terminal.print('Ollama not connected: the endpoint is unreachable. Check the Ollama tab or run ollamaconnect.', 'error'); return { message: null, tool_calls: null, error: 'Not connected' }; } const useModel = model || this.currentModel; @@ -893,7 +921,13 @@ class OllamaManager { } return { - message: message ? { role: message.role || 'assistant', content: content } : null, + message: message ? { + role: message.role || 'assistant', + content: content, + // OpenAI-compatible providers require tool_calls INSIDE the assistant message; + // a bare { role:'tool' } with no preceding tool_calls is rejected with HTTP 400. + ...(Array.isArray(toolCalls) && toolCalls.length ? { tool_calls: toolCalls } : {}), + } : null, tool_calls: toolCalls ? toolCalls.map(tc => ({ id: tc.id, type: tc.type || 'function', |