From 4baa484b84c5184286cac2743575251454a5ee2b Mon Sep 17 00:00:00 2001 From: Agando Date: Mon, 25 May 2026 18:02:02 +0200 Subject: auto: 2026-05-25_18:02 changes --- BUGFIX-SUGGESTIONS.md | 355 ++++++++ download-newos4.html | 88 ++ help.html | 926 +++++++++++++++++++++ index.html | 810 +++++++++++++++++++ index.html.pre-refactor | 35 + modules/core/module-aiasm.js | 26 + modules/core/module-aiassist.js | 1435 +++++++++++++++++++++++++++++++++ modules/core/module-chatstudio.js | 1060 ++++++++++++++++++++++++ modules/core/module-config.js | 92 +++ modules/core/module-duckduckgo.js | 38 + modules/core/module-dynamicprompts.js | 102 +++ modules/core/module-fetcher.js | 65 ++ modules/core/module-fs.js | 445 ++++++++++ modules/core/module-io.js | 80 ++ modules/core/module-loader.js | 178 ++++ modules/core/module-manager.js | 138 ++++ modules/core/module-modmgr.js | 40 + modules/core/module-my.js | 82 ++ modules/core/module-ollama.js | 52 ++ modules/core/module-ollamamodels.js | 87 ++ modules/core/module-searxng.js | 73 ++ modules/core/module-servermon.js | 107 +++ modules/core/module-simplegui.js | 67 ++ modules/core/module-skills.js | 271 +++++++ modules/core/module-smartctx.js | 93 +++ modules/core/module-system.js | 154 ++++ modules/core/ollama-manager.js | 842 +++++++++++++++++++ modules/core/secret-vault.js | 459 +++++++++++ modules/core/terminal.js | 1362 +++++++++++++++++++++++++++++++ modules/core/virtual-fs.js | 320 ++++++++ modules/ext/alias.js | 108 +++ modules/ext/calc.js | 77 ++ modules/ext/cron.js | 110 +++ modules/ext/diff.js | 287 +++++++ modules/ext/editor.js | 70 ++ modules/ext/filemgr.js | 755 +++++++++++++++++ modules/ext/finder.js | 47 ++ modules/ext/mindmap.js | 217 +++++ modules/ext/skill-pack-core.js | 285 +++++++ modules/ext/theme.js | 172 ++++ modules/ext/voice.js | 102 +++ modules/ext/webdav.js | 532 ++++++++++++ modules/ext/wpm.js | 149 ++++ packages.json | 112 +++ packages.json.orig | 168 ++++ space.yaml | 14 + summary.txt | 14 + 47 files changed, 13101 insertions(+) create mode 100644 BUGFIX-SUGGESTIONS.md create mode 100644 download-newos4.html create mode 100644 help.html create mode 100644 index.html create mode 100644 index.html.pre-refactor create mode 100644 modules/core/module-aiasm.js create mode 100644 modules/core/module-aiassist.js create mode 100644 modules/core/module-chatstudio.js create mode 100644 modules/core/module-config.js create mode 100644 modules/core/module-duckduckgo.js create mode 100644 modules/core/module-dynamicprompts.js create mode 100644 modules/core/module-fetcher.js create mode 100644 modules/core/module-fs.js create mode 100644 modules/core/module-io.js create mode 100644 modules/core/module-loader.js create mode 100644 modules/core/module-manager.js create mode 100644 modules/core/module-modmgr.js create mode 100644 modules/core/module-my.js create mode 100644 modules/core/module-ollama.js create mode 100644 modules/core/module-ollamamodels.js create mode 100644 modules/core/module-searxng.js create mode 100644 modules/core/module-servermon.js create mode 100644 modules/core/module-simplegui.js create mode 100644 modules/core/module-skills.js create mode 100644 modules/core/module-smartctx.js create mode 100644 modules/core/module-system.js create mode 100644 modules/core/ollama-manager.js create mode 100644 modules/core/secret-vault.js create mode 100644 modules/core/terminal.js create mode 100644 modules/core/virtual-fs.js create mode 100644 modules/ext/alias.js create mode 100644 modules/ext/calc.js create mode 100644 modules/ext/cron.js create mode 100644 modules/ext/diff.js create mode 100644 modules/ext/editor.js create mode 100644 modules/ext/filemgr.js create mode 100644 modules/ext/finder.js create mode 100644 modules/ext/mindmap.js create mode 100644 modules/ext/skill-pack-core.js create mode 100644 modules/ext/theme.js create mode 100644 modules/ext/voice.js create mode 100644 modules/ext/webdav.js create mode 100644 modules/ext/wpm.js create mode 100644 packages.json create mode 100644 packages.json.orig create mode 100644 space.yaml create mode 100644 summary.txt diff --git a/BUGFIX-SUGGESTIONS.md b/BUGFIX-SUGGESTIONS.md new file mode 100644 index 0000000..1d10fb6 --- /dev/null +++ b/BUGFIX-SUGGESTIONS.md @@ -0,0 +1,355 @@ +# newOS4 Bug Fix Suggestions + +**Generated:** 2026-05-24 +**Status:** Pending Review +**Priority:** High (13 issues across 7 files) + +--- + +## Executive Summary + +| Category | Issues | Files | Severity | +|----------|--------|-------|----------| +| File Structure | 5 | 4 | High | +| Agent System | 5 | 2 | High | +| AI Routing | 2 | 1 | Medium | +| Cache | 1 | 1 | Low | +| **Total** | **13** | **7** | | + +--- + +## 1. File Structure Issues + +### 1.1 Splash Fallback Ext List Incomplete +- **File:** `index.html` +- **Line:** 770 +- **Issue:** Splash fallback hardcodes 5 ext modules but `modules/ext/` directory has 8 files +- **Missing:** `mindmap.js`, `webdav.js`, `skill-pack-core.js` +- **Fix:** + ```javascript + // Before (5 modules) + ['Extensions', ['calc.js','voice.js','cron.js','theme.js','wpm.js'], false] + + // After (8 modules) + ['Extensions', ['calc.js','voice.js','cron.js','theme.js','wpm.js','mindmap.js','webdav.js','skill-pack-core.js'], false] + ``` + +--- + +### 1.2 Preview Iframe Blocks JavaScript +- **File:** `index.html` +- **Line:** 684 +- **Issue:** ` + + + + ``` + +--- + +### 1.3 Dead Code: EXT_FILES Array +- **File:** `module-loader.js` +- **Lines:** 35-37 +- **Issue:** `EXT_FILES` array defined but never referenced (dead code, wrong count) +- **Impact:** Maintenance confusion, wrong module count (6 vs actual 8) +- **Fix:** Remove entirely — ext loading uses separate `extKeys` array on line 159 + +--- + +### 1.4 Packages.json URL Errors +- **File:** `packages.json` +- **Lines:** 7-167 +- **Issues:** + - All 18 package URLs point to `modules/.js` (wrong path) + - Actual files are in `modules/ext/.js` + - 12 packages reference files that don't exist anywhere +- **Impact:** WPM installs fail with 404 errors +- **Fix Options:** + 1. **Clean:** Remove 12 phantom packages, fix 6 real URLs to `modules/ext/` + 2. **Stub:** Create missing files in `modules/ext/` +- **Recommendation:** Option 1 (clean) — reduces bloat + +--- + +### 1.5 Search Uses Intrusive Alert() +- **File:** `help.html` +- **Line:** 1149 +- **Issue:** `alert('No results found')` blocks UI +- **Impact:** Poor UX, modal interruption +- **Fix:** + ```javascript + // Before + if (!found) alert('No results found for: ' + query); + + // After + if (!found) { + const msg = document.getElementById('search-result-msg'); + if (msg) { + msg.textContent = 'No results found for: ' + query; + msg.style.display = 'block'; + setTimeout(() => { msg.style.display = 'none'; }, 3000); + } + } + ``` + +--- + +## 2. Agent System Issues + +### 2.1 Agent Loop Caps at 1 Turn (Not 5) +- **File:** `module-aiassist.js` +- **Lines:** 537-547 +- **Issue:** When model returns text + tool_calls, agent executes tools then `return`s (exits loop) +- **Impact:** Agent stops after 1 tool turn instead of advertised `MAX_TOOL_TURNS = 5` +- **Root Cause:** Follow-up text-response block returns instead of continuing loop +- **Fix:** + ```javascript + // Before (lines 537-548) + if (pendingText) { + const followUp = await this.terminal.ollamaManager.chatWithTools(messages, []); + if (followUp.message && followUp.message.content) { + this.terminal.printAIResponse(followUp.message.content); + return { finalAnswer: followUp.message.content, toolTurns }; // ← exits loop + } + } + continue; + + // After + if (pendingText) { + messages.push({ role: 'assistant', content: pendingText }); + } + continue; // ← loop continues, allows up to 5 turns + ``` + +--- + +### 2.2 edit_module Not Checked as Destructive +- **File:** `module-aiassist.js` +- **Line:** 730 +- **Issue:** `isDestructive()` doesn't include `edit_module` but system prompt (line 227) says it's `(DESTRUCTIVE)` +- **Impact:** AI told to ask permission but code never confirms +- **Fix:** + ```javascript + // Before + isDestructive(name) { + return name === 'write_file' || name === 'delete_file' || + name === 'run_command' || name === 'make_directory'; + } + + // After + isDestructive(name) { + return name === 'write_file' || name === 'delete_file' || + name === 'run_command' || name === 'make_directory' || + name === 'edit_module'; + } + ``` + +--- + +### 2.3 Empty tools:[] Sent to Ollama +- **File:** `module-aiassist.js` +- **Line:** 539 +- **Issue:** `chatWithTools(messages, [])` sends empty tools array +- **Impact:** Some Ollama models reject `tools: []`, triggers unnecessary fallback request +- **Fix:** Remove follow-up call; push text as `assistant` message then `continue` loop (see 2.1) + +--- + +### 2.4 Default Model Blocks Auto-Selection +- **File:** `ollama-manager.js` +- **Lines:** 8, 42-45 +- **Issue:** Default model `'gemma4:31b-cloud'` is truthy, so auto-selection skipped even if model doesn't exist +- **Impact:** Users get silent model errors until manually selecting +- **Fix:** + ```javascript + // Before (lines 42-45) + if (!this.currentModel && this.models.length > 0) { + this.currentModel = this.models[0].name; + // ... + } + + // After + if (!this.currentModel || !this.models.some(m => m.name === this.currentModel)) { + if (this.models.length > 0) { + this.currentModel = this.models[0].name; + // ... + } + } + ``` + +--- + +### 2.5 abort() Misses chat() and chatStream() +- **File:** `ollama-manager.js` +- **Lines:** 70, 140, 176-183 +- **Issue:** `_abortController` only set in `chatWithTools()` (line 195), not in `chat()` or `chatStream()` +- **Impact:** `ai-stop` command only cancels tool-calling requests, not direct chat or streaming +- **Fix:** + ```javascript + // Add to chat() method (around line 70) + this._abortController = new AbortController(); + const signal = this._abortController.signal; + const response = await fetch(`${this.endpoint}/api/generate`, { + // ... + signal + }); + this._abortController = null; + + // Add to chatStream() method (around line 140) - same pattern + ``` + +--- + +## 3. AI Routing Issues + +### 3.1 Unknown Commands Not Awaited (executeCommand) +- **File:** `terminal.js` +- **Line:** 629 +- **Issue:** `aiMod.chat([rawInput.trim()])` called without `await` +- **Impact:** Fire-and-forget execution, errors uncaught, no sequencing +- **Fix:** + ```javascript + // Before + aiMod.chat([rawInput.trim()]); + return; + + // After + this.print('→ Routing to AI assistant...', 'info'); + await aiMod.chat([rawInput.trim()]); + return; + ``` + +--- + +### 3.2 Unknown Commands Not Awaited (dispatchCommand) +- **File:** `terminal.js` +- **Line:** 691 +- **Issue:** Same as 3.1 but in `dispatchCommand()` (used for pipe chains) +- **Fix:** Same as 3.1 + +**Combined Impact:** Unknown commands like `hello` route to AI but without proper error handling or user feedback + +--- + +## 4. Cache Invalidation + +### 4.1 Cache Version Stale +- **File:** `module-loader.js` +- **Line:** 102 +- **Issue:** `CACHE = '?v=17'` serves old cached modules +- **Impact:** Browser doesn't re-fetch updated module files +- **Fix:** + ```javascript + // Before + const CACHE = '?v=17'; + + // After + const CACHE = '?v=18'; + ``` +- **Note:** Requires users to hard refresh (Ctrl+F5 / Cmd+Shift+R) + +--- + +## Implementation Priority + +### High Priority (Critical Bugs) +1. **2.1** Agent loop caps at 1 turn — breaks multi-step AI tasks +2. **1.4** Packages.json 404 errors — WPM installs broken +3. **2.4** Default model blocks auto-selection — invisible failures +4. **2.5** abort() incomplete — `ai-stop` doesn't work for chat/stream + +### Medium Priority (Functional Issues) +5. **1.1** Splash fallback incomplete — minor UX issue +6. **2.2** edit_module destructive check — security inconsistency +7. **3.1 + 3.2** AI routing without await — error handling gaps +8. **1.2** Preview iframe sandbox — JS previews broken + +### Low Priority (Cleanup) +9. **1.3** Dead EXT_FILES code — maintenance clarity +10. **1.5** Search alert() → inline — UX polish +11. **4.1** Cache version — required after applying fixes + +--- + +## Tradeoffs & Considerations + +### Packages.json Cleanup (1.4) +- **Option A (Recommended):** Remove 12 phantom packages + - ✅ Reduces bloat, cleaner codebase + - ❌ Breaks backward compatibility if anyone installed them +- **Option B:** Create stub files for missing packages + - ✅ Preserves compatibility + - ❌ Adds dead weight, maintenance burden + +### Agent Loop Change (2.1) +- Removing follow-up call changes message structure slightly +- Text+tool hybrid responses now push text as `assistant` message, then loop continues +- This is correct behavior but differs from original design + +### AI Routing (3.1 + 3.2) +- Routing all unknown commands to AI may increase token usage +- Consider adding `ai-mode` toggle or `--no-ai` flag for opt-out + +### Cache Version (4.1) +- Bumping version requires all users to hard refresh +- Consider adding release notes or announcement + +--- + +## Testing Checklist + +After applying fixes: + +- [ ] Hard refresh page (Ctrl+F5) +- [ ] Test splash screen shows all 8 ext modules +- [ ] Test HTML preview with JavaScript executes +- [ ] Test WPM package installs succeed +- [ ] Test search shows inline message (no alert) +- [ ] Test multi-step AI task (5 tool turns) +- [ ] Test `edit_module` prompts for confirmation +- [ ] Test auto-select model on connect +- [ ] Test `ai-stop` cancels chat/stream +- [ ] Test unknown command routes to AI (`hello`) + +--- + +## Files to Modify + +``` +spaces/chatstudio/newos4/ +├── index.html (lines 684, 770) +├── help.html (line 1149) +├── packages.json (lines 7-167) +└── modules/core/ + ├── module-loader.js (lines 35-37, 102) + ├── module-aiassist.js (lines 537-547, 730) + ├── ollama-manager.js (lines 8, 42-45, 70, 140) + └── terminal.js (lines 629, 691) +``` + +--- + +## Estimated Effort + +| Category | Time | Complexity | +|----------|------|------------| +| File fixes | 15 min | Low | +| Agent fixes | 30 min | Medium | +| AI routing | 10 min | Low | +| Testing | 20 min | Medium | +| **Total** | **~75 min** | | + +--- + +## Approval Required + +**Ready to execute?** Reply with: +- ✅ "Execute all" — apply all 13 fixes +- ✅ "Execute high priority" — apply top 4 fixes only +- ✅ "Skip #X" — exclude specific fixes +- ❓ "Discuss #X" — more details on specific issue diff --git a/download-newos4.html b/download-newos4.html new file mode 100644 index 0000000..af4cf73 --- /dev/null +++ b/download-newos4.html @@ -0,0 +1,88 @@ + + + + + +Download newOS4 v4.2 + + + +

⬇ newOS4 v4.2 — Download

+ +📦 Download newOS4.tar.gz (101 KB) + +
+

Includes: index.html, help.html, packages.json, BUGFIX-SUGGESTIONS.md, all 22 core modules, 8 ext modules

+

How to use on mobile:

+
+1. Tap download button above
+2. Unzip with a file manager or use this page:
+
+
+ +

📂 Quick Extractor

+ +
Select the .tar.gz file above
+ + + + +
+

+🔗 Direct link: newos4.tar.gz
+📏 Size: 101 KB | Updated: May 2026
+💡 Tip: On Android, use ZArchiver or RAR app to extract the tar.gz. +

+
+ + diff --git a/help.html b/help.html new file mode 100644 index 0000000..80c2e9c --- /dev/null +++ b/help.html @@ -0,0 +1,926 @@ + + + + + + +WebX11 v4.2 — Help + + + + +
+
+
+ +

WebX11 Docs

+
+
+ v4.2 + 24 core · 12 ext + 110+ commands +
+
+
+ + + +
+ +
+ + +
+

⛅ Overview

+

WebX11 v4.2 is a browser-based Unix-like terminal — dynamic modules, AI assistant (Ollama), persistent virtual filesystem, web search, voice input, code editor, and a full skill system. Everything runs client-side in your browser.

+
+

🧩 24 Core + 12 Ext

Modules loaded at startup. Load new ones from any URL at runtime.

+

🤖 Agent with Tools

15+ tools — file ops, web search, skills, modules. Destructive action confirmations with inline bar + countdown.

+

📚 Skills System

Persistent procedural memory. Create, patch, search, export skills for repeatable workflows.

+

💾 VFS + Sessions

Virtual filesystem persists in localStorage. Save/load/export sessions.

+

🎤 Voice Input

Speech recognition in the terminal. Works in Chrome, Edge, Safari.

+

🌐 Web Search

SearXNG + DuckDuckGo fallback + URL fetch/extract. All built-in.

+

☁️ WebDAV Sync

Push, pull, backup VFS to any WebDAV server.

+

📱 Mobile First

Touch-optimized. srcdoc HTML preview works on mobile.

+

✏️ Text Editor

Inline VFS editor — edit <path> opens a modal with save/cancel.

+

🔗 Aliases

ll, h, cls built-in. Define custom aliases with alias x=y.

+
+
+ + +
+

🚀 Quick Start

+
    +
  1. Open index.html in Chrome/Edge
  2. +
  3. Watch the splash screen load modules
  4. +
  5. Type help + Enter
  6. +
  7. Use the quick-action buttons above the input area
  8. +
+ +

First Commands

+
# List VFS files
+ls
+
+# List files faster
+ll
+
+# Connect to Ollama AI
+ollamaconnect
+
+# Chat with AI
+ai what's in this directory?
+
+# Create a file
+put hello.txt Hello World
+
+# Edit a file
+edit hello.txt
+
+# Search the web
+search quantum computing
+
+# Find files
+find hello
+
+# Compare two files
+diff a.txt b.txt
+ +
+ ⌨️ Keyboard Shortcuts +
    +
  • Enter — Execute
  • +
  • Tab — Cycle autocomplete
  • +
  • ↑/↓ — Command history
  • +
  • Esc — Hide autocomplete / stop AI
  • +
  • | — Pipe: ls | grep hello
  • +
+
+
+ + +
+

📖 Full Command Reference

+ +

System

+
+ + + + + + + + + + + + +
CommandDescriptionExample
helpShow all commandshelp
clear / clsClear terminalcls
echoPrint textecho Hello
memShow memory usagemem
statusSystem statusstatus
font-sizeChange font size (8–32)font-size 16
load-localLoad module from VFS or URLload-local modules/ext/calc.js
startup-modulesOpen startup module selectorstartup-modules
previewPreview HTML in iframepreview test.html
factory-resetReset configfactory-reset
reset-vfsReset VFSreset-vfs
+ +

AI Assistant

+
+ + + + + + + + +
CommandDescriptionExample
ai / askChat with AI (tool-calling)ai create a notes file
ai-clearReset conversation contextai-clear
ai-systemView/set system promptai-system
ai-contextShow conversation memoryai-context
ai-allowAllow pending destructive actionai-allow
ai-denyDeny pending destructive actionai-deny
ai-stopCancel running AI requestai-stop
+ +

Smart Context

+
+ + + + + + +
CommandDescriptionExample
contextSet active profilecontext expert
contextsList profilescontexts
context-newCreate profilecontext-new myprofile
context-deleteDelete profilecontext-delete old
smart-askAsk with current profilesmart-ask explain this
+ +

Dynamic Prompts

+
+ + + + + + + +
CommandDescriptionExample
promptUse saved templateprompt summarize
prompt-saveSave templateprompt-save myprompt
prompt-listList saved promptsprompt-list
prompt-deleteDelete promptprompt-delete old
prompt-varSet template variableprompt-var name Alice
prompt-varsList variablesprompt-vars
+ +

Skills

+
+ + + + + + + + + +
CommandDescriptionExample
skill-listList all skillsskill-list
skill-viewView skill contentskill-view web-scraper
skill-createCreate a skillskill-create name desc "steps" tags
skill-patchUpdate a skillskill-patch name "new content"
skill-deleteDelete a skillskill-delete oldskill
skill-exportExport skill to JSONskill-export myskill
skill-importImport skill from JSONskill-import
skill-syncRebuild skill indexskill-sync
+ +

Config

+
+ + + + + + + +
CommandDescriptionExample
configGet a config valueconfig
config-setSet a config valueconfig-set key value
config-listList all configconfig-list
config-deleteDelete a config entryconfig-delete key
config-exportExport config as JSONconfig-export
config-resetReset all configconfig-reset
+
+ + +
+

📁 Filesystem

+

All VFS files persist in localStorage. ls, cd, pwd work like a real terminal. Clickable output — tap a file to cat, tap a dir to cd.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
CommandDescriptionExample
ls / llList filesll
cdChange directorycd notes
pwdPrint working directorypwd
catView filecat readme.txt
putCreate file with contentput hello.txt Hello World
editOpen in modal editoredit notes.md
rmDelete filerm temp.txt
mkdirCreate directorymkdir projects
rmdirRemove directoryrmdir empty
mvMove/renamemv old.txt new.txt
cpCopy filecp a.txt b.txt
touchCreate empty filetouch newfile.txt
treeDirectory treetree
findRecursive file searchfind .txt
diffCompare two filesdiff a.txt b.txt
grepSearch file contentsgrep hello *.txt
headFirst N lineshead file.txt 10
tailLast N linestail file.txt 10
sortSort file linessort file.txt
wcCount lines/words/charswc file.txt
duDisk usagedu
uploadUpload file from deviceupload
downloadDownload filedownload file.txt
vfs-exportDownload VFS as JSONvfs-export
vfs-importImport VFS from JSONvfs-import
+
+ + +
+

🤖 AI Assistant

+
+ Tool-Calling Agent + The AI runs a full agent loop with 15+ tools: filesystem, web search, URL fetch, skills, WPM, session memory, user profile, module introspection. +
+ +

How It Works

+
    +
  1. Type ai <request>
  2. +
  3. AI loads relevant skills + recent session memory
  4. +
  5. Decides which tools to call (up to 5 turns)
  6. +
  7. Destructive tools trigger an inline confirm bar — no dark overlay
  8. +
  9. 120s countdown: yellow → bold red at 30s → pulsing at 10s
  10. +
  11. Type ai-allow / ai-deny or tap the buttons
  12. +
  13. After complex tasks, AI suggests creating a skill
  14. +
+ +
+ ⚠️ Destructive Tools + write_file, delete_file, run_command, make_directory, edit_module all require confirmation before executing. +
+ +

Available Tools

+
+ + + + + + + + + + + + + + + + +
ToolDescriptionDestructive?
list_filesList VFS directory❌
read_fileRead file (8k trunc)❌
write_fileWrite to file✅
make_directoryCreate directory✅
delete_fileDelete file/dir✅
get_statusSystem status❌
get_helpList commands❌
web_searchSearXNG + DDG❌
web_fetchExtract URL content❌
run_commandExecute terminal cmd✅
load_skillsSearch relevant skills❌
create/patch/list_skillsSkill management❌
view/edit/test/reloadModule operationsedit ✅
user_get/set/list/forgetUser profile❌
session_save/load/recent/statsSession memory❌
+ +

Sound Notification

+

A short "ding" plays via WebAudio when AI finishes. Toggle with the 🔊 Sound button in quick-actions. Persists in localStorage.

+
+ + +
+

📚 Skills System

+
+ Procedural Memory + Skills are reusable markdown procedures stored in VFS /skills/. The AI auto-loads relevant skills at task start. Create skills after complex workflows (3+ tool calls) to save the approach. +
+ +

Format

+
---
+name: web-scrape-and-summarize
+description: Scrape a URL and summarize
+tags: [web, research]
+category: general
+---
+## Steps
+1. Use web_fetch(url) to get content
+2. Present a concise summary
+3. Save with create_skill
+ +

Commands

+
# List all skills
+skill-list
+
+# Create a skill
+skill-create my-skill "Does X" "Steps to do X" "tag1,tag2"
+
+# View / patch / delete
+skill-view my-skill
+skill-patch my-skill "Updated steps"
+skill-delete my-skill
+
+# Export / Import
+skill-export my-skill
+skill-import
+ +

Auto-Creation

+

When the AI encounters the same tool error 3+ times, a workaround skill is auto-created. Every 20 messages, skills are audited for staleness.

+
+ + +
+

🤖 Ollama

+
+ Local or Remote AI + WebX11 connects to any Ollama instance. All tool-calling and chat features require a connection. +
+ +

Setup

+
# Install & pull a model
+curl -fsSL https://ollama.com/install.sh | sh
+ollama pull llama2
+
+# Start Ollama
+ollama serve
+
+# In WebX11
+ollamaconnect
+ +

Commands

+
+ + + + + + + + + + + + +
CommandDescription
ollamaconnectConnect to Ollama
ollamasetproxySet remote endpoint
ollamamodelsList models
ollamasetmodelSet active model
ollamadisconnectDisconnect
ollamaOpen Ollama Manager UI
modelsList all models
model-infoModel details
model-pullPull a model
model-deleteDelete a model
model-runRun model with prompt
+ +
+ 🔌 CORS for Remote + OLLAMA_ORIGINS="*" ollama serve +
+
+ + +
+

🌐 Search & Web

+
+ SearXNG + DDG + URL Fetcher + Three modules provide comprehensive web capabilities via the proxy API. +
+
+ + + + + +
CommandModuleDescriptionExample
search / sSearXNGWeb searchsearch raspberry pi
ddg / duckDDGDuckDuckGo fallbackddg news today
fetch / webFetcherFetch URL contentfetch https://example.com
extractFetcherClean readable textextract https://news.article
+

The AI agent also has web_search and web_fetch tools using the same APIs.

+
+ + +
+

🎤 Voice & Sound

+
+ Speech Recognition + A 🎤 button in the input area activates SpeechRecognition. Chrome, Edge, Safari 14.1+. Not Firefox. +
+
    +
  • Tap 🎤 to start listening — button pulses green
  • +
  • Say your command — recognized text fills the input
  • +
  • Tap again or wait to stop
  • +
  • Red flash indicates failure
  • +
+

Sound

+

Toggle AI response "ding" with the 🔊 Sound button. Persists in localStorage.

+
+ + +
+

⚙️ Configuration

+

All config values stored as webx11_config_* in localStorage. Use the ⚙ Config button or CLI commands.

+

Notable Keys

+
+ + + +
KeyDescriptionValues
webx11_config_ai_soundAI sound toggleon, off
webx11_config_voice_visibleMic button visibilityon, off
+

Persisted State

+
+ + + + + + + + + + + + +
KeyDescription
webx11_filesystemVFS JSON
webx11_modulesLoaded module URLs
webx11_ollama_endpointOllama API URL
webx11_ollama_modelSelected model
webx11_startup_modulesStartup selection
webx11_font_sizeFont size (8–32)
webx11_command_historyCommand history
webx11_terminal_outputPersisted output
webx11_ai_contextAI conversation history
webx11_user_profileAI user profile
webx11_aliasesUser-defined aliases
+
+ + +
+

🧩 Module System

+
+ 24 Core + 12 Ext + Modules loaded in dependency order at startup. Toggle ext modules with ⚙ gear on splash. +
+

Commands

+
+ + + + + +
CommandDescription
modulesOpen Module Manager UI
loadmod <url>Load module from URL
removemod <name>Remove a module
listmodsList all loaded modules
+ +

Boot Order

+
    +
  1. Splash screen with ⚙ gear
  2. +
  3. Foundation: virtual-fs.js → module-manager.js → ollama-manager.js → terminal.js
  4. +
  5. Built-in modules loaded async
  6. +
  7. Ext modules loaded if selected
  8. +
  9. Splash hides, terminal ready
  10. +
+ +

Code Editor

+

The AI can edit_module(name) to copy source into the Code Editor modal. Edit inline, then 💾 Save & Test to load live from /_mods/. Use test_module without reloading the page.

+
+ + +
+

🧰 Extended Modules

+ +

WebDAV Sync

+
+ + + + + + + + + +
CommandDescription
webdav-configConfigure server
webdav-statusConnection status
webdav-lsList remote dir
webdav-pushPush file
webdav-pullPull file
webdav-backupBackup VFS
webdav-restoreRestore VFS
webdav-backup-listList backups
+ +

Calculator & Cron

+
+ + + + + + +
CommandModuleDescription
calccalc.jsEvaluate math / unit conversion
cron-everycron.jsSchedule a command
cron-listcron.jsList scheduled jobs
cron-stopcron.jsStop a job
cron-clearcron.jsClear all jobs
+ +

Theme, WPM & Mindmap

+
+ + + + +
CommandModuleDescription
themetheme.jsSet color theme (dark, light, amber, cyber, …)
wpmwpm.jsPackage manager
mindmapmindmap.jsShow file structure as mindmap
+ +

Server Monitor

+
+ + + + + + +
CommandDescription
serverAdd server to monitor
serversList monitored servers
server-checkPing a server
server-removeRemove server
monitorToggle auto-monitoring
+ +

IO / Backup

+
+ + + + + + +
CommandDescription
export-modsExport module configs
import-modsImport module configs
backupFull system backup
restoreRestore from backup
share-modShare module as text
+ +

Aliases

+

Built-in: ll = ls, h = help, cls = clear. Define custom: alias x=ls -la. Remove: unalias x. Persists in localStorage.

+
+ + +
+

💬 Sessions

+

Sessions persist AI conversation logs in VFS /memory/. The AI auto-loads recent sessions at start for continuity.

+

AI Tools

+
+ + + + + +
ToolDescription
session_saveSave summary
session_loadLoad by date
session_recentShow last N
session_statsAggregate statistics
+

The terminal auto-saves session state every 60s and auto-restores on page reload.

+
+ + +
+

💾 Storage

+
+ localStorage Persistence + Everything survives hard refreshes via 15+ localStorage keys. +
+
# Clear all WebX11 data
+localStorage.clear()
+location.reload()
+
+# Back up / restore VFS
+vfs-export
+vfs-import
+

~5–10MB per domain. Only URLs (not code) are stored for modules.

+
+ + +
+

🔒 Security

+
+ + + + + + + + +
FeatureStatus
Input Validation✅
Output Escaping✅
No eval() on User Input✅
Path Traversal Prevention✅
Sandboxed File System✅
AI Destructive Confirmations✅ Required + timeout
Iframe Preview Sandbox✅ allow-same-origin allow-scripts
+
+ ⚠️ Module Loading + Only load modules from trusted sources — they have full terminal access. +
+
+ + +
+

🔧 Troubleshooting

+
+ + + + + + + + +
IssueSolution
AI not respondingRun ollamaconnect first, ensure Ollama is running
Voice not workingChrome/Edge/Safari only. Grant mic permission
Module fails to loadCheck CORS on the hosting server
Data lost after refreshCheck localStorage is enabled. Private mode clears data
AI actions not confirmingConfirm bar auto-denies after 120s. Type ai-allow
Sound not playingBrowser may need first user gesture. Toggle off/on
Ollama CORS errorOLLAMA_ORIGINS="*" ollama serve
+

Debug

+
# Console
+F12 → Console
+
+# View AI state
+ai-system
+ai-context
+
+# Module status
+listmods
+
+ + +
+

⚡ Quick Reference

+
+
WebX11 v4.2
+
+
helpShow all commands
+
clsClear terminal
+
llList files
+
cd <dir>Change directory
+
cat <f>View file
+
edit <f>Edit file
+
ai <msg>Chat with AI
+
search <q>Web search
+
find <q>File search
+
diff a bCompare files
+
ollamaconnectConnect AI
+
ai-allowAllow destructive
+
ai-denyDeny destructive
+
modulesModule Manager
+
skill-listList skills
+
aliasList aliases
+
previewHTML preview
+
uploadUpload file
+
+
+
+ + +
+

📋 Changelog

+ +
+
v4.2 — May 20262026-05-25
+
    +
  • Editor — edit <path> opens inline VFS editor modal
  • +
  • Diff — diff a b LCS-based comparison + diff --module
  • +
  • Aliases — ll, h, cls defaults + custom alias definition
  • +
  • File Finder — find <query> recursive VFS search
  • +
  • Agent loop: capped at 5 turns (was 1), edit_module destructive confirmation, model auto-validation
  • +
  • ai-stop now cancels chat + stream, not just tool calls
  • +
  • Unknown commands awaited, "Command not found" suppressed on AI route
  • +
  • loopResult scope fix — no more undefined error on AI fail
  • +
  • Preview iframe allows JS (allow-scripts), cache bump v=18
  • +
  • Splash fallback + startup modal list all 12 ext modules
  • +
+
+ +
+
v4.12026-05-23
+
    +
  • AI Agent with full tool-calling loop (15+ tools)
  • +
  • Inline destructive confirmation bar with 120s countdown timer
  • +
  • Skills system (8 CLI commands + UI modal)
  • +
  • Voice input (🎤 button, SpeechRecognition, show/hide toggle)
  • +
  • Sound notification (🔊 toggle, WebAudio ding)
  • +
  • AI spinner in status bar
  • +
  • Session system (save/load/recent/stats)
  • +
  • WebDAV sync (8 commands)
  • +
  • Web search (SearXNG + DDG + URL fetch/extract)
  • +
  • Config manager (6 commands + UI)
  • +
  • Startup modules selector (⚙ gear on splash)
  • +
  • Code editor modal (module editing, save & test live)
  • +
  • HTML preview via srcdoc (mobile)
  • +
  • File/dir autocomplete, pipe support
  • +
  • Smart context profiles, dynamic prompts with variables
  • +
  • Server monitor, ChatStudio overlay
  • +
  • Modular architecture: 23 JS modules, async loader
  • +
+
+ +
+
v4.02026-05-20
+
    +
  • Monolith refactored into core/ext modules
  • +
  • Async module loader with splash screen
  • +
  • Config manager modal
  • +
  • Bootloader fix for disabled modules
  • +
+
+
+
+ + + +↑ + + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..f1f315e --- /dev/null +++ b/index.html @@ -0,0 +1,810 @@ + + + + + + + WebX11 Module System v4.2 + + + +
+
+ WebX11 + v4.2 +
+
Mem: --
+
+ +
+
WebX11 Module System v4.2 - Unix-like Terminal
+
Commands: ls, cd, pwd, cat, echo, rm, mkdir, mv, cp, touch, tree, find, grep
+
Type 'help' for all commands
+
+
Loading modules...
+
+ + + + +
+ + + + + + + + + +
+ +
+ $ + + +
+
+ +
+ Ready + 📶 Online + +
+ + + + + + + + + + + +
+ ⚙ +
⚡ WebX11
+
v4.2
+
+
Initializing...
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/index.html.pre-refactor b/index.html.pre-refactor new file mode 100644 index 0000000..c9779c9 --- /dev/null +++ b/index.html.pre-refactor @@ -0,0 +1,35 @@ + + // Initialize the modular system + const webx11 = new WebX11Terminal(); + window.webx11 = webx11; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/core/module-aiasm.js b/modules/core/module-aiasm.js new file mode 100644 index 0000000..60e1757 --- /dev/null +++ b/modules/core/module-aiasm.js @@ -0,0 +1,26 @@ +// Extracted from newOS4 index.html — WebXAIAliasModule + class WebXAIAliasModule { + constructor() { this.commands = {}; this.terminal = null; } + getHelpInfo() { + return { + 'webxai': 'webxai — Send a question to the AI assistant', + 'wx': 'wx — Alias for webxai', + 'webx': 'webx — Alias for webxai' + }; + } + registerCommands() { + this.commands['webxai'] = (args) => this.forwardToAI(args); + this.commands['wx'] = (args) => this.forwardToAI(args); + this.commands['webx'] = (args) => this.forwardToAI(args); + } + forwardToAI(args) { + if (!args[0]) { + this.terminal.print("Usage: webxai ", "warning"); return; + } + if (this.terminal.moduleManager.modules['WebX11AIAssistantModule']) { + this.terminal.moduleManager.modules['WebX11AIAssistantModule'].chat(args); + } else { + this.terminal.ollamaManager.chat(args.join(' ')); + } + } + } diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js new file mode 100644 index 0000000..0ef6a2f --- /dev/null +++ b/modules/core/module-aiassist.js @@ -0,0 +1,1435 @@ +// Extracted from newOS4 index.html — WebX11AIAssistantModule (Full Agent Loop) + class WebX11AIAssistantModule { + constructor() { + this.commands = {}; this.terminal = null; + this.systemPrompt = this.buildSystemPrompt(); + this.context = []; this.maxContext = 10; + this.maxContextTokens = 2000; + this.MAX_TOOL_TURNS = 8; + this._pendingConfirmation = null; + this._confirmResolve = null; + this._errorLog = []; + this._commandFreq = {}; + this._messageCount = 0; + this._userProfile = {}; + this._sessionMemoryLoaded = false; + this._sessionSaved = false; + this._thinkingTimer = null; + this._thinkingStartTime = 0; + this._toolDefs = null; + try { const s = localStorage.getItem('webx11_ai_patterns'); if (s) this._commandFreq = JSON.parse(s); } catch (e) {} + try { const p = localStorage.getItem('webx11_user_profile'); if (p) this._userProfile = JSON.parse(p); } catch (e) {} + // Restore AI conversation context from localStorage + try { + const saved = localStorage.getItem('webx11_ai_context'); + if (saved) { + const parsed = JSON.parse(saved); + if (parsed && Array.isArray(parsed.context)) { + this.context = parsed.context; + this._messageCount = parsed.messageCount || 0; + } + } + } catch (e) {} + } + buildSystemPrompt() { + return `You are WebX11AI, the AI assistant embedded in the WebX11 browser terminal. You have direct access to read/write files, search the web, install packages, edit modules, and run terminal commands — all via tool calls. Use tools proactively. Keep responses concise. + +== TOOL REFERENCE == +You have these tools available. Use them WITHOUT asking permission unless the tool description says (DESTRUCTIVE): + +FILESYSTEM: +- list_files(path?) — List files in VFS directory (omit for current dir) +- read_file(path) — Read file content (truncated past 8000 chars) +- write_file(path, content) — Write to file (DESTRUCTIVE — confirm with user first) +- make_directory(path) — Create a directory (DESTRUCTIVE — confirm first) +- delete_file(path) — Delete file (DESTRUCTIVE — confirm first) +- get_current_dir — Show current working directory + +SYSTEM: +- get_status — Show filesystem, modules, Ollama, memory status +- get_help — List all available terminal commands +- run_command(command) — Execute any terminal command and capture output +- font-size <8-32> — Change terminal font size (persists in localStorage) + +SEARCH: +- web_search(query) — Search the web via proxy API (SearXNG + DDG fallback, results with title/url/snippet) +- web_fetch(url) — Fetch and extract readable content from a URL via proxy API (returns title + clean text + word count) + +PACKAGES: +- wpm_install(package) — Install WPM package by name (e.g. "widget", "calc") +- wpm_search(keyword) — Search available WPM packages + +SKILLS (PERSISTENT PROCEDURAL MEMORY): +- list_skills(category?) — Browse available skills, optionally filtered by category +- load_skills(query) — Search skills relevant to current task. Call at START of every task. +- create_skill(name, description, content, tags, category) — Save a new stored procedure. Call after completing complex multi-step tasks (3+ tool calls). +- patch_skill(name, content) — Update an existing skill when the approach changes + +MODULE SELF-IMPROVEMENT: +- view_module(name) — View source code of any loaded module +- edit_module(name) — Copy module into editor. Opens editor for modifications. +- test_module(name) — Load edited module from /_mods/ for live testing +- reload_module(name) — Discard edits and reload from original URL +- list_modified_modules — List modules with local edits pending +- get_module_status — Full status of all modules (loaded, modified, URLs) + +== SKILLS WORKFLOW == +1. At the START of every task, call load_skills(query) to check if a stored procedure exists +2. Follow the skill's steps. If they're wrong, call patch_skill to fix them. +3. After a complex multi-step task (3+ tool calls), call create_skill to save the approach +4. Skills persist in /skills/ as markdown files with YAML frontmatter + +== MEMORY SYSTEM == +- I maintain a conversation context (this.context) that persists across messages in this session +- The last ${this.maxContext} exchanges are remembered +- Type "ai-clear" to reset conversation memory +- Type "ai-context" to see what I remember +- Type "ai-model" to see/switch models +- Type "ai-stop" to cancel a running AI request +- Smart context profiles (set via "context ") adjust my temperature and system prompt style +- Saved prompts (via "prompt", "prompt-save") let you reuse template with {variable} substitution + +== USER PROFILE == +- Use user_get, user_set to read/write persistent user preferences across sessions +- user_set dot-notation supported: user_set knownFacts.name "Alex" +- Profile auto-stores known facts (name, project, preferences) +- Use session_save after complex tasks to log what was done +- Use session_load to review past sessions +- Use session_recent at start of session to remember what was worked on before + +== SELF-IMPROVEMENT WORKFLOW == +1. User asks for module changes → call get_module_status to list modules +2. Call view_module(name) to read the module source +3. Call edit_module(name) to copy source into editor for modification +4. After editing, call test_module(name) to load and test the edit +5. If it works, the user keeps it. If not, call reload_module(name). + +== AUTO-IMPROVEMENT (SYSTEM-DRIVEN) == +- If I encounter the same tool error 3+ times, a workaround skill is created automatically +- After complex tasks (3+ tool calls), I run health checks and suggest improvements +- I track conversation patterns and surface frequent topics +- Every 20 messages, skills are audited for staleness +- Use check_updates to verify module source URLs are reachable + +== RESPONSE STYLE == +- Be concise. Use bullet points for lists. +- When using tools, explain what you're doing briefly. +- If a tool fails, try an alternative approach. +- For web research tasks: search first, then analyze the results, then present findings. +|- When the user says "update" or "improve" without specifics, use get_status + get_help to understand the current state first. + +== REACT PROTOCOL == +For every task, follow this cycle: +1. **REASON** — Think about what you need next. What information is missing? +2. **ACT** — Call ONE tool at a time (unless multiple independent lookups are needed). +3. **OBSERVE** — Study the tool result carefully. Did it succeed? Is the data useful? +4. **DECIDE** — Do you have enough to answer? If not, go back to step 1. + +RULES: +- Call tools in sequence when the second depends on the first result +- Independent lookups (e.g. reading two unrelated files) can run in parallel +- If a tool returns an error, try a different approach before giving up +- After 6 tool turns, start synthesizing a final answer +- Always provide a complete, helpful final answer — don't leave the user hanging`; + } + getToolDefinitions() { + if (!this._toolDefs) this._toolDefs = this._buildToolDefs(); + return this._toolDefs; + } + _buildToolDefs() { + return [{ + type: 'function', + function: { + name: 'list_files', + description: 'List files and directories in the current or specified virtual filesystem directory', + parameters: { type: 'object', properties: { path: { type: 'string', description: 'Directory path to list, e.g. "" for root, "notes" for notes dir' } }, required: [] } + } + }, { + type: 'function', + function: { + name: 'read_file', + description: 'Read contents of a file from the virtual filesystem', + parameters: { type: 'object', properties: { path: { type: 'string', description: 'File path to read, e.g. "hello.txt" or "notes/todo.md"' } }, required: ['path'] } + } + }, { + type: 'function', + function: { + name: 'write_file', + description: 'Write content to a file in the virtual filesystem (DESTRUCTIVE)', + parameters: { type: 'object', properties: { path: { type: 'string', description: 'File path to write' }, content: { type: 'string', description: 'Content to write' } }, required: ['path', 'content'] } + } + }, { + type: 'function', + function: { + name: 'make_directory', + description: 'Create a new directory in the virtual filesystem', + parameters: { type: 'object', properties: { path: { type: 'string', description: 'Directory name to create' } }, required: ['path'] } + } + }, { + type: 'function', + function: { + name: 'delete_file', + description: 'Delete a file or empty directory from the virtual filesystem (DESTRUCTIVE)', + parameters: { type: 'object', properties: { path: { type: 'string', description: 'Path to delete' } }, required: ['path'] } + } + }, { + type: 'function', + function: { + name: 'get_status', + description: 'Get system status: filesystem, modules, Ollama connection, memory', + parameters: { type: 'object', properties: {}, required: [] } + } + }, { + type: 'function', + function: { + name: 'get_help', + description: 'Get full command reference for WebX11', + parameters: { type: 'object', properties: {}, required: [] } + } + }, { + type: 'function', + function: { + name: 'web_search', + description: 'Search the web using the WebX11 proxy API (SearXNG + DDG fallback). Returns structured results with title, URL, and content snippet.', + parameters: { type: 'object', properties: { query: { type: 'string', description: 'Search query' } }, required: ['query'] } + } + }, { + type: 'function', + function: { + name: 'web_fetch', + description: 'Fetch and extract clean readable content from a URL using the WebX11 proxy API. Returns title, clean text content, and word count. Use for reading articles, documentation, or web pages.', + parameters: { type: 'object', properties: { url: { type: 'string', description: 'Full URL to fetch' } }, required: ['url'] } + } + }, { + type: 'function', + function: { + name: 'wpm_install', + description: 'Install a WebX11 package from the package index', + parameters: { type: 'object', properties: { package: { type: 'string', description: 'Package name to install, e.g. "widget" or "calc"' } }, required: ['package'] } + } + }, { + type: 'function', + function: { + name: 'wpm_search', + description: 'Search available WebX11 packages by keyword', + parameters: { type: 'object', properties: { keyword: { type: 'string', description: 'Search keyword' } }, required: ['keyword'] } + } + }, { + type: 'function', + function: { + name: 'run_command', + description: 'Execute any WebX11 terminal command directly and return its output', + parameters: { type: 'object', properties: { command: { type: 'string', description: 'Full command to execute, e.g. "grep error" or "wc -l"' } }, required: ['command'] } + } + }, { + type: 'function', + function: { + name: 'get_current_dir', + description: 'Get current working directory path in virtual filesystem', + parameters: { type: 'object', properties: {}, required: [] } + } + }, { + type: 'function', + function: { + name: 'view_module', + description: 'View the source code of any loaded WebX11 module', + parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name, e.g. ThemeSwitcherModule' } }, required: ['name'] } + } + }, { + type: 'function', + function: { + name: 'edit_module', + description: 'Copy a module source into the editor for modification (DESTRUCTIVE)', + parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name to edit' } }, required: ['name'] } + } + }, { + type: 'function', + function: { + name: 'test_module', + description: 'Load an edited module from /_mods/ for live testing', + parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] } + } + }, { + type: 'function', + function: { + name: 'reload_module', + description: 'Reload a module from its original URL, discarding local edits', + parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] } + } + }, { + type: 'function', + function: { + name: 'list_modified_modules', + description: 'List which modules have local edits pending test', + parameters: { type: 'object', properties: {}, required: [] } + } + }, { + type: 'function', + function: { + name: 'get_module_status', + description: 'Full status of all modules: loaded, modified, URLs', + parameters: { type: 'object', properties: {}, required: [] } + } + }, { + type: 'function', + function: { + name: 'load_skills', + description: 'Search available skills by relevance to a query. Skills are stored procedures with step-by-step approaches for recurring tasks. Returns matching skill content to help you work efficiently.', + parameters: { type: 'object', properties: { query: { type: 'string', description: 'Search query describing the task — e.g. "search the web" or "organize files"' } }, required: ['query'] } + } + }, { + type: 'function', + function: { + name: 'create_skill', + description: 'Save a new skill (stored procedure) so you can reuse this approach next time. Call after completing a complex multi-step task. Skills persist across sessions.', + parameters: { type: 'object', properties: { + name: { type: 'string', description: 'Short kebab-case name, e.g. "web-scrape-and-summarize"' }, + description: { type: 'string', description: 'One-line description of what this skill does' }, + content: { type: 'string', description: 'Markdown content with YAML frontmatter. Must start with "---\\nname: \\n..."' }, + tags: { type: 'string', description: 'Comma-separated tags, e.g. "search,web,scraping"' }, + category: { type: 'string', description: 'Category, e.g. "research", "system", "dev"' } + }, required: ['name', 'description', 'content'] } + } + }, { + type: 'function', + function: { + name: 'patch_skill', + description: 'Update an existing skill file. Use when a stored procedure is outdated or has better steps. Replaces the entire skill content.', + parameters: { type: 'object', properties: { + name: { type: 'string', description: 'Skill name to patch (without .md)' }, + content: { type: 'string', description: 'Complete new markdown content including YAML frontmatter' } + }, required: ['name', 'content'] } + } + }, { + type: 'function', + function: { + name: 'list_skills', + description: 'List all available skills. Optionally filter by category.', + parameters: { type: 'object', properties: { category: { type: 'string', description: 'Optional category filter, e.g. "research" or "system"' } }, required: [] } + } + }, { + type: 'function', + function: { + name: 'check_updates', + description: 'Check if loaded modules are reachable at their source URLs. Reports any that appear outdated or unreachable.', + parameters: { type: 'object', properties: {}, required: [] } + } + }, { + type: 'function', + function: { + name: 'user_get', + description: 'Read a value from the persistent user profile. Returns null if key not found.', + parameters: { type: 'object', properties: { key: { type: 'string', description: 'Profile key to read, e.g. "preferredModel", "fontSize", "knownFacts.name"' } }, required: ['key'] } + } + }, { + type: 'function', + function: { + name: 'user_set', + description: 'Write a value to the persistent user profile. Saves across sessions.', + parameters: { type: 'object', properties: { key: { type: 'string', description: 'Profile key, e.g. "preferredModel"' }, value: { type: 'string', description: 'Value to store' } }, required: ['key', 'value'] } + } + }, { + type: 'function', + function: { + name: 'user_list', + description: 'List all stored keys in the user profile.', + parameters: { type: 'object', properties: {}, required: [] } + } + }, { + type: 'function', + function: { + name: 'user_forget', + description: 'Delete a key from the user profile.', + parameters: { type: 'object', properties: { key: { type: 'string', description: 'Key to delete' } }, required: ['key'] } + } + }, { + type: 'function', + function: { + name: 'session_save', + description: 'Save a summary of the current session to /memory/. Use after complex tasks. Creates a daily log file.', + parameters: { type: 'object', properties: { summary: { type: 'string', description: 'Brief summary of what was accomplished this session' } }, required: ['summary'] } + } + }, { + type: 'function', + function: { + name: 'session_load', + description: 'Load a past session log by date. Format: YYYY-MM-DD.', + parameters: { type: 'object', properties: { date: { type: 'string', description: 'Date string like "2026-05-23"' } }, required: ['date'] } + } + }, { + type: 'function', + function: { + name: 'session_recent', + description: 'Show summaries from the most recent sessions.', + parameters: { type: 'object', properties: { count: { type: 'number', description: 'Number of recent sessions to show (default 3)' } }, required: [] } + } + }, { + type: 'function', + function: { + name: 'session_stats', + description: 'Aggregate statistics across all sessions: total tasks, most-used tools, common topics.', + parameters: { type: 'object', properties: {}, required: [] } + } + }]; + } + getHelpInfo() { + return { + 'ai': 'ai — Ask the AI assistant anything', + 'ask': 'ask — Alias for ai command', + 'ai-clear': 'ai-clear — Clear conversation context', + 'ai-system': 'ai-system — Set system prompt', + 'ai-context': 'ai-context — Show current conversation context', + 'ai-allow': 'ai-allow — Confirm pending AI action', + 'ai-deny': 'ai-deny — Deny pending AI action', + 'ai-stop': 'ai-stop — Cancel current AI operation', + 'ai-model': 'ai-model [model] — Show or set AI model' + }; + } + registerCommands() { + this.commands['ai'] = (args) => this.chat(args); + this.commands['ask'] = (args) => this.chat(args); + this.commands['ai-clear'] = () => this.clearContext(); + this.commands['ai-system'] = (args) => this.setSystemPrompt(args); + this.commands['ai-context'] = () => this.showContext(); + this.commands['ai-allow'] = () => this.confirmPending(); + this.commands['ai-deny'] = () => this.denyPending(); + this.commands['ai-stop'] = () => this.cancelAi(); + this.commands['ai-model'] = (args) => this.showModel(args); + this.commands['ai-compress'] = () => this._manualCompress(); + } + async chat(args) { + const userMessage = args.join(' '); + if (!userMessage) { + this.terminal.print('Usage: ai ', 'warning'); return; + } + this._cancelled = false; + this.terminal.setAiBusy(true); + while (this._chatLock) await new Promise(r => setTimeout(r, 50)); + this._chatLock = true; + this.terminal.markDirty(); + // Re-read context from localStorage (auto-restore may have written it after init) + this._loadContext(); + // Compress context if it exceeds the token threshold + this._compressContext(); + let loopResult; + try { + const messages = [ + { role: 'system', content: this.systemPrompt } + ]; + this.context.slice().forEach(msg => messages.push(msg)); + messages.push({ role: 'user', content: userMessage }); + this.terminal.print('🤖 WebX11AI thinking...', 'info'); + // Ensure skills directory exists and seed defaults on first run + this.terminal.fs.mkdirRecursive('/skills'); + if (!this.terminal.fs.readFile('/skills/_seeded')) { + try { + await this._seedDefaultSkills(); + } catch (e) { /* non-critical */ } + } + // Auto-load skills relevant to the user's query (awaited, not fire-and-forget) + // Skip skill loading for trivial queries (< 10 chars, no actionable keywords) + const isTrivial = userMessage.length < 10 && !/\b(search|write|install|edit|create|make|find|delete|remove|change|update|improve|add|fix|build|set|load|save|help)\b/i.test(userMessage); + if (!isTrivial) { + try { + const skillRes = await this.runTool('load_skills', { query: userMessage }); + const skillData = JSON.parse(skillRes); + if (skillData && skillData.matches && skillData.matches.length > 0) { + const skillNotes = skillData.matches.map(m => `[Skill: ${m.name}] ${m.content}`).join('\n\n---\n\n'); + messages.push({ role: 'system', content: `Relevant skills for this task:\n\n${skillNotes}\n\nUse the steps above as guidance. If they're wrong or outdated, use patch_skill to fix them after the task.` }); + this.terminal.print(`📚 Loaded ${skillData.count} relevant skill(s)`, 'info'); + } + } catch (e) { /* skills not available yet — first run */ } + } + + // Track conversation patternsfor learning + this._trackPattern(userMessage); + + // Extract facts from user message + this._extractFacts(userMessage, ''); + + // On first message of session, auto-load recent memory + if (!this._sessionMemoryLoaded) { + this._sessionMemoryLoaded = true; + this.terminal.fs.mkdirRecursive('/memory'); + try { + const recentRes = await this.runTool('session_recent', { count: 3 }); + const rd = JSON.parse(recentRes); + if (rd.sessions && rd.sessions.length > 0) { + const memNotes = rd.sessions.map(s => '[' + s.date + '] ' + (s.tasks || 1) + ' tasks: ' + (s.summary || 'No summary')).join('\n'); + messages.push({ role: 'system', content: 'Recent sessions:\n' + memNotes + '\nThis provides continuity. Refer to past work when relevant.' }); + this.terminal.print('📖 Loaded ' + rd.sessions.length + ' recent session(s)', 'info'); + } + } catch (e) {} + } + + loopResult = await this.agenticLoop(messages); + this.context.push({ role: 'user', content: userMessage }); + if (loopResult.finalAnswer) { + this.context.push({ role: 'assistant', content: loopResult.finalAnswer }); + } + if (this.context.length > this.maxContext * 2) { + this.context = this.context.slice(-this.maxContext * 2); + } + // Reflection: after complex tasks, run health scan and auto-skill creation + if (loopResult.toolTurns >= 2 && !this._cancelled) { + await this.afterActionReflection(userMessage, loopResult.toolTurns); + } + // Auto-create skills from repeated errors + if (this._errorLog.length >= 3) { + await this._autoCreateErrorSkill(); + } + // Skill quality audit every 20 messages + this._messageCount++; + if (this._messageCount % 20 === 0) { + await this._auditSkills(); + } + // Sound notification on AI response + if (this.terminal.playNotificationSound) { + setTimeout(() => this.terminal.playNotificationSound(), 100); + } + } finally { + this._chatLock = false; + this.terminal.setAiBusy(false); + // Persist conversation context after each exchange + this._saveContext(); + // Log the AI interaction + this.terminal.log('INFO', 'AI', 'Query: ' + userMessage.substring(0, 100)); + if (loopResult && loopResult.toolTurns) { + this.terminal.log('INFO', 'AI', 'Used ' + loopResult.toolTurns + ' tool(s)'); + } + } + // Register session save on page unload (runs once per session) + if (!this._sessionSaved) { + this._sessionSaved = true; + window.addEventListener('beforeunload', () => { + this.terminal.fs.mkdirRecursive('/memory'); + const today = new Date().toISOString().split('T')[0]; + const index = this.terminal.fs.fsReadJSON('/memory/index.json') || {}; + index[today] = index[today] || { summary: 'Session in progress', tasks: 0 }; + index[today].tasks = (index[today].tasks || 0) + 1; + index[today].updated = new Date().toISOString(); + this.terminal.fs.fsWriteJSON('/memory/index.json', index); + // Also persist AI context on page close + this._saveContext(); + }); + } + } + _summarizeObservation(result, maxLen) { + if (!result || typeof result !== 'string' || result.length <= maxLen) return result || ''; + const lines = result.split('\n'); + if (lines.length > 5) { + return lines.slice(0, 2).join('\n') + '\n...(' + (lines.length - 4) + ' more lines)...\n' + lines.slice(-2).join('\n'); + } + return result.substring(0, maxLen) + '...(truncated from ' + result.length + ' chars)'; + } + _isToolError(result) { + if (!result || typeof result !== 'string') return null; + const trimmed = result.trim(); + if (trimmed.startsWith('{"error":') || trimmed.startsWith("{'error':")) { + try { const parsed = JSON.parse(trimmed); return parsed.error || null; } + catch (e) { return null; } + } + return null; + } + async agenticLoop(messages) { + let toolTurns = 0; + while (toolTurns < this.MAX_TOOL_TURNS) { + if (this._cancelled) { this.terminal.print('AI loop cancelled', 'warning'); return { finalAnswer: null, toolTurns }; } + toolTurns++; + // On last turn, inject wrap-up prompt + if (toolTurns === this.MAX_TOOL_TURNS) { + this.terminal.print(`⏱ Final turn (${toolTurns}/${this.MAX_TOOL_TURNS}) — AI must provide final answer`, 'warning'); + messages.push({ role: 'system', content: 'This is your last tool turn. You MUST provide a final answer after this tool call. Synthesize what you know.' }); + } + const thinkStart = Date.now(); + const res = await this.terminal.ollamaManager.chatWithTools(messages, this.getToolDefinitions()); + const thinkTime = Math.round((Date.now() - thinkStart) / 1000); + if (this._cancelled) return { finalAnswer: null, toolTurns }; + if (res.error) { this.terminal.print(`⚠️ AI model error (${thinkTime}s): ${res.error}`, 'error'); return { finalAnswer: null, toolTurns }; } + if (res.message && !res.tool_calls) { + const text = res.message.content || ''; + this.terminal.printAIResponse(text || '(no response)'); + if (text) this.terminal.ollamaManager.fireResponseHooks(text); + this.terminal.print(`💬 AI answered (turn ${toolTurns}/${this.MAX_TOOL_TURNS}, ${thinkTime}s)`, 'info'); + return { finalAnswer: text, toolTurns }; + } + if (res.tool_calls && res.tool_calls.length > 0) { + this.terminal.print(`🔧 AI wants ${res.tool_calls.length} tool(s) — turn ${toolTurns}/${this.MAX_TOOL_TURNS} (${thinkTime}s)`, 'info'); + messages.push(res.message); + let pendingText = res.message?.content || null; + const destructiveTools = res.tool_calls.filter(tc => this.isDestructive(tc.function.name)); + const safeTools = res.tool_calls.filter(tc => !this.isDestructive(tc.function.name)); + // Execute non-destructive tools in parallel (independent read-only operations) + const safeResults = await Promise.all(safeTools.map(async (tc) => { + const fn = tc.function; + const t0 = Date.now(); + const toolResult = await this.runTool(fn.name, fn.arguments); + const t1 = Math.round((Date.now() - t0) / 1000); + return { fn, toolResult, t1 }; + })); + for (const { fn, toolResult, t1 } of safeResults) { + this.terminal.print(`🔧 Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info'); + const obs = this._summarizeObservation(toolResult, 150); + const err = this._isToolError(toolResult); + if (err) { + this.terminal.print(`📊 Result (${t1}s): ⚠️ Error: ${err}`, 'warning'); + } else { + this.terminal.print(`📊 Result (${t1}s): ${obs}`, 'info'); + } + messages.push({ role: 'tool', content: toolResult }); + } + // Only block on destructive tools + if (destructiveTools.length > 0) { + const approved = await this.confirmActionMulti(destructiveTools); + if (approved) { + for (const tc of destructiveTools) { + const fn = tc.function; + const t0 = Date.now(); + this.terminal.print(`🔧 Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info'); + const toolResult = await this.runTool(fn.name, fn.arguments); + const t1 = Math.round((Date.now() - t0) / 1000); + const dObs = this._summarizeObservation(toolResult, 150); + const dErr = this._isToolError(toolResult); + if (dErr) { + this.terminal.print(`📊 Result (${t1}s): ⚠️ Error: ${dErr}`, 'warning'); + } else { + this.terminal.print(`📊 Result (${t1}s): ${dObs}`, 'info'); + } + messages.push({ role: 'tool', content: toolResult }); + } + } else { + messages.push({ + role: 'tool', + content: `Destructive actions (${destructiveTools.map(tc => tc.function.name).join(', ')}) denied by user. Non-destructive actions already executed.` + }); + } + } + if (pendingText) { + messages.push({ role: 'assistant', content: pendingText }); + } + continue; + } + return { finalAnswer: null, toolTurns }; + } + // Loop exited without final answer — auto-summary + const toolNames = Object.keys(this._commandFreq).slice(-3).join(', '); + this.terminal.print(`🤖 AI reached the ${toolTurns}/${this.MAX_TOOL_TURNS} tool limit.`, 'warning'); + this.terminal.print('Tools were executed but no final answer was produced — try a more specific query.', 'info'); + return { finalAnswer: null, toolTurns }; + } + async afterActionReflection(query, toolTurns) { + // Debounce: only suggest skill creation once per 30 minutes + const now = Date.now(); + if (this._lastSkillSuggestion && (now - this._lastSkillSuggestion) < 1800000) { + return; + } + const suggestions = []; + const fs = this.terminal.fs; + try { + // Batch: read all status info in one VFS pass + const reg = this.terminal.moduleManager.moduleRegistry || []; + const total = reg.length; + const loaded = Object.keys(this.terminal.moduleManager.modules).length; + if (loaded < total) { + suggestions.push(loaded + '/' + total + ' modules loaded — use loadmod to restore'); + } + } catch (e) {} + try { + const skillIdx = fs.fsReadJSON('/skills/index.json'); + const skillCount = skillIdx ? Object.keys(skillIdx).length : 0; + if (skillCount === 0 && toolTurns >= 4) { + suggestions.push('Save this approach as a skill with create_skill'); + this._lastSkillSuggestion = now; + } + } catch (e) {} + try { + const modsDir = fs.getCurrentDir(); + const mc = modsDir.content['_mods']; + const modCount = mc && mc.content ? Object.keys(mc.content).filter(k => k.endsWith('.js') && !k.endsWith('.orig.js')).length : 0; + if (modCount > 0) { + suggestions.push(modCount + ' pending module edit(s) in /_mods/ — use test_module to verify'); + } + } catch (e) {} + try { + if (performance.memory && performance.memory.usedJSHeapSize > 100 * 1048576) { + suggestions.push('High memory (' + (performance.memory.usedJSHeapSize / 1048576).toFixed(0) + 'MB) — consider clearing terminal'); + } + } catch (e) {} + if (toolTurns >= 4 && suggestions.length === 0) { + suggestions.push('Used ' + toolTurns + ' tools — create a skill to save this workflow'); + this._lastSkillSuggestion = now; + } + if (suggestions.length > 0) { + this.terminal.print('💡 ' + suggestions.join(' | '), 'info'); + } + } + async confirmActionMulti(toolCalls) { + const names = toolCalls.map(tc => tc.function.name).join(', '); + const args = toolCalls.map(tc => typeof tc.function.arguments === 'string' ? tc.function.arguments : JSON.stringify(tc.function.arguments)).join(' | '); + const truncatedArgs = args.length > 60 ? args.substring(0, 60) + '...' : args; + this.terminal.printHTML(`⚠️ AI wants to: ${names}(${truncatedArgs}). ai-allow to confirm, ai-deny to refuse. (Auto-denies in 120s)`); + // Show confirm bar OUTSIDE scrollable output (always visible) + const area = document.getElementById('ai-confirm-area'); + if (!area) { + // Fallback to promise resolve + return true; + } + area.innerHTML = ` + ⚠️ ${names}(${truncatedArgs}) + + + + Type: ai-allow or ai-deny + `; + area.classList.add('visible'); + this._confirmArea = area; + this._confirmBar = area; + + // Attach button handlers via both addEventListener + DOM property (inline onclick fallback) + const allowBtn = document.getElementById('ai-confirm-allow'); + const denyBtn = document.getElementById('ai-confirm-deny'); + const onAllow = () => { + this._resolveConfirm(true); + }; + const onDeny = () => { + this._resolveConfirm(false); + }; + allowBtn.addEventListener('click', onAllow); + denyBtn.addEventListener('click', onDeny); + allowBtn.__allow = onAllow; + denyBtn.__deny = onDeny; + + const startTime = Date.now(); + return new Promise((resolve) => { + this._pendingConfirmation = { names, args }; + this._confirmResolve = resolve; + this._confirmAllowBtn = allowBtn; + this._confirmDenyBtn = denyBtn; + this._confirmOnAllow = onAllow; + this._confirmOnDeny = onDeny; + // Countdown timer updates with escalating visibility + const timerInterval = setInterval(() => { + const elapsed = Math.round((Date.now() - startTime) / 1000); + const remaining = Math.max(0, 120 - elapsed); + const timerEl = document.getElementById('ai-confirm-timer'); + if (timerEl) { + timerEl.textContent = `⏱ ${remaining}s`; + timerEl.className = 'confirm-timer'; + if (remaining > 30) { + timerEl.style.color = 'var(--warning-color)'; + } else if (remaining > 10) { + timerEl.classList.add('urgent'); + } else { + timerEl.classList.add('critical'); + } + } + if (remaining === 60) { + this.terminal.print(`⏱ 60s remaining to respond to AI confirmation for: ${names}`, 'warning'); + } else if (remaining === 30) { + this.terminal.print(`⏱ 30s — ${names} will be auto-denied soon!`, 'warning'); + } else if (remaining === 10) { + this.terminal.print(`⏱ 10s remaining! Type ai-allow or click Allow now!`, 'warning'); + } + }, 500); + this._confirmTimerInterval = timerInterval; + setTimeout(() => { + clearInterval(timerInterval); + if (this._confirmResolve) { + this._confirmResolve(false); + this._cleanupConfirmBar(); + this.terminal.print('⏱ Confirmation timed out — tool not executed', 'warning'); + } + }, 120000); + }); + } + _resolveConfirm(approved) { + if (this._confirmResolve) { + this._confirmResolve(approved); + this._cleanupConfirmBar(); + if (approved) { + this.terminal.print('Action allowed', 'success'); + } else { + this.terminal.print('Action denied', 'warning'); + } + } + } + _cleanupConfirmBar() { + clearInterval(this._confirmTimerInterval); + this._confirmTimerInterval = null; + this._confirmResolve = null; + this._pendingConfirmation = null; + this._confirmAllowBtn = null; + this._confirmDenyBtn = null; + this._confirmOnAllow = null; + this._confirmOnDeny = null; + const area = document.getElementById('ai-confirm-area'); + if (area) { + area.classList.remove('visible'); + area.innerHTML = ''; + } + this._confirmArea = null; + this._confirmBar = null; + const timerEl = document.getElementById('ai-confirm-timer'); + if (timerEl) timerEl.style.animation = ''; + } + confirmPending() { + this._resolveConfirm(true); + } + denyPending() { + this._resolveConfirm(false); + } + cancelAi() { + this._cancelled = true; + this.terminal.print('⏹ Cancelling AI...', 'warning'); + // Abort any in-flight HTTP request + if (this.terminal.ollamaManager) { + this.terminal.ollamaManager.abort(); + } + // Clear any pending confirmation + if (this._confirmResolve) { + this._resolveConfirm(false); + } + // Immediately reset UI so user knows it stopped + this.terminal.setAiBusy(false); + } + isDestructive(name) { + return name === 'write_file' || name === 'delete_file' || name === 'run_command' || name === 'make_directory' || name === 'edit_module'; + } + async runTool(name, argsStr) { + try { + const a = typeof argsStr === 'object' ? argsStr : (argsStr ? JSON.parse(argsStr) : {}); + 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 = []; + } + if (p) { + const parts = p.split('/'); + const dir = this.terminal.fs.root; + let cur = dir; + 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 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 }))); + } + case 'read_file': { + const rp = a.path || ''; + const savedR = [...this.terminal.fs.currentPath]; + if (rp.startsWith('/')) this.terminal.fs.currentPath = []; + const path = rp.replace(/^\//, ''); + const content = this.terminal.fs.readFile(path); + this.terminal.fs.currentPath = savedR; + if (content === null) return JSON.stringify({ error: `File not found: ${path}` }); + return content.length > 8000 ? content.substring(0, 8000) + '\n...(truncated)' : content; + } + case 'write_file': { + const wp = a.path || ''; + const savedW = [...this.terminal.fs.currentPath]; + if (wp.startsWith('/')) this.terminal.fs.currentPath = []; + const path = wp.replace(/^\//, ''); + const content = a.content || ''; + this.terminal.fs.writeFile(path, content); + this.terminal.fs.currentPath = savedW; + return `File written: ${path} (${content.length} chars)`; + } + case 'make_directory': { + const mp = a.path || ''; + const savedM = [...this.terminal.fs.currentPath]; + if (mp.startsWith('/')) this.terminal.fs.currentPath = []; + const path = mp.replace(/^\//, ''); + const ok = this.terminal.fs.mkdir(path); + this.terminal.fs.currentPath = savedM; + if (ok) return `Directory created: ${path}`; + return JSON.stringify({ error: `Cannot create: ${path}` }); + } + case 'delete_file': { + const dp = a.path || ''; + const savedD = [...this.terminal.fs.currentPath]; + if (dp.startsWith('/')) this.terminal.fs.currentPath = []; + const path = dp.replace(/^\//, ''); + const delOk = this.terminal.fs.deleteFile(path); + this.terminal.fs.currentPath = savedD; + if (delOk) return `Deleted: ${path}`; + return JSON.stringify({ error: `Cannot delete: ${path}` }); + } + case 'get_status': { + const fs = this.terminal.fs; + const mm = this.terminal.moduleManager; + const om = this.terminal.ollamaManager; + return JSON.stringify({ + files: fs.ls().length, + dirs: fs.currentPath.length ? '/' + fs.currentPath.join('/') : '/', + modules: Object.keys(mm.modules).length, + ollama: om.connected ? `Connected (${om.currentModel})` : 'Disconnected', + memory: performance.memory ? (performance.memory.usedJSHeapSize / 1048576).toFixed(1) + 'MB' : 'N/A' + }); + } + case 'get_help': { + const cmds = this.terminal.moduleManager.getAllCommands(); + return JSON.stringify(Object.keys(cmds).sort()); + } + case 'get_current_dir': { + return '/' + this.terminal.fs.currentPath.join('/'); + } + case 'web_search': { + const query = a.query || ''; + if (!query) return JSON.stringify({ error: 'No query provided' }); + try { + const resp = await fetch('/api/search?q=' + encodeURIComponent(query)); + if (!resp.ok) { return JSON.stringify({ error: 'Search API returned HTTP ' + resp.status }); } + const data = await resp.json(); + const results = data.results || []; + if (results.length === 0) return JSON.stringify({ message: 'No results found' }); + return JSON.stringify({ count: results.length, source: data.source, results: results.map(r => ({ title: r.title, url: r.url, content: (r.content || '').substring(0, 300) })) }); + } catch (e) { return JSON.stringify({ error: 'Search failed: ' + e.message }); } + } + case 'web_fetch': { + const url = a.url || ''; + if (!url) return JSON.stringify({ error: 'No URL provided' }); + try { + const resp = await fetch('/api/extract?url=' + encodeURIComponent(url)); + if (resp.status === 403) return JSON.stringify({ error: 'URL blocked — internal network' }); + if (!resp.ok) { return JSON.stringify({ error: 'Fetch API returned HTTP ' + resp.status }); } + const data = await resp.json(); + return JSON.stringify({ title: data.title || '', content: (data.content || '').substring(0, 4000), word_count: data.word_count || 0, method: data.method || 'extract' }); + } catch (e) { return JSON.stringify({ error: 'Fetch failed: ' + e.message }); } + } + case 'wpm_install': { + const pkg = a.package || ''; + if (!pkg) return JSON.stringify({ error: 'No package name' }); + const mod = this.terminal.moduleManager.modules['WpmModule']; + if (mod) { await mod.install(pkg); return `Package "${pkg}" installation attempted`; } + return JSON.stringify({ error: 'WPM module not available' }); + } + case 'wpm_search': { + const kw = a.keyword || ''; + if (!kw) return JSON.stringify({ error: 'No keyword' }); + const mod = this.terminal.moduleManager.modules['WpmModule']; + if (mod) { + const originalPrint = this.terminal.print.bind(this.terminal); + let output = ''; + this.terminal.print = (text, type) => { output += text + '\n'; }; + try { await mod.search(kw); } finally { this.terminal.print = originalPrint; } + return output.trim() || 'Search completed'; + } + return JSON.stringify({ error: 'WPM module not available' }); + } + case 'run_command': { + const cmd = a.command || ''; + if (!cmd) return JSON.stringify({ error: 'No command provided' }); + const originalPrint = this.terminal.print.bind(this.terminal); + let output = ''; + this.terminal.print = (text, type) => { output += text + '\n'; }; + try { await this.terminal.dispatchCommand(cmd.split(' ')[0].toLowerCase(), cmd.split(' ').slice(1)); } finally { this.terminal.print = originalPrint; } + return output.trim() || '(no output)'; + } + case 'check_updates': { + const reg = this.terminal.moduleManager.moduleRegistry; + const results = []; + for (const mod of reg) { + if (mod.url && mod.url.startsWith('http')) { + try { + const resp = await fetch(mod.url, { method: 'HEAD' }); + results.push({ name: mod.name, url: mod.url, status: resp.ok ? 'reachable' : 'error (' + resp.status + ')' }); + } catch (e) { + results.push({ name: mod.name, url: mod.url, status: 'unreachable' }); + } + } + } + return JSON.stringify({ checked: results.length, modules: results }); + } + case 'user_get': { + const key = a.key || ''; + if (!key) return JSON.stringify({ error: 'Key is required' }); + const val = key.split('.').reduce((o, k) => (o && typeof o === 'object') ? o[k] : undefined, this._userProfile); + return JSON.stringify({ key, value: val !== undefined ? val : null }); + } + case 'user_set': { + const ukey = a.key || ''; + if (!ukey) return JSON.stringify({ error: 'Key and value are required' }); + const uval = a.value || ''; + const keys = ukey.split('.'); + let obj = this._userProfile; + for (let i = 0; i < keys.length - 1; i++) { + if (!obj[keys[i]] || typeof obj[keys[i]] !== 'object') obj[keys[i]] = {}; + obj = obj[keys[i]]; + } + obj[keys[keys.length - 1]] = uval; + try { localStorage.setItem('webx11_user_profile', JSON.stringify(this._userProfile)); } catch (e) {} + return JSON.stringify({ success: true, key: ukey, value: uval }); + } + case 'user_list': { + return JSON.stringify({ profile: this._userProfile, keys: Object.keys(this._userProfile) }); + } + case 'user_forget': { + const fkey = a.key || ''; + if (!fkey) return JSON.stringify({ error: 'Key is required' }); + if (fkey.includes('.')) { + const parts = fkey.split('.'); + let obj = this._userProfile; + for (let i = 0; i < parts.length - 1; i++) { + if (!obj[parts[i]]) return JSON.stringify({ error: 'Key not found' }); + obj = obj[parts[i]]; + } + delete obj[parts[parts.length - 1]]; + } else { + delete this._userProfile[fkey]; + } + try { localStorage.setItem('webx11_user_profile', JSON.stringify(this._userProfile)); } catch (e) {} + return JSON.stringify({ success: true, key: fkey }); + } + case 'session_save': { + const summary = a.summary || 'No summary provided'; + const fs = this.terminal.fs; + fs.mkdirRecursive('/memory'); + const today = new Date().toISOString().split('T')[0]; + const log = { date: today, summary, toolCallCount: 0, skillsCreated: [], modulesEdited: [], timestamp: new Date().toISOString() }; + fs.fsWriteJSON('/memory/' + today + '.json', log); + const index = fs.fsReadJSON('/memory/index.json') || {}; + index[today] = { summary, tasks: (index[today]?.tasks || 0) + 1, updated: new Date().toISOString() }; + fs.fsWriteJSON('/memory/index.json', index); + return JSON.stringify({ success: true, date: today }); + } + case 'session_load': { + const sdate = a.date || ''; + if (!sdate) return JSON.stringify({ error: 'Date required (YYYY-MM-DD)' }); + const sfs = this.terminal.fs; + const slog = sfs.fsReadJSON('/memory/' + sdate + '.json'); + if (!slog) return JSON.stringify({ error: 'No session log for ' + sdate }); + return JSON.stringify(slog); + } + case 'session_recent': { + const count = a.count || 3; + const rfs = this.terminal.fs; + const rindex = rfs.fsReadJSON('/memory/index.json') || {}; + const dates = Object.keys(rindex).sort().reverse().slice(0, count); + const sessions = dates.map(d => ({ date: d, ...rindex[d] })); + return JSON.stringify({ sessions, count: sessions.length }); + } + case 'session_stats': { + const sfs = this.terminal.fs; + const sindex = sfs.fsReadJSON('/memory/index.json') || {}; + const entries = Object.entries(sindex); + let totalTasks = 0; + for (const [, meta] of entries) totalTasks += meta.tasks || 0; + return JSON.stringify({ totalSessions: entries.length, totalTasks, dates: entries.map(([d]) => d) }); + } + case 'load_skills': { + const query = (a && a.query || '').toLowerCase(); + const fs = this.terminal.fs; + const index = fs.fsReadJSON('/skills/index.json') || {}; + if (!query) { + const names = Object.keys(index); + return JSON.stringify({ skills: names, count: names.length, message: 'No query provided — showing all skill names. Call again with a query to load content.' }); + } + const matches = []; + for (const [name, meta] of Object.entries(index)) { + const score = (name.includes(query) ? 3 : 0) + (meta.description.toLowerCase().includes(query) ? 2 : 0) + ((meta.tags || []).some(t => t.toLowerCase().includes(query)) ? 2 : 0); + if (score > 0) { + const content = fs.fsRead(`/skills/${name}.md`) || ''; + matches.push({ name, description: meta.description, category: meta.category, score, content: content.substring(0, 2000) }); + } + } + matches.sort((a, b) => b.score - a.score); + const top = matches.slice(0, 3); + return JSON.stringify(top.length > 0 ? { matches: top.map(m => ({ name: m.name, description: m.description, content: m.content })), count: top.length } : { matches: [], count: 0, message: 'No matching skills found. You can create one after this task.' }); + } + case 'create_skill': { + const name = (a.name || '').trim().replace(/\s+/g, '-').toLowerCase(); + if (!name) return JSON.stringify({ error: 'Skill name is required' }); + const description = a.description || ''; + let content = a.content || ''; + const tags = (a.tags || '').split(',').map(t => t.trim()).filter(Boolean); + const category = a.category || 'general'; + if (!content.startsWith('---')) { + content = `---\nname: ${name}\ndescription: ${description}\ntags: [${tags.join(', ')}]\ncategory: ${category}\n---\n## Steps\n${content}`; + } + const fs = this.terminal.fs; + fs.mkdirRecursive('/skills'); + fs.fsWrite(`/skills/${name}.md`, content); + const index = fs.fsReadJSON('/skills/index.json') || {}; + index[name] = { name, description, tags, category, created: index[name]?.created || new Date().toISOString(), updated: new Date().toISOString() }; + fs.fsWriteJSON('/skills/index.json', index); + this.terminal.print(`💡 Skill saved: ${name}`, 'success'); + return JSON.stringify({ success: true, name, message: `Skill "${name}" created (${content.length} chars)` }); + } + case 'patch_skill': { + const pname = (a.name || '').trim(); + if (!pname) return JSON.stringify({ error: 'Skill name is required' }); + const pcontent = a.content || ''; + if (!pcontent) return JSON.stringify({ error: 'Skill content is required' }); + const fs = this.terminal.fs; + const index = fs.fsReadJSON('/skills/index.json') || {}; + if (!index[pname]) return JSON.stringify({ error: `Skill "${pname}" not found` }); + fs.fsWrite(`/skills/${pname}.md`, pcontent); + index[pname].updated = new Date().toISOString(); + // Re-parse frontmatter for metadata updates + const metaMatch = pcontent.match(/^---\n([\s\S]*?)\n---/); + if (metaMatch) { + const metaLines = metaMatch[1].split('\n'); + metaLines.forEach(line => { + const [k, ...v] = line.split(':'); + if (k === 'description') index[pname].description = v.join(':').trim(); + if (k === 'tags') index[pname].tags = v.join(':').trim().replace(/[\[\]]/g, '').split(',').map(t => t.trim()); + if (k === 'category') index[pname].category = v.join(':').trim(); + }); + } + fs.fsWriteJSON('/skills/index.json', index); + this.terminal.print(`💡 Skill patched: ${pname}`, 'success'); + return JSON.stringify({ success: true, name: pname, message: `Skill "${pname}" updated` }); + } + case 'list_skills': { + const filterCat = (a.category || '').toLowerCase(); + const fs = this.terminal.fs; + const index = fs.fsReadJSON('/skills/index.json') || {}; + const list = Object.entries(index).map(([name, meta]) => ({ name, description: meta.description, category: meta.category || 'general', tags: meta.tags || [], updated: meta.updated })) + .filter(s => !filterCat || (s.category || '').toLowerCase() === filterCat); + return JSON.stringify({ skills: list, count: list.length }); + } + case 'view_module': + case 'edit_module': + case 'test_module': + case 'reload_module': + case 'list_modified_modules': + case 'get_module_status': + return await this.runModuleTool(name, argsStr); + default: + return JSON.stringify({ error: `Unknown tool: ${name}` }); + } + } catch (e) { + this._errorLog.push({ tool: name, args: a, error: e.message, timestamp: Date.now() }); + return JSON.stringify({ error: e.message, stack: e.stack?.substring(0, 200) }); + } + } + async runModuleTool(name, argsStr) { + const a = typeof argsStr === 'object' ? argsStr : (argsStr ? JSON.parse(argsStr) : {}); + switch (name) { + case 'view_module': { + const modName = a.name || ''; + const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName); + const code = this.terminal.fs.readFile(`_mods/${modName}.js`); + if (code) return code; + const url = entry ? entry.url : null; + if (!url) return JSON.stringify({ error: `No source URL for ${modName}` }); + try { + const resp = await fetch(url); + if (resp.ok) { + const src = await resp.text(); + this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, src); + return src.substring(0, 8000); + } + return JSON.stringify({ error: `HTTP ${resp.status}` }); + } catch (e) { return JSON.stringify({ error: e.message }); } + } + case 'edit_module': { + const modName = a.name || ''; + const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName); + const url = entry ? entry.url : null; + let code = this.terminal.fs.readFile(`_mods/${modName}.js`); + if (!code && url) { + try { + const resp = await fetch(url); + if (resp.ok) { + code = await resp.text(); + this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, code); + this.terminal.fs.writeFile(`_mods/${modName}.js`, code); + this.terminal.fs.writeFile(`_mods/${modName}.meta.json`, JSON.stringify({ originalUrl: url, modifiedAt: new Date().toISOString(), editCount: 0 })); + } + } catch (e) {} + } + if (code) this.terminal.openCodeEditor(modName, url || '', code); + return code ? `Editor opened for ${modName} (${code.length} chars)` : JSON.stringify({ error: `Could not load source for ${modName}` }); + } + case 'test_module': { + const modName = a.name || ''; + const code = this.terminal.fs.readFile(`_mods/${modName}.js`); + if (!code) return JSON.stringify({ error: `No local edit for ${modName}` }); + this.terminal.moduleManager.loadModuleFromCode(code, `blob://_mods/${modName}.js`); + return `Module "${modName}" loaded from local edit for testing`; + } + case 'reload_module': { + const modName = a.name || ''; + const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName); + this.terminal.fs.deleteFile(`_mods/${modName}.js`); + this.terminal.fs.deleteFile(`_mods/${modName}.orig.js`); + this.terminal.fs.deleteFile(`_mods/${modName}.meta.json`); + if (entry && entry.url) { + this.terminal.moduleManager.unloadModule(modName); + await this.terminal.moduleManager.loadModuleFromURL(entry.url); + return `Module "${modName}" reloaded from ${entry.url}`; + } + return JSON.stringify({ error: `No original URL for ${modName}` }); + } + case 'list_modified_modules': { + const modsDir = this.terminal.fs.getCurrentDir(); + const mc = modsDir.content['_mods']; + if (!mc || Object.keys(mc.content).length === 0) return JSON.stringify({ modified: [], message: 'No local edits' }); + const files = Object.keys(mc.content).filter(k => k.endsWith('.js') && !k.endsWith('.orig.js')); + return JSON.stringify({ modified: files.map(f => f.replace('.js', '')), count: files.length }); + } + case 'get_module_status': { + const mm = this.terminal.moduleManager; + const modsDir = this.terminal.fs.getCurrentDir(); + const hasMods = modsDir.content['_mods'] ? !!Object.keys(modsDir.content['_mods'].content).length : false; + const modules = mm.moduleRegistry.map(m => ({ + name: m.name, url: m.url, enabled: m.enabled, + loaded: !!mm.modules[m.name], + modified: !!this.terminal.fs.readFile(`_mods/${m.name}.js`) + })); + return JSON.stringify({ modules, total: modules.length, hasLocalEdits: hasMods }); + } + default: return JSON.stringify({ error: `Unknown module tool: ${name}` }); + } + } + clearContext() { + this.context = []; + localStorage.removeItem('webx11_ai_context'); + this.terminal.print('AI conversation context cleared', 'success'); + } + _saveContext() { + try { + const data = JSON.stringify({ + context: this.context.slice(-20), + messageCount: this._messageCount, + savedAt: Date.now() + }); + localStorage.setItem('webx11_ai_context', data); + // Also persist to VFS for crash survival + try { + const fs = this.terminal.fs; + if (fs) { + fs.mkdirRecursive('_sessions'); + fs.writeFile('_sessions/_auto-context.json', data); + } + } catch (e) {} + } catch (e) { + // localStorage quota exceeded — silently ignore + } + } + _loadContext() { + try { + const saved = localStorage.getItem('webx11_ai_context'); + if (saved) { + const data = JSON.parse(saved); + if (data && data.context) { + this.context = data.context.slice(-20); + this._messageCount = data.messageCount || 0; + } + } + } catch (e) {} + } + setSystemPrompt(args) { + if (!args[0]) { + this.terminal.print(`Current system prompt: ${this.systemPrompt.substring(0, 50)}...`, 'info'); return; + } + this.systemPrompt = args.join(' '); + this.terminal.print('System prompt updated', 'success'); + } + showModel(args) { + const om = this.terminal.ollamaManager; + if (args.length > 0) { + const name = args.join(' '); + if (om.models && om.models.some(m => m.name === name)) { + om.currentModel = name; + localStorage.setItem('webx11_ollama_model', name); + this.terminal.print(`AI model set to: ${name}`, 'success'); + } else { + this.terminal.print(`Model "${name}" not found in available models. Use ai-model to list.`, 'warning'); + } + return; + } + this.terminal.print(`Current AI model: ${om.currentModel || 'none'}`, 'info'); + if (om.models && om.models.length > 0) { + const maxShow = 20; + const names = om.models.slice(0, maxShow).map(m => m.name).join(', '); + this.terminal.print(`Available (${om.models.length}): ${names}${om.models.length > maxShow ? `... +${om.models.length - maxShow} more` : ''}`, 'info'); + } else { + this.terminal.print('No models available. Use ollamaconnect first.', 'warning'); + } + } + showContext() { + if (this.context.length === 0) { + this.terminal.print('No conversation context', 'info'); return; + } + this.terminal.print(`=== Context (${this.context.length} messages) ===`, 'info'); + this.context.forEach(msg => { + const icon = msg.role === 'user' ? '👤' : msg.role === 'assistant' ? '🤖' : '🔧'; + const content = typeof msg.content === 'string' ? msg.content.substring(0, 60) : JSON.stringify(msg).substring(0, 60); + this.terminal.print(`${icon} ${content}...`); + }); + } + _extractFacts(userMsg, aiResp) { + const patterns = [ + { re: /my name is (\w+)/i, key: 'name' }, + { re: /call me (\w+)/i, key: 'name' }, + { re: /i(?:'m| am) working on (.+)/i, key: 'project' }, + { re: /(?:my|the) project is (.+)/i, key: 'project' }, + { re: /i have a server at (.+)/i, key: 'server' }, + { re: /i use (\w+).*?(?:editor|ide|text)/i, key: 'preferredEditor' }, + { re: /i (?:like|prefer|love) (.+)/i, key: 'preference' }, + ]; + let changed = false; + for (const { re, key } of patterns) { + const m = (userMsg + ' ' + (aiResp || '')).match(re); + if (m && m[1]) { + if (!this._userProfile.knownFacts) this._userProfile.knownFacts = {}; + if (this._userProfile.knownFacts[key] !== m[1].trim()) { + this._userProfile.knownFacts[key] = m[1].trim(); + changed = true; + } + } + } + if (changed) { + try { localStorage.setItem('webx11_user_profile', JSON.stringify(this._userProfile)); } catch (e) {} + } + } + async _seedDefaultSkills() { + const fs = this.terminal.fs; + const mindmapSkill = `--- +name: mindmap-user-guide +description: How to create, view, and manage brainstorming mindmaps using the mindmap terminal module +tags: [mindmap, brainstorming, organization, planning] +category: productivity +--- +## Steps + +1. **Create a new mindmap:** + - \`mindmap new "" -t tag1,tag2\` — creates a root node + - Tags are optional (e.g. planning, research, dev) + - Root gets auto-assigned a color + +2. **Add child nodes:** + - \`mindmap add "" -t tag\` — add a child + - Get the parent-id from \`mindmap show\` + - IDs are 4-char random strings (e.g. a3f8, x7k2) + +3. **View the mindmap:** + - \`mindmap show\` — full tree with Unicode box-drawing + - \`mindmap show \` — subtree starting at that node + +4. **Edit nodes:** + - \`mindmap rename ""\` — change text + - \`mindmap color \` — set color (red/green/yellow/blue/cyan/magenta/white) + - \`mindmap tag \` — add a tag + +5. **Persistence:** + - \`mindmap save \` — save to localStorage + - \`mindmap load \` — restore + - \`mindmap list\` — show saved mindmaps + - \`mindmap delete \` — remove saved + +6. **Export:** + - \`mindmap export [id]\` — Markdown bullet list output + +## When to use +- User wants to brainstorm, organize ideas, plan features +- User says "map out", "diagram", "brainstorm", "organize thoughts" +- Visualizing project structure or task decomposition`; + + fs.fsWrite('/skills/mindmap-user-guide.md', mindmapSkill); + const existingIndex = fs.fsReadJSON('/skills/index.json') || {}; + existingIndex['mindmap-user-guide'] = { name: 'mindmap-user-guide', description: 'How to create, view, and manage brainstorming mindmaps using the mindmap terminal module', tags: ['mindmap', 'brainstorming', 'organization', 'planning'], category: 'productivity', created: existingIndex['mindmap-user-guide']?.created || new Date().toISOString(), updated: new Date().toISOString() }; + fs.fsWriteJSON('/skills/index.json', existingIndex); + fs.fsWrite('/skills/_seeded', '1'); + } + _trackPattern(userMessage) { + const lower = userMessage.toLowerCase(); + const patterns = ['font size','bigger text','smaller text','export','backup','save','install','load module','search','find','color','theme','skill','learn','remember','mindmap','brainstorm']; + for (const p of patterns) { + if (lower.includes(p)) this._commandFreq[p] = (this._commandFreq[p] || 0) + 1; + } + try { localStorage.setItem('webx11_ai_patterns', JSON.stringify(this._commandFreq)); } catch (e) {} + } + async _autoCreateErrorSkill() { + const grouped = {}; + for (const e of this._errorLog) { + if (!grouped[e.tool]) grouped[e.tool] = []; + grouped[e.tool].push(e); + } + for (const [tool, errors] of Object.entries(grouped)) { + if (errors.length >= 3) { + const skillName = 'tool-' + tool + '-workaround'; + const existing = this.terminal.fs.fsRead('/skills/' + skillName + '.md'); + if (existing) continue; + const steps = errors.map((e, i) => (i + 1) + '. When calling ' + tool + ', avoid: "' + JSON.stringify(e.args) + '" which caused: ' + e.error).join('\n'); + const content = '---\nname: ' + skillName + '\ndescription: Workarounds for ' + tool + ' failures\ntags: [' + tool + ', error, workaround]\ncategory: system\n---\n## Steps\n' + steps; + await this.runTool('create_skill', { name: skillName, description: 'Workarounds for ' + tool + ' failures', content: content, tags: tool + ',error,workaround', category: 'system' }); + this.terminal.print('🤖 Auto-created skill from repeated errors: ' + skillName, 'info'); + } + } + this._errorLog = []; + } + async _auditSkills() { + const fs = this.terminal.fs; + const index = fs.fsReadJSON('/skills/index.json') || {}; + const now = Date.now(); + const issues = []; + for (const [name, meta] of Object.entries(index)) { + const content = fs.fsRead('/skills/' + name + '.md'); + if (!content || content.length < 50) { + issues.push({ name, issue: 'empty or too short' }); + continue; + } + const updated = new Date(meta.updated || 0).getTime(); + const ageDays = (now - updated) / 86400000; + if (ageDays > 30) issues.push({ name, issue: 'not updated in ' + Math.round(ageDays) + ' days' }); + } + if (issues.length > 0) { + this.terminal.print('📋 Skill audit: ' + issues.length + ' issue(s) found', 'info'); + issues.slice(0, 3).forEach(i => this.terminal.print(' - ' + i.name + ': ' + i.issue, 'warning')); + } + } + _estimateTokens(text) { + if (!text || typeof text !== 'string') return 0; + return Math.ceil(text.length / 4); + } + _estimateContextTokens() { + let total = 0; + for (const msg of this.context) { + total += this._estimateTokens(msg.content || ''); + } + return total; + } + async _compressContext() { + if (this.context.length <= 6) return; + const totalTokens = this._estimateContextTokens(); + if (totalTokens < this.maxContextTokens) return; + const keepCount = 3; + const oldMsgs = this.context.slice(0, -keepCount * 2); + if (oldMsgs.length < 2) return; + const recentMsgs = this.context.slice(-keepCount * 2); + const summaryText = oldMsgs.map(m => + (m.role === 'user' ? '👤' : m.role === 'assistant' ? '🤖' : '⚙️') + ': ' + + (m.content || '').substring(0, 150) + ).join(' | '); + let compressed = `[Earlier context (${oldMsgs.length} messages): ${summaryText}]`; + // Try AI summarization, fallback to truncation + try { + if (this.terminal.ollamaManager && this.terminal.ollamaManager.connected) { + const sumPrompt = 'Summarize these conversation exchanges into a single compact sentence. Focus on what the user asked and what actions were taken. Keep it under 200 characters.\n\n' + JSON.stringify(oldMsgs.map(m => ({ role: m.role, content: (m.content || '').substring(0, 200) }))); + const sumRes = await this.terminal.ollamaManager.chat(sumPrompt); + if (sumRes && sumRes.length < 300) { + compressed = '[Earlier context: ' + sumRes.trim() + ']'; + } + } + } catch (e) { /* fallback to truncation */ } + this.context = [{ role: 'system', content: compressed }, ...recentMsgs]; + this.terminal.print('📦 Context compressed (' + oldMsgs.length + ' messages → 1 summary, ~' + totalTokens + ' tokens)', 'info'); + } + _manualCompress() { + this._compressContext().then(() => { + this.terminal.print('Context: ' + this.context.length + ' message(s), ~' + this._estimateContextTokens() + ' estimated tokens', 'info'); + }).catch(e => { + this.terminal.print('Compression error: ' + e.message, 'error'); + }); + } + } diff --git a/modules/core/module-chatstudio.js b/modules/core/module-chatstudio.js new file mode 100644 index 0000000..5777acf --- /dev/null +++ b/modules/core/module-chatstudio.js @@ -0,0 +1,1060 @@ +/* + * ChatStudio Overlay Module for newOS4 v4.2 + * Integrates ChatStudio Lite's full chat UI as an overlay launched via `chat` command. + * All Lite engine functions are inlined from lite/cslite-core.js, adapted to use + * the terminal's OllamaManager for endpoint configuration. + */ +class ChatStudioModule { + constructor() { + this.commands = {}; + this.terminal = null; + this._overlay = null; + this._active = false; + this._cssLoaded = false; + this._escapeHandler = null; + this._initialized = false; + } + + getHelpInfo() { + return { + 'chat': 'Toggle the ChatStudio overlay', + 'chat-setapi': 'Set the API base URL for ChatStudio', + 'chat-setmodel': 'Set the default model for ChatStudio', + 'chat-ask': 'Ask a question directly via ChatStudio' + }; + } + + registerCommands() { + this.commands['chat'] = () => this.toggleChatOverlay(); + this.commands['chat-setapi'] = (args) => { + if (!args[0]) { + const base = window.API_BASE || '/api'; + this.terminal.print('Current API base: ' + base, 'info'); + this.terminal.print('Usage: chat-setapi ', 'info'); + this.terminal.print('Example: chat-setapi /api', 'info'); + return; + } + const url = args[0].replace(/\/+$/, ''); + window.API_BASE = url; + try { + var settings = JSON.parse(localStorage.getItem('webx11_chat_settings') || '{}'); + settings.apiBase = url; + localStorage.setItem('webx11_chat_settings', JSON.stringify(settings)); + } catch (e) {} + this.terminal.print('API base set to: ' + url + ' (reopen chat to apply)', 'success'); + }; + } + + toggleChatOverlay() { + if (this._active) { + this._closeOverlay(); + } else { + this._openOverlay(); + } + } + + _loadOverlayAssets() { + return new Promise((resolve, reject) => { + // Load CSS + if (!this._cssLoaded) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'chat/chat-ui.css?v=4'; + link.onload = () => { this._cssLoaded = true; }; + link.onerror = () => reject(new Error('Failed to load chat/chat-ui.css')); + document.head.appendChild(link); + } + + // Expose ALL Lite globals BEFORE scripts load so chat-modules.js & chat-commands.js can use them + if (!window._csGlobalsInited) { + window.OLLAMA_URL = (this.terminal && this.terminal.ollamaManager && this.terminal.ollamaManager.endpoint) || '/ollama'; + window.OLLAMA_API_KEY = ''; + window.DEFAULT_MODEL = 'llama3.2'; + window.USE_STREAMING = true; + window.API_BASE = '/api'; + window.latestAiContent = ''; + window.logs = []; + window.messages = []; + window.isStreaming = false; + window.CHATSTUDIO_VERSION = '1.0.0-lite-inline'; + window.modules = []; + window.currentChatModule = 'default'; + + // Utility functions + window.utf8_to_b64 = function(str) { return btoa(unescape(encodeURIComponent(str))); }; + window.b64_to_utf8 = function(str) { return decodeURIComponent(escape(atob(str))); }; + window.escapeHtml = function(text) { + if (typeof text !== 'string') return ''; + return text.replace(/&/g, '&').replace(//g, '>').replace(/\"/g, '"'); + }; + window.generateId = function() { return Date.now().toString(36) + '_' + Math.random().toString(36).substr(2, 9); }; + window.showToast = function(msg) { + var t = document.getElementById('toast'); + if (!t) return; + t.textContent = msg; + t.style.display = 'block'; + setTimeout(function() { t.style.display = 'none'; }, 2500); + }; + window.addLog = function(action, detail) { + // Placeholder — replaced by full impl in _initLiteEngine + }; + // Placeholder functions — replaced by full implementations in _initLiteEngine + window.addMessage = function(role, content) {}; + window.showTyping = function() {}; + window.hideTyping = function() {}; + window.loadSettings = function() {}; + window.saveSettings = function() {}; + window.loadModels = function() {}; + window.switchModule = function(id) {}; + window.getCurrentModule = function() {}; + window.sendMessage = function() {}; + window.clearChat = function() {}; + window.closeSettingsModal = function() {}; + window.openSettingsModal = function() {}; + window._csGlobalsInited = true; + } + + // Fetch HTML template + fetch('chat/chat-ui.html?v=4') + .then(r => r.text()) + .then(html => { + // Insert overlay into body if not already there + if (!document.getElementById('chat-overlay')) { + const temp = document.createElement('div'); + temp.innerHTML = html; + document.body.appendChild(temp.firstElementChild); + + // Load commands.js and modules.js dynamically + const loadScript = (src) => { + return new Promise((res, rej) => { + const s = document.createElement('script'); + s.src = src; + s.onload = () => res(); + s.onerror = () => rej(new Error('Failed to load ' + src)); + document.body.appendChild(s); + }); + }; + + Promise.all([ + loadScript('chat/chat-commands.js?v=4'), + loadScript('chat/chat-modules.js?v=4') + ]).then(() => { + resolve(); + }); + } else { + resolve(); + } + }) + .catch(() => reject(new Error('Failed to load chat/chat-ui.html'))); + }); + } + + async _openOverlay() { + if (this._active) return; + this._active = true; + + try { + // Load overlay assets first (CSS, HTML, scripts) + await this._loadOverlayAssets(); + + // Initialize the Lite engine + this._initLiteEngine(); + + // Show overlay + const overlay = document.getElementById('chat-overlay'); + if (overlay) { + overlay.style.display = 'flex'; + overlay.style.flexDirection = 'column'; + } + + // Add Escape listener + this._escapeHandler = (e) => { + if (e.key === 'Escape') { + this._closeOverlay(); + } + }; + document.addEventListener('keydown', this._escapeHandler); + + // Set window-level endpoint for Lite to use + if (this.terminal && this.terminal.ollamaManager) { + window.ollamaEndpoint = this.terminal.ollamaManager.endpoint; + } + } catch (err) { + // Print error to terminal + if (this.terminal && this.terminal.print) { + this.terminal.print('Chat overlay failed: ' + err.message, 'error'); + } + + // Show a toast on the main page if possible + if (typeof window.showToast === 'function') { + window.showToast('Chat overlay failed: ' + err.message); + } + + // Clean up any partial overlay DOM + const partialOverlay = document.getElementById('chat-overlay'); + if (partialOverlay) { + partialOverlay.remove(); + } + + // Reset state so user can retry + this._active = false; + } + } + + _closeOverlay() { + if (!this._active) return; + this._active = false; + + const overlay = document.getElementById('chat-overlay'); + if (overlay) { + overlay.style.display = 'none'; + } + + // Remove Escape listener + if (this._escapeHandler) { + document.removeEventListener('keydown', this._escapeHandler); + this._escapeHandler = null; + } + + // Focus terminal input + if (this.terminal && this.terminal.inputField) { + setTimeout(() => this.terminal.inputField.focus(), 50); + } + } + + _initLiteEngine() { + // Already initialized — no-op + if (this._initialized) return; + + // Set the overlay reference + this._overlay = document.getElementById('chat-overlay'); + if (!this._overlay) { + if (this.terminal && this.terminal.print) { + this.terminal.print('Chat overlay: #chat-overlay element not found', 'error'); + } + return; + } + + this._initialized = true; + + try { + // Close button already exists in HTML as "✕ Close" button calling closeChatOverlay() + window.closeChatOverlay = () => this._closeOverlay(); + + // ── Constants ── + const CHATSTUDIO_VERSION = '1.0.0-lite-inline'; + const API_KEY = 'webx11_chat_api'; + const MESSAGES_KEY = 'cslite_messages'; + const SETTINGS_KEY = 'webx11_chat_settings'; + const LOGS_KEY = 'webx11_chat_logs'; + + // ── State ── + let OLLAMA_URL = '/ollama'; + let OLLAMA_API_KEY = ''; + let DEFAULT_MODEL = 'llama3.2'; + let USE_STREAMING = true; + let API_BASE = '/api'; + let latestAiContent = ''; + let logs = []; + let messages = []; + let isStreaming = false; + + // Sync OLLAMA_URL from terminal + if (this.terminal && this.terminal.ollamaManager) { + OLLAMA_URL = this.terminal.ollamaManager.endpoint || '/ollama'; + } + + // ── Utility Functions ── + function escapeHtml(text) { + if (typeof text !== 'string') return ''; + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + function utf8_to_b64(str) { + return btoa(unescape(encodeURIComponent(str))); + } + + function b64_to_utf8(str) { + return decodeURIComponent(escape(atob(str))); + } + + function generateId() { + return Date.now().toString(36) + '_' + Math.random().toString(36).substr(2, 9); + } + + function showToast(msg) { + var t = document.getElementById('toast'); + if (!t) return; + t.textContent = msg; + t.style.display = 'block'; + setTimeout(function () { + t.style.display = 'none'; + }, 2500); + } + + function autoExpand(el) { + el.style.height = 'auto'; + el.style.height = Math.min(el.scrollHeight, 150) + 'px'; + } + + function toggleSidebar() { + var s = document.getElementById('sidebar'); + if (s) s.classList.toggle('open'); + var b = document.getElementById('sidebarBackdrop'); + if (b) b.classList.toggle('open'); + } + function closeSidebar() { + var s = document.getElementById('sidebar'); + if (s) s.classList.remove('open'); + var b = document.getElementById('sidebarBackdrop'); + if (b) b.classList.remove('open'); + } + + // ── Log System ── + function addLog(action, detail) { + var time = new Date().toLocaleTimeString(); + logs.unshift({ time: time, action: action, detail: detail }); + if (logs.length > 50) logs.pop(); + renderLogs(); + try { + localStorage.setItem(LOGS_KEY, utf8_to_b64(JSON.stringify(logs))); + } catch (e) { /* ignore */ } + } + + function renderLogs() { + var container = document.getElementById('logsContent'); + if (!container) return; + container.innerHTML = logs.map(function (l) { + return '
' + + '' + escapeHtml(l.time) + '' + + '' + escapeHtml(l.action) + '' + + '' + escapeHtml(l.detail) + '' + + '
'; + }).join(''); + } + + function copyLogs() { + var text = logs.map(function (l) { + return l.time + ' | ' + l.action + ' | ' + l.detail; + }).join('\n'); + navigator.clipboard.writeText(text).then(function () { + showToast('Logs copied!'); + }); + } + + function clearLogs() { + if (!confirm('Clear all activity logs?')) return; + logs = []; + try { + localStorage.removeItem(LOGS_KEY); + } catch (e) { /* ignore */ } + renderLogs(); + showToast('Logs cleared'); + } + + function loadLogs() { + try { + var saved = localStorage.getItem(LOGS_KEY); + if (saved) logs = JSON.parse(b64_to_utf8(saved)); + } catch (e) { + logs = []; + } + renderLogs(); + } + + // ── Settings ── + function loadSettings() { + try { + var saved = localStorage.getItem(SETTINGS_KEY); + if (saved) { + var settings = JSON.parse(b64_to_utf8(saved)); + OLLAMA_URL = settings.ollamaUrl || '/ollama'; + OLLAMA_API_KEY = settings.ollamaApiKey || ''; + DEFAULT_MODEL = settings.defaultModel || 'llama3.2'; + USE_STREAMING = settings.useStreaming !== false; + API_BASE = settings.apiBase || '/api'; + } + } catch (e) { + console.warn('Failed to load settings:', e); + } + // Sync from terminal if available + var term = window.webx11; + if (term && term.ollamaManager) { + OLLAMA_URL = term.ollamaManager.endpoint || OLLAMA_URL; + } + var urlEl = document.getElementById('ollamaUrl'); + var keyEl = document.getElementById('ollamaApiKey'); + var modelEl = document.getElementById('defaultModel'); + var streamEl = document.getElementById('streamingToggle'); + var apiEl = document.getElementById('apiBase'); + if (urlEl) urlEl.value = OLLAMA_URL; + if (keyEl) keyEl.value = OLLAMA_API_KEY; + if (modelEl) modelEl.value = DEFAULT_MODEL; + if (streamEl) streamEl.checked = USE_STREAMING; + if (apiEl) apiEl.value = API_BASE; + } + + function saveSettings() { + OLLAMA_URL = document.getElementById('ollamaUrl').value.trim() || '/ollama'; + OLLAMA_API_KEY = document.getElementById('ollamaApiKey').value.trim(); + DEFAULT_MODEL = document.getElementById('defaultModel').value.trim() || 'llama3.2'; + USE_STREAMING = document.getElementById('streamingToggle').checked; + API_BASE = document.getElementById('apiBase').value.trim() || '/api'; + try { + var settings = { + ollamaUrl: OLLAMA_URL, + ollamaApiKey: OLLAMA_API_KEY, + defaultModel: DEFAULT_MODEL, + useStreaming: USE_STREAMING, + apiBase: API_BASE + }; + localStorage.setItem(SETTINGS_KEY, utf8_to_b64(JSON.stringify(settings))); + } catch (e) { + console.warn('Failed to save settings:', e); + } + closeSettingsModal(); + showToast('Settings saved!'); + } + + function openSettingsModal() { + loadSettings(); + var m = document.getElementById('settingsModal'); + if (m) m.classList.add('open'); + } + + function closeSettingsModal() { + var m = document.getElementById('settingsModal'); + if (m) m.classList.remove('open'); + } + + async function testConnection() { + var status = document.getElementById('connectionStatus'); + if (!status) return; + status.textContent = 'Testing...'; + status.style.color = 'var(--text2)'; + try { + var headers = { 'Content-Type': 'application/json' }; + if (OLLAMA_API_KEY) headers['Authorization'] = 'Bearer ' + OLLAMA_API_KEY; + var response = await fetch(OLLAMA_URL + '/api/tags', { headers: headers }); + if (response.ok) { + var data = await response.json(); + var modelCount = data.models ? data.models.length : 0; + status.textContent = 'Connected! ' + modelCount + ' models available.'; + status.style.color = 'var(--accent)'; + } else { + status.textContent = 'Connection failed: HTTP ' + response.status; + status.style.color = 'var(--danger)'; + } + } catch (e) { + status.textContent = 'Error: ' + e.message; + status.style.color = 'var(--danger)'; + } + } + + // ── Models ── + async function loadModels() { + try { + var response = await fetch(OLLAMA_URL + '/api/tags'); + if (!response.ok) throw new Error('Failed to fetch models'); + var data = await response.json(); + var select = document.getElementById('modelSelect'); + if (!select) return; + var shortNames = { + 'llama3.2': 'Llama3.2', + 'llama3': 'Llama3', + 'gemma3:4b': 'Gemma3-4B', + 'gemma3:1b': 'Gemma3-1B', + 'gemma2:2b': 'Gemma2', + 'qwen2.5:7b-instruct': 'Qwen2.5', + 'qwen3:8b': 'Qwen3', + 'mistral': 'Mistral', + 'tinyllama': 'Tiny' + }; + select.innerHTML = data.models.map(function (m) { + var name = m.name.split(':')[0]; + var display = shortNames[m.name] || name.substring(0, 14); + return ''; + }).join(''); + addLog('Init', 'Loaded ' + data.models.length + ' models'); + } catch (e) { + console.error('Failed to load models:', e); + var select = document.getElementById('modelSelect'); + if (select) select.innerHTML = ''; + } + } + + var modelSelect = document.getElementById('modelSelect'); + if (modelSelect) { + modelSelect.addEventListener('change', function () { + addLog('Model', 'Switched to ' + this.value); + }); + } + + // ── Message History ── + function saveMessages(msgs) { + msgs = msgs || messages; + try { + localStorage.setItem(MESSAGES_KEY, JSON.stringify(msgs)); + } catch (e) { + console.warn('Failed to save messages:', e); + } + } + + function loadMessages() { + try { + var raw = localStorage.getItem(MESSAGES_KEY); + if (raw) { + messages = JSON.parse(raw); + } + } catch (e) { + console.warn('Failed to load messages:', e); + messages = []; + } + } + + function renderMessagesFromData() { + var container = document.getElementById('chatContainer'); + if (!container) return; + if (!messages || messages.length === 0) { + return; + } + container.innerHTML = ''; + for (var i = 0; i < messages.length; i++) { + var msg = messages[i]; + appendMessageToDOM(msg.role, msg.content, msg.id, false); + } + container.scrollTop = container.scrollHeight; + } + + function appendMessageToDOM(role, content, msgId, save) { + if (save === undefined) save = true; + var container = document.getElementById('chatContainer'); + if (!container) return; + var div = document.createElement('div'); + div.className = 'message ' + role; + if (msgId) div.dataset.msgId = msgId; + var previewBtn = role === 'ai' + ? '' + : ''; + if (role === 'ai') latestAiContent = content; + var renderedContent = role === 'user' ? escapeHtml(content) : formatContent(content); + div.innerHTML = '
' + + '' + (role === 'user' ? 'You' : 'AI') + '' + + '
' + + '' + + previewBtn + + '
' + + '
' + renderedContent + '
'; + div.dataset.content = content; + container.appendChild(div); + container.scrollTop = container.scrollHeight; + if (save) { + saveMessages(); + } + } + + function addMessage(role, content) { + var msgId = generateId(); + var msg = { + id: msgId, + role: role, + content: content, + timestamp: Date.now() + }; + messages.push(msg); + appendMessageToDOM(role, content, msgId, true); + return msgId; + } + + // ── Content Formatting ── + function formatContent(text) { + // Step 1: Extract code blocks into placeholders + var blocks = []; + var processed = text + .replace(/```html\n?([\s\S]*?)```/g, function(m, code) { + var idx = blocks.length; + blocks.push({lang: 'html', code: code, isBlock: true}); + return '%%CODEBLOCK_' + idx + '%%'; + }) + .replace(/```(\w*)\n?([\s\S]*?)```/g, function(m, lang, code) { + var idx = blocks.length; + blocks.push({lang: lang, code: code, isBlock: true}); + return '%%CODEBLOCK_' + idx + '%%'; + }) + .replace(/`([^`]+)`/g, function(m, content) { + var idx = blocks.length; + blocks.push({lang: '', code: content, isBlock: false, isInline: true}); + return '%%CODEBLOCK_' + idx + '%%'; + }); + + // Step 2: Escape everything outside code blocks + processed = escapeHtml(processed); + + // Step 3: Apply markdown formatting on the escaped text + processed = processed + .replace(/\*\*([^*]+)\*\*/g, function(m, content) { return '' + content + ''; }) + .replace(/(https?:\/\/[^\s<]+)/g, '$1') + .replace(/\n/g, '
'); + + // Step 4: Restore code blocks with proper HTML + processed = processed.replace(/%%CODEBLOCK_(\d+)%%/g, function(m, idx) { + idx = parseInt(idx); + var block = blocks[idx]; + if (!block) return m; + if (block.isInline) { + return '' + escapeHtml(block.code) + ''; + } + var id = 'code_' + Math.random().toString(36).substr(2, 9); + if (block.lang === 'html') { + return '
' + + '
' +
+                        '' +
+                        '' +
+                        '' + escapeHtml(block.code) + '
'; + } + return '
' +
+                    '' +
+                    '' + escapeHtml(block.code) + '
'; + }); + + return processed; + } + + // ── Copy & Preview ── + function copyMessage(btn, role) { + var content; + if (role === 'code') { + content = btn.nextElementSibling.textContent; + } else { + var msg = btn.closest('.message'); + content = msg.dataset.content; + } + navigator.clipboard.writeText(content).then(function () { + showToast('Copied!'); + }); + } + + function showHtmlPreview() { + var frame = document.getElementById('previewFrame'); + if (!frame) return; + var doc = frame.contentWindow.document; + doc.open(); + doc.write(latestAiContent); + doc.close(); + var m = document.getElementById('previewModal'); + if (m) m.classList.add('open'); + } + + function previewCodeBlock(btn) { + var block = btn.closest('.code-block'); + if (!block) return; + var code = decodeURIComponent(block.dataset.code); + var frame = document.getElementById('previewFrame'); + if (!frame) return; + var doc = frame.contentWindow.document; + doc.open(); + doc.write(code); + doc.close(); + var m = document.getElementById('previewModal'); + if (m) m.classList.add('open'); + } + + function closePreviewModal() { + var m = document.getElementById('previewModal'); + if (m) m.classList.remove('open'); + } + + // ── Typing Indicator ── + function showTyping() { + var container = document.getElementById('chatContainer'); + if (!container) return; + var div = document.createElement('div'); + div.className = 'typing'; + div.id = 'typingIndicator'; + div.innerHTML = 'AI is thinking' + + '' + + '' + + ''; + container.appendChild(div); + container.scrollTop = container.scrollHeight; + } + + function hideTyping() { + var t = document.getElementById('typingIndicator'); + if (t) t.remove(); + } + + // ── Send Message ── + async function sendMessage() { + if (isStreaming) return; + + var input = document.getElementById('chatInput'); + if (!input) return; + var msg = input.value.trim(); + if (!msg) return; + + input.value = ''; + input.style.height = 'auto'; + + addMessage('user', msg); + addLog('User', msg.substring(0, 50) + (msg.length > 50 ? '...' : '')); + + if (msg.startsWith('/')) { + await handleCommand(msg); + return; + } + + showTyping(); + + // Sync OLLAMA_URL from terminal before sending + var term = window.webx11; + if (term && term.ollamaManager) { + OLLAMA_URL = term.ollamaManager.endpoint || OLLAMA_URL; + } + // Re-set window-level endpoint + window.ollamaEndpoint = OLLAMA_URL; + + var modelSelect = document.getElementById('modelSelect'); + var model = modelSelect ? modelSelect.value : DEFAULT_MODEL; + var mod = typeof getCurrentModule === 'function' ? getCurrentModule() : null; + var systemPrompt = mod && mod.prompt ? mod.prompt : ''; + var chatHistory = buildChatContext(); + + try { + var headers = { 'Content-Type': 'application/json' }; + if (OLLAMA_API_KEY) headers['Authorization'] = 'Bearer ' + OLLAMA_API_KEY; + + var fullPrompt = systemPrompt + ? systemPrompt + '\n\n' + chatHistory + '\n\nUser: ' + msg + '\n\nAI:' + : chatHistory + '\n\nUser: ' + msg + '\n\nAI:'; + + if (USE_STREAMING) { + await sendStreamingRequest(headers, model, fullPrompt); + } else { + await sendNonStreamingRequest(headers, model, fullPrompt); + } + } catch (e) { + hideTyping(); + console.error('Chat error:', e); + addMessage('ai', 'Sorry, I could not connect to the AI service. Make sure Ollama is running on your server.\n\nError: ' + e.message); + } + } + + async function sendStreamingRequest(headers, model, prompt) { + isStreaming = true; + hideTyping(); + + var msgId = generateId(); + var container = document.getElementById('chatContainer'); + if (!container) { isStreaming = false; return; } + var div = document.createElement('div'); + div.className = 'message ai'; + div.dataset.msgId = msgId; + div.innerHTML = '
AI
' + + '' + + '' + + '
'; + div.dataset.content = ''; + container.appendChild(div); + container.scrollTop = container.scrollHeight; + + var contentEl = div.querySelector('.msg-content'); + var fullContent = ''; + + try { + var response = await fetch(OLLAMA_URL + '/api/generate', { + method: 'POST', + headers: headers, + body: JSON.stringify({ + model: model, + prompt: prompt, + stream: true + }) + }); + + if (!response.ok) { + contentEl.textContent = 'AI request failed: HTTP ' + response.status; + throw new Error('AI request failed: HTTP ' + response.status); + } + + var reader = response.body.getReader(); + var decoder = new TextDecoder(); + var buffer = ''; + + var lastFormat = 0; + var formatInterval = 400; + + while (true) { + var result = await reader.read(); + if (result.done) break; + + buffer += decoder.decode(result.value, { stream: true }); + var lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + if (!line) continue; + try { + var parsed = JSON.parse(line); + if (parsed.response) { + fullContent += parsed.response; + var now = Date.now(); + if (now - lastFormat >= formatInterval) { + contentEl.innerHTML = formatContent(fullContent) + ''; + container.scrollTop = container.scrollHeight; + lastFormat = now; + } + } + if (parsed.done) { + break; + } + } catch (e) { + } + } + } + + // Process remaining buffer + if (buffer.trim()) { + try { + var parsed = JSON.parse(buffer); + if (parsed.response) { + fullContent += parsed.response; + } + } catch (e) { /* ignore */ } + } + + // Final render without cursor + contentEl.innerHTML = formatContent(fullContent); + div.dataset.content = fullContent; + latestAiContent = fullContent; + container.scrollTop = container.scrollHeight; + + messages.push({ + id: msgId, + role: 'ai', + content: fullContent, + timestamp: Date.now() + }); + saveMessages(); + addLog('AI', fullContent.substring(0, 50) + '...'); + + } catch (e) { + if (contentEl) contentEl.textContent = 'Error: ' + e.message; + console.error('Stream error:', e); + } + + isStreaming = false; + } + + async function sendNonStreamingRequest(headers, model, prompt) { + try { + var response = await fetch(OLLAMA_URL + '/api/generate', { + method: 'POST', + headers: headers, + body: JSON.stringify({ + model: model, + prompt: prompt, + stream: false + }) + }); + + hideTyping(); + + if (!response.ok) throw new Error('AI request failed'); + + var data = await response.json(); + addMessage('ai', data.response || 'No response'); + addLog('AI', (data.response || '').substring(0, 50) + '...'); + } catch (e) { + hideTyping(); + throw e; + } + } + + function buildChatContext() { + var context = ''; + for (var i = 0; i < messages.length; i++) { + var m = messages[i]; + var role = m.role === 'user' ? 'User' : 'AI'; + var content = m.content.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(); + context += role + ': ' + content + '\n'; + } + return context; + } + + function getChatHistory() { + return buildChatContext(); + } + + // ── Clear & Share ── + function clearChat() { + if (!confirm('Clear chat history?')) return; + messages = []; + localStorage.removeItem(MESSAGES_KEY); + var container = document.getElementById('chatContainer'); + if (container) { + container.innerHTML = + '
' + + '
AI
' + + 'Chat cleared! How can I help you?' + + '
'; + } + addLog('Clear', 'Chat history cleared'); + } + + function shareChat() { + var msgs = []; + for (var i = 0; i < messages.length; i++) { + var m = messages[i]; + var role = m.role === 'user' ? 'User' : 'AI'; + msgs.push('**' + role + ':** ' + m.content); + } + var text = '# WebX11 AI Chat\n\n' + msgs.join('\n\n'); + + if (navigator.clipboard) { + navigator.clipboard.writeText(text).then(function () { + showToast('Chat copied to clipboard!'); + addLog('Share', 'Chat copied to clipboard'); + }); + } else { + var blob = new Blob([text], { type: 'text/plain' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = 'chat-export.txt'; + a.click(); + URL.revokeObjectURL(url); + addLog('Share', 'Chat downloaded as file'); + } + } + + // ── Expose Lite globals for commands.js and modules.js ── + // All functions exposed under CSLite namespace AND as individual window.* + // for backward compatibility with chat-commands.js and chat-modules.js + window.CSLite = { + CHATSTUDIO_VERSION: CHATSTUDIO_VERSION, + OLLAMA_URL: OLLAMA_URL, + OLLAMA_API_KEY: OLLAMA_API_KEY, + DEFAULT_MODEL: DEFAULT_MODEL, + USE_STREAMING: USE_STREAMING, + API_BASE: API_BASE, + getApiUrl: function(path) { return (window.CSLite.API_BASE || '/api') + path; }, + latestAiContent: latestAiContent, + logs: logs, + messages: messages, + isStreaming: isStreaming, + modules: [], + currentChatModule: 'default', + escapeHtml: escapeHtml, + utf8_to_b64: utf8_to_b64, + b64_to_utf8: b64_to_utf8, + generateId: generateId, + showToast: showToast, + autoExpand: autoExpand, + toggleSidebar: toggleSidebar, + closeSidebar: closeSidebar, + addLog: addLog, + renderLogs: renderLogs, + loadSettings: loadSettings, + saveSettings: saveSettings, + loadModels: loadModels, + loadMessages: loadMessages, + saveMessages: saveMessages, + appendMessageToDOM: appendMessageToDOM, + addMessage: addMessage, + formatContent: formatContent, + copyMessage: copyMessage, + showHtmlPreview: showHtmlPreview, + previewCodeBlock: previewCodeBlock, + closePreviewModal: closePreviewModal, + showTyping: showTyping, + hideTyping: hideTyping, + sendMessage: sendMessage, + sendStreamingRequest: sendStreamingRequest, + sendNonStreamingRequest: sendNonStreamingRequest, + buildChatContext: buildChatContext, + getChatHistory: getChatHistory, + clearChat: clearChat, + shareChat: shareChat, + copyLogs: copyLogs, + clearLogs: clearLogs, + loadLogs: loadLogs, + testConnection: testConnection, + openSettingsModal: openSettingsModal, + closeSettingsModal: closeSettingsModal + }; + // Individual window.* assignments for backward compat + window.CHATSTUDIO_VERSION = CHATSTUDIO_VERSION; + window.OLLAMA_URL = OLLAMA_URL; + window.OLLAMA_API_KEY = OLLAMA_API_KEY; + window.DEFAULT_MODEL = DEFAULT_MODEL; + window.USE_STREAMING = USE_STREAMING; + window.API_BASE = API_BASE; + window.getApiUrl = function(path) { return (window.API_BASE || '/api') + path; }; + window.latestAiContent = latestAiContent; + window.logs = logs; + window.messages = messages; + window.isStreaming = isStreaming; + window.escapeHtml = escapeHtml; + window.utf8_to_b64 = utf8_to_b64; + window.b64_to_utf8 = b64_to_utf8; + window.generateId = generateId; + window.showToast = showToast; + window.autoExpand = autoExpand; + window.toggleSidebar = toggleSidebar; + window.closeSidebar = closeSidebar; + window.addLog = addLog; + window.renderLogs = renderLogs; + window.loadSettings = loadSettings; + window.saveSettings = saveSettings; + window.loadModels = loadModels; + window.loadMessages = loadMessages; + window.saveMessages = saveMessages; + window.appendMessageToDOM = appendMessageToDOM; + window.addMessage = addMessage; + window.formatContent = formatContent; + window.copyMessage = copyMessage; + window.showHtmlPreview = showHtmlPreview; + window.previewCodeBlock = previewCodeBlock; + window.closePreviewModal = closePreviewModal; + window.showTyping = showTyping; + window.hideTyping = hideTyping; + window.sendMessage = sendMessage; + window.sendStreamingRequest = sendStreamingRequest; + window.sendNonStreamingRequest = sendNonStreamingRequest; + window.buildChatContext = buildChatContext; + window.getChatHistory = getChatHistory; + window.clearChat = clearChat; + window.shareChat = shareChat; + window.copyLogs = copyLogs; + window.clearLogs = clearLogs; + window.loadLogs = loadLogs; + window.testConnection = testConnection; + window.openSettingsModal = openSettingsModal; + window.closeSettingsModal = closeSettingsModal; + + // ── Init ── + loadSettings(); + loadMessages(); + loadLogs(); + loadModels(); + renderMessagesFromData(); + addLog('Init', 'Chat overlay initialized'); + } catch (err) { + // Reset initialization flag so user can retry + this._initialized = false; + + // Print error to terminal + if (this.terminal && this.terminal.print) { + this.terminal.print('Chat overlay init error: ' + err.message, 'error'); + } + + console.error('ChatStudioModule._initLiteEngine error:', err); + } + } +} diff --git a/modules/core/module-config.js b/modules/core/module-config.js new file mode 100644 index 0000000..fb5199e --- /dev/null +++ b/modules/core/module-config.js @@ -0,0 +1,92 @@ +// Extracted from newOS4 index.html — ConfigManagerModule + class ConfigManagerModule { + constructor() { + this.commands = {}; this.terminal = null; + this.configPrefix = 'webx11_config_'; + } + getHelpInfo() { + return { + 'config': 'config [key] — Show all config or a specific key', + 'config-set': 'config-set — Set a configuration value', + 'config-list': 'config-list — List all configuration keys', + 'config-delete': 'config-delete — Delete a configuration key', + 'config-export': 'config-export — Export all config as JSON', + 'config-reset': 'config-reset — Reset all configuration to defaults', + }; + } + registerCommands() { + this.commands['config'] = (args) => this.getConfig(args); + this.commands['config-set'] = (args) => this.setConfig(args); + this.commands['config-list'] = () => this.listConfig(); + this.commands['config-delete'] = (args) => this.deleteConfig(args); + this.commands['config-export'] = () => this.exportConfig(); + this.commands['config-reset'] = () => this.resetConfig(); + } + getConfig(args) { + if (!args[0]) { + this.terminal.print("Usage: config ", "warning"); return; + } + const value = localStorage.getItem(this.configPrefix + args[0]); + if (value) { + this.terminal.print(`${args[0]}: ${value}`, "info"); + } else { + this.terminal.print(`Key '${args[0]}' not found`, "error"); + } + } + setConfig(args) { + if (args.length < 2) { + this.terminal.print("Usage: config-set ", "warning"); return; + } + const key = args[0], value = args.slice(1).join(' '); + localStorage.setItem(this.configPrefix + key, value); + this.terminal.print(`Config saved: ${key} = ${value}`, "success"); + } + listConfig() { + const configs = {}; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key.startsWith(this.configPrefix)) { + configs[key.replace(this.configPrefix, '')] = localStorage.getItem(key); + } + } + if (Object.keys(configs).length === 0) { + this.terminal.print("No configuration saved", "info"); return; + } + this.terminal.print("=== Configuration ===", "info"); + Object.entries(configs).forEach(([k, v]) => { this.terminal.print(`${k}: ${v}`); }); + } + deleteConfig(args) { + if (!args[0]) { + this.terminal.print("Usage: config-delete ", "warning"); return; + } + localStorage.removeItem(this.configPrefix + args[0]); + this.terminal.print(`Config '${args[0]}' deleted`, "success"); + } + exportConfig() { + const configs = {}; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key.startsWith(this.configPrefix)) { + configs[key] = localStorage.getItem(key); + } + } + const json = JSON.stringify(configs, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; a.download = 'webx11-config.json'; a.click(); + URL.revokeObjectURL(url); + this.terminal.print("Configuration exported", "success"); + } + resetConfig() { + if (confirm("Reset all configuration to defaults?")) { + const keysToRemove = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key.startsWith(this.configPrefix)) { keysToRemove.push(key); } + } + keysToRemove.forEach(k => localStorage.removeItem(k)); + this.terminal.print("Configuration reset to defaults", "success"); + } + } + } diff --git a/modules/core/module-duckduckgo.js b/modules/core/module-duckduckgo.js new file mode 100644 index 0000000..c443b72 --- /dev/null +++ b/modules/core/module-duckduckgo.js @@ -0,0 +1,38 @@ +// Extracted from newOS4 index.html — DuckDuckGoModule + class DuckDuckGoModule { + constructor() { + this.commands = {}; + this.terminal = null; + } + getHelpInfo() { + return { + 'ddg': 'ddg — Search DuckDuckGo (opens query in new tab)', + 'duck': 'duck — Alias for ddg', + }; + } + registerCommands() { + this.commands['ddg'] = (args) => this.search(args); + this.commands['duck'] = (args) => this.search(args); + } + async search(args) { + if (!args[0]) { + this.terminal.print("Usage: ddg ", "warning"); + this.terminal.print("Example: ddg javascript tutorials", "info"); + this.terminal.print("Note: Due to CORS, results may be limited. Try 'search' command for better results.", "warning"); + return; + } + const query = args.join(' '); + this.terminal.print(`🦆 DuckDuckGo Search: ${query}`, "info"); + + // DuckDuckGo doesn't have a public API, so we provide alternatives + this.terminal.print("⚠️ DuckDuckGo doesn't provide a public API for direct access.", "warning"); + this.terminal.print("Opening search in new tab...", "info"); + + // Open in new tab as fallback + const ddgUrl = `https://duckduckgo.com/?q=${encodeURIComponent(query)}`; + window.open(ddgUrl, '_blank'); + + this.terminal.print(`\n💡 Alternative: Use 'search ' for SearXNG (privacy-focused)`, "info"); + this.terminal.print(`🔗 Direct link: ${ddgUrl}`, "info"); + } + } diff --git a/modules/core/module-dynamicprompts.js b/modules/core/module-dynamicprompts.js new file mode 100644 index 0000000..ff8c5ea --- /dev/null +++ b/modules/core/module-dynamicprompts.js @@ -0,0 +1,102 @@ +// Extracted from newOS4 index.html — DynamicPromptsModule + class DynamicPromptsModule { + constructor() { + this.commands = {}; this.terminal = null; + this.prompts = this.loadPrompts(); this.variables = {}; + } + getHelpInfo() { + return { + 'prompt': 'prompt [additional-text] — Use a saved prompt template', + 'prompt-save': 'prompt-save