aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/core/module-aiassist.js113
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/module-system.js2
-rw-r--r--modules/core/ollama-manager.js22
-rw-r--r--modules/core/terminal.js65
-rw-r--r--modules/ext/editor.js4
-rw-r--r--modules/ext/tutorial.js277
7 files changed, 383 insertions, 102 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index 602ae3e..f6fadb5 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -543,42 +543,6 @@ RULES:
}
return null;
}
- _parseToolBlocksFromText(text) {
- if (!text) return [];
- const toolCalls = [];
- // Match ```tool name(args) or ```tool\nname(args) blocks
- const blockRegex = /```tool(?:\s*\n)?\s*(\w+)\s*({[^}]*})?\s*```/gi;
- let match;
- while ((match = blockRegex.exec(text)) !== null) {
- const name = match[1];
- let args = {};
- try {
- if (match[2]) args = JSON.parse(match[2]);
- } catch (e) {
- // If JSON parse fails, treat as raw string
- args = match[2] ? { path: match[2].replace(/[{}]/g,'').trim() } : {};
- }
- toolCalls.push({
- id: 'text_' + Date.now() + '_' + toolCalls.length,
- type: 'function',
- function: { name, arguments: args }
- });
- }
- // Also handle single-line format: ```tool get_status{}
- const simpleRegex = /```tool\s+(\w+)\s*\{([^}]*)\}\s*```/gi;
- while ((match = simpleRegex.exec(text)) !== null) {
- // Avoid duplicates — skip if already caught by blockRegex
- const name = match[1];
- if (!toolCalls.find(tc => tc.function.name === name)) {
- toolCalls.push({
- id: 'text_' + Date.now() + '_' + toolCalls.length,
- type: 'function',
- function: { name, arguments: {} }
- });
- }
- }
- return toolCalls;
- }
_trackToolFailure(toolName, error) {
try {
const key = 'webx11_tool_failures';
@@ -611,67 +575,7 @@ RULES:
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) {
- let text = res.message.content || '';
- // Fallback: parse ```tool blocks from text when model writes them instead of native tool_calls
- let parsedToolCalls = this._parseToolBlocksFromText(text);
- if (parsedToolCalls.length > 0) {
- this.terminal.print(`🔧 Parsed ${parsedToolCalls.length} tool call(s) from text block — turn ${toolTurns}/${this.MAX_TOOL_TURNS} (${thinkTime}s)`, 'info');
- // Extract any non-tool text as pending assistant content
- const cleanText = text.replace(/```tool[\s\S]*?```/g, '').trim();
- if (cleanText) messages.push({ role: 'assistant', content: cleanText });
- messages.push({ role: 'assistant', content: '(calling tool via text block)' });
- let pendingText = cleanText || null;
- const destructiveTools = parsedToolCalls.filter(tc => this.isDestructive(tc.function.name));
- const safeTools = parsedToolCalls.filter(tc => !this.isDestructive(tc.function.name));
- 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.log('WARN', 'AI', `Tool ${fn.name} failed: ${err} (${t1}s)`);
- this._trackToolFailure(fn.name, err);
- this.terminal.print(`📊 Result (${t1}s): ⚠️ Error: ${err}`, 'warning');
- } else {
- this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`);
- this.terminal.print(`📊 Result (${t1}s): ${obs}`, 'info');
- }
- messages.push({ role: 'tool', content: toolResult });
- }
- 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.log('WARN', 'AI', `Tool ${fn.name} failed: ${dErr} (${t1}s)`);
- this._trackToolFailure(fn.name, dErr);
- this.terminal.print(`📊 Result (${t1}s): ⚠️ Error: ${dErr}`, 'warning');
- } else {
- this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`);
- 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.` });
- }
- }
- if (pendingText) messages.push({ role: 'assistant', content: pendingText });
- continue;
- }
+ 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');
@@ -734,6 +638,21 @@ RULES:
});
}
}
+// Validator: check safe tool results for consistency
+ if (safeResults.length > 0 && pendingText) {
+ const om = this.terminal.ollamaManager;
+ if (om && om.validatorEnabled && om.validatorModel) {
+ let anyInvalid = false;
+ for (const { fn, toolResult } of safeResults) {
+ const valid = await om.validateToolResult(fn.name, fn.arguments, toolResult, pendingText || '');
+ if (!valid && !this._cancelled) {
+ anyInvalid = true;
+ this.terminal.print('🔍 Validator: ' + fn.name + ' result — AI note may not match (logged)', 'info');
+ // Just log it — don't force retry (cloud models get 400 on retry)
+ }
+ }
+ }
+ }
if (pendingText) {
messages.push({ role: 'assistant', content: pendingText });
}
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index 3a32f38..cc3a79c 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -156,7 +156,7 @@
// Phase 4: Ext modules (filtered)
if (typeof webx11._loadExtModules === 'function') {
- const extKeys = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip'];
+ const extKeys = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'tutorial'];
const extSelected = selected
? extKeys.filter(k => isSelected(k + '.js', selected))
: extKeys;
diff --git a/modules/core/module-system.js b/modules/core/module-system.js
index b561ef2..29a9a87 100644
--- a/modules/core/module-system.js
+++ b/modules/core/module-system.js
@@ -207,7 +207,7 @@
ai: ['ai','ask','ai-clear','ai-system','ai-context','ai-allow','ai-deny','ai-stop','ai-model'],
search: ['search','s','ddg','duck','fetch','web','extract'],
tools: ['calc','cron-every','cron-list','cron-stop','cron-clear','wpm','mindmap','theme','voice','alias','unalias',
- 'which','env','date','cal','yes','seq','base64','printf','cut','uniq','hostname','whoami','basename','dirname','sleep','expand'],
+ 'which','env','date','cal','yes','seq','base64','printf','cut','uniq','hostname','whoami','basename','dirname','sleep','expand','tutorial'],
system: ['help','clear','cls','echo','mem','status','font-size','setfont','smart-prompt','logs','preview','factory-reset','reset-vfs','export','unset','script','set','test'],
net: ['server','servers','server-check','server-remove','monitor'],
mods: ['modules','listmods','loadmod','removemod','load-local','loadlocal','startup-modules'],
diff --git a/modules/core/ollama-manager.js b/modules/core/ollama-manager.js
index 34ece29..420cbc3 100644
--- a/modules/core/ollama-manager.js
+++ b/modules/core/ollama-manager.js
@@ -927,6 +927,28 @@ class OllamaManager {
this.saveSettings();
}
+ async validateToolResult(toolName, args, result, aiAnswer) {
+ if (!this.validatorEnabled || !this.validatorModel) return true;
+ const endpoint = this.validatorEndpoint || this.endpoint;
+ const prompt = 'Tool called: ' + toolName + '(' + JSON.stringify(args) + ')\n' +
+ 'Tool returned: ' + ((result || '').substring(0, 500)) + '\n' +
+ 'AI answered: ' + ((aiAnswer || '').substring(0, 500)) + '\n' +
+ 'QUESTION: Is the AI answer consistent with the tool result?\nAnswer only YES or NO.';
+ try {
+ const res = await fetch(endpoint + '/api/chat', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ model: this.validatorModel, messages: [{ role: 'user', content: prompt }], stream: false })
+ });
+ const data = await res.json();
+ const answer = (data.message?.content || '').trim().toUpperCase();
+ return answer === 'YES';
+ } catch (e) {
+ if (this.terminal) this.terminal.print('⚠️ Validator error: ' + e.message, 'warning');
+ return true;
+ }
+ }
+
getModels() { return this.models; }
addResponseHook(fn) { this.responseHooks.push(fn); }
diff --git a/modules/core/terminal.js b/modules/core/terminal.js
index 5e6db6a..de2f4a7 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -211,6 +211,7 @@
this.suggestions = [];
this._isCdAutocomplete = false;
this._autocompletePrefix = '';
+ this._welcomeShown = false;
this.initEventListeners();
this.updateNetworkStatus();
this.loadBuiltInModules();
@@ -226,6 +227,8 @@
soundBtn.textContent = localStorage.getItem('webx11_config_ai_sound') === 'off' ? '🔇 Muted' : '🔊 Sound';
}
setTimeout(() => this.inputField.focus(), 100);
+ // First-run welcome check — fires after ext modules are loaded
+ setTimeout(() => this._checkFirstRun(), 2000);
}
initEventListeners() {
this.inputField.addEventListener('keydown', (e) => {
@@ -943,7 +946,7 @@
catch (e) { console.warn(`[SafeRegister] Skipped ${label}: ${e.message}`); }
}
async _loadExtModules(filterKeys) {
- const autoLoadKeys = filterKeys || ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell'];
+ const autoLoadKeys = filterKeys || ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial'];
const cacheVer = localStorage.getItem('webx11_cache_ver') || '2026-05-27-14';
try {
const res = await fetch('packages.json?v=' + cacheVer);
@@ -1048,11 +1051,34 @@
for (const modName in this.moduleManager.modules) {
const mod = this.moduleManager.modules[modName];
if (mod.commands && mod.commands[cmd]) {
- try { await mod.commands[cmd](args); return; }
+ try { await mod.commands[cmd](args);
+ // Notify tutorial module for quest tracking
+ if (this.tutorialModule && cmd !== 'tutorial') this.tutorialModule.onCommandExecuted(cmd, args);
+ return; }
catch (err) { this.print(`Error in ${modName}.${cmd}: ${err.message}`, 'error'); return; }
}
}
- // No AI routing here — security manager handles that via checkAndRun
+ // Enhanced: check fuzzy suggestions before showing "not found"
+ const suggestion = this.security.suggest(cmd);
+ if (suggestion && suggestion.dist <= 2) {
+ this.print(`Command not found: ${cmd}`, 'error');
+ this.print(`Did you mean: ${suggestion.cmd}? (yes) (no)`, 'info');
+ // Render clickable yes/no
+ const out = this.outputContainer;
+ if (out) {
+ const yesSpan = document.createElement('span');
+ yesSpan.textContent = ' [yes] ';
+ yesSpan.style.cssText = 'color:var(--success-color);cursor:pointer;text-decoration:underline;margin-right:12px';
+ yesSpan.onclick = () => this.executeCommand(suggestion.cmd + (args.length ? ' ' + args.join(' ') : ''));
+ const noSpan = document.createElement('span');
+ noSpan.textContent = ' [no] ';
+ noSpan.style.cssText = 'color:var(--error-color);cursor:pointer;text-decoration:underline';
+ noSpan.onclick = () => { /* do nothing — user declined */ };
+ const line = out.lastElementChild;
+ if (line) { line.appendChild(yesSpan); line.appendChild(noSpan); }
+ }
+ return;
+ }
this.print(`Command not found: ${cmd}`, 'error');
}
openFileBrowser() {
@@ -1675,6 +1701,39 @@
}
} catch (e) {}
}
+ // ── First-run welcome ──
+ _checkFirstRun() {
+ if (localStorage.getItem('webx11_tutorial_complete') || localStorage.getItem('webx11_tutorial_skipped')) return;
+ // Check if tutorial module is loaded — retry if ext modules not ready yet
+ const tutMod = this.moduleManager.modules['TutorialModule'];
+ if (!tutMod) {
+ // Ext modules may still be loading; retry in 2s
+ if (!this._welcomeRetry) {
+ this._welcomeRetry = true;
+ setTimeout(() => this._checkFirstRun(), 2000);
+ }
+ return;
+ }
+ this.tutorialModule = tutMod;
+ // Only show welcome if user hasn't explicitly dismissed it
+ if (!localStorage.getItem('webx11_welcome_dismissed')) {
+ this.showWelcomeModal();
+ }
+ }
+ showWelcomeModal() {
+ const modal = document.getElementById('welcome-modal');
+ if (modal) {
+ modal.style.display = 'flex';
+ this._welcomeShown = true;
+ }
+ }
+ dismissWelcome() {
+ const modal = document.getElementById('welcome-modal');
+ if (modal) modal.style.display = 'none';
+ localStorage.setItem('webx11_welcome_dismissed', '1');
+ // Also mark tutorial as skipped so it doesn't reappear
+ if (this.tutorialModule) this.tutorialModule.skipTutorial();
+ }
log(level, source, message) {
const d = new Date();
const ts = d.toISOString().split('T')[1].split('.')[0];
diff --git a/modules/ext/editor.js b/modules/ext/editor.js
index c597dd5..8c3ac0f 100644
--- a/modules/ext/editor.js
+++ b/modules/ext/editor.js
@@ -44,6 +44,10 @@ class EditorModule {
const ok = this.terminal.fs.writeFile(this._vfsPath, code);
if (ok) {
this.terminal.print('✅ Saved to ' + this._vfsPath, 'success');
+ // Notify tutorial module for quest 4 (edit + save)
+ if (this.terminal.tutorialModule) {
+ this.terminal.tutorialModule.onFileSaved(this._vfsPath);
+ }
} else {
this.terminal.print('❌ Failed to save ' + this._vfsPath, 'error');
}
diff --git a/modules/ext/tutorial.js b/modules/ext/tutorial.js
new file mode 100644
index 0000000..8983322
--- /dev/null
+++ b/modules/ext/tutorial.js
@@ -0,0 +1,277 @@
+class TutorialModule {
+ constructor() {
+ this.commands = {};
+ this.terminal = null;
+ this.STORAGE_KEY = 'webx11_tutorial_state';
+ this.COMPLETE_KEY = 'webx11_tutorial_complete';
+ this.SKIP_KEY = 'webx11_tutorial_skipped';
+ this._maxTries = 3;
+ this._initState();
+ }
+
+ _initState() {
+ // Restore saved state or start fresh
+ const saved = localStorage.getItem(this.STORAGE_KEY);
+ if (saved) {
+ try {
+ const s = JSON.parse(saved);
+ this._completed = new Set(s.completed || []);
+ this._activeIndex = s.activeIndex || 0;
+ this._tries = s.tries || {};
+ this._started = s.started || false;
+ return;
+ } catch(e) { /* corrupt, reset */ }
+ }
+ this._completed = new Set();
+ this._activeIndex = 0;
+ this._tries = {};
+ this._started = false;
+ }
+
+ _saveState() {
+ try {
+ localStorage.setItem(this.STORAGE_KEY, JSON.stringify({
+ completed: Array.from(this._completed),
+ activeIndex: this._activeIndex,
+ tries: this._tries,
+ started: this._started
+ }));
+ } catch(e) { /* quota */ }
+ }
+
+ _getQuests() {
+ return [
+ { id: 1, title: 'Your First Command', desc: 'Type `help` to see every command available in newOS4.',
+ check: (cmd) => cmd === 'help', hint: 'Just type: help' },
+ { id: 2, title: 'Explore the Filesystem', desc: 'Type `ls` to list files in the current directory.',
+ check: (cmd) => cmd === 'ls', hint: 'Type: ls' },
+ { id: 3, title: 'Create Something', desc: 'Use `touch hello.txt` to create your first file.',
+ check: (cmd) => cmd === 'touch',
+ hint: 'Type: touch hello.txt' },
+ { id: 4, title: 'Write to a File', desc: 'Open the editor with `edit hello.txt`, type something, and click Save.',
+ check: (cmd, term) => { const c = term && term.fs && term.fs.readFile('hello.txt'); return c && c.length > 0; },
+ hint: 'Type: edit hello.txt, then write something and click 💾 Save & Test' },
+ { id: 5, title: 'Talk to the AI', desc: 'Type `ai "hello"` to greet the AI assistant.',
+ check: (cmd) => cmd === 'ai' || cmd === 'ask',
+ hint: 'Type: ai "hello"' },
+ { id: 6, title: 'Check Your Progress', desc: 'Type `status` to see how the system is doing.',
+ check: (cmd) => cmd === 'status',
+ hint: 'Type: status' },
+ ];
+ }
+
+ requirements() {
+ return { met: true, missing: [] };
+ }
+
+ getHelpInfo() {
+ return {
+ 'tutorial': 'tutorial [start|skip|status] — Interactive guided tour of newOS4 (6 quests)',
+ };
+ }
+
+ registerCommands() {
+ this.commands['tutorial'] = (args) => this._handleTutorial(args);
+ }
+
+ // ── Public API called by terminal.js ──
+
+ /** Called after every command execution to check quest progress */
+ onCommandExecuted(cmd, args) {
+ if (!this._started || this.isComplete()) return;
+ const quests = this._getQuests();
+ const current = quests[this._activeIndex];
+ if (!current || this._completed.has(current.id)) return;
+
+ // Special handling: quest 4 (edit) — don't count as wrong, wait for save
+ if (current.id === 4 && cmd === 'edit') {
+ this.terminal.print(' ✅ Editor opened! Write something and click Save.', 'success');
+ return; // Don't fail, don't complete — wait for onFileSaved
+ }
+
+ // Check if this command completes the current quest
+ if (current.check(cmd, this.terminal)) {
+ this._completeCurrentQuest();
+ } else {
+ // Wrong command — increment try count
+ const count = (this._tries[current.id] || 0) + 1;
+ this._tries[current.id] = count;
+ this._saveState();
+ if (count >= this._maxTries) {
+ this.terminal.print(`💡 Hint: ${current.hint}`, 'warning');
+ this._tries[current.id] = 0; // reset so hint repeats
+ }
+ }
+ }
+
+ /** Called by editor module after a file is saved via the modal */
+ onFileSaved(path) {
+ if (!this._started || this.isComplete()) return;
+ const quests = this._getQuests();
+ const current = quests[this._activeIndex];
+ if (!current || current.id !== 4 || this._completed.has(current.id)) return;
+
+ if (current.check('__file_saved__', this.terminal)) {
+ this._completeCurrentQuest();
+ }
+ }
+
+ _completeCurrentQuest() {
+ const quests = this._getQuests();
+ const current = quests[this._activeIndex];
+ this._completed.add(current.id);
+ const nextIdx = this._activeIndex + 1;
+ const next = quests[nextIdx];
+ if (next) {
+ this._activeIndex = nextIdx;
+ this._saveState();
+ this._showQuestNext(next);
+ } else {
+ this._activeIndex = nextIdx;
+ this._saveState();
+ this._showCompletion();
+ }
+ }
+
+ isComplete() {
+ return localStorage.getItem(this.COMPLETE_KEY) !== null;
+ }
+
+ isSkipped() {
+ return localStorage.getItem(this.SKIP_KEY) !== null;
+ }
+
+ isStarted() {
+ return this._started;
+ }
+
+ /** Called by welcome modal to begin the tutorial */
+ startTutorial() {
+ this._started = true;
+ this._completed = new Set();
+ this._activeIndex = 0;
+ this._tries = {};
+ this._saveState();
+ localStorage.removeItem(this.COMPLETE_KEY);
+ localStorage.removeItem(this.SKIP_KEY);
+ this.terminal.print('╔══════════════════════════════════════╗', 'success');
+ this.terminal.print('║ 🎮 newOS4 Tutorial Started! ║', 'success');
+ this.terminal.print('╚══════════════════════════════════════╝', 'success');
+ this.terminal.print('Follow the quests below. Type the correct command to advance.', 'info');
+ this._showQuestNext(this._getQuests()[0]);
+ }
+
+ skipTutorial() {
+ localStorage.setItem(this.SKIP_KEY, '1');
+ this._started = false;
+ this._completed = new Set();
+ this._activeIndex = 0;
+ this._tries = {};
+ localStorage.removeItem(this.STORAGE_KEY);
+ this.terminal.print('Tutorial skipped. Type `tutorial start` to begin anytime, or `help` to see all commands.', 'info');
+ }
+
+ /** Show status of tutorial progress */
+ showStatus() {
+ const quests = this._getQuests();
+ const total = quests.length;
+ const done = this._completed.size;
+
+ if (!this._started && !this.isComplete()) {
+ this.terminal.print('📋 Tutorial has not been started. Type `tutorial start` to begin!', 'info');
+ return;
+ }
+ if (this.isComplete()) {
+ this.terminal.print('🎉 Tutorial complete! All ' + total + ' quests finished.', 'success');
+ return;
+ }
+
+ this.terminal.print('📋 Tutorial Progress: ' + done + '/' + total + ' quests', 'info');
+ quests.forEach((q, i) => {
+ const status = this._completed.has(q.id) ? '✅' : (i === this._activeIndex ? '👉' : '⬜');
+ this.terminal.print(' ' + status + ' Quest ' + q.id + ': ' + q.title);
+ });
+ if (this._activeIndex < total) {
+ this.terminal.print('');
+ this._showQuestNext(quests[this._activeIndex]);
+ }
+ }
+
+ // ── Internal ──
+
+ _handleTutorial(args) {
+ if (!args || args.length === 0) {
+ // No args — show status or start if first time
+ if (!this._started && !this.isComplete()) {
+ this.terminal.print('╔══════════════════════════════════════╗', 'info');
+ this.terminal.print('║ 🎮 newOS4 Interactive Tutorial ║', 'info');
+ this.terminal.print('╚══════════════════════════════════════╝', 'info');
+ this.terminal.print('Learn newOS4 by completing 6 short quests.', 'info');
+ this.terminal.print('');
+ this.terminal.print('Commands:', 'info');
+ this.terminal.print(' tutorial start — Begin or restart the tutorial');
+ this.terminal.print(' tutorial status — Show your progress');
+ this.terminal.print(' tutorial skip — Dismiss the tutorial');
+ return;
+ }
+ this.showStatus();
+ return;
+ }
+
+ const sub = args[0].toLowerCase();
+ if (sub === 'start') this.startTutorial();
+ else if (sub === 'skip') this.skipTutorial();
+ else if (sub === 'status') this.showStatus();
+ else this.terminal.print('Usage: tutorial [start|skip|status]', 'warning');
+ }
+
+ _showQuestNext(quest) {
+ this.terminal.print('── Quest ' + quest.id + '/' + this._getQuests().length + ': ' + quest.title + ' ──', 'info');
+ this.terminal.print(' ' + quest.desc, 'text-muted');
+ this.terminal.print(' 💡 ' + quest.hint, 'warning');
+ }
+
+ _showCompletion() {
+ this.terminal.print('');
+ this.terminal.print('🎉🎉🎉 CONGRATULATIONS! 🎉🎉🎉', 'success');
+ this.terminal.print('═'.repeat(40), 'success');
+ this.terminal.print(' All 6 quests complete!', 'success');
+ this.terminal.print(' You now know the basics of newOS4.', 'success');
+ this.terminal.print('═'.repeat(40), 'success');
+ this.terminal.print('');
+ this.terminal.print('What next?', 'info');
+ this.terminal.print(' • Explore commands: help', 'info');
+ this.terminal.print(' • Full docs: help.html', 'info');
+ this.terminal.print(' • AI assistant: ai \"your question\"', 'info');
+ this.terminal.print(' • Schedule tasks: cron-every 60 echo hello', 'info');
+ this.terminal.print(' • Install packages: wpm list', 'info');
+ this.terminal.print(' • Manage files: edit, find, grep, files', 'info');
+
+ localStorage.setItem(this.COMPLETE_KEY, Date.now().toString());
+ localStorage.removeItem(this.STORAGE_KEY);
+
+ // Fire confetti
+ this._fireConfetti();
+ }
+
+ _fireConfetti() {
+ const emojis = ['🎉', '✨', '⭐', '🌟', '💫', '🎊', '🔷', '🟢', '🔶'];
+ const container = document.body;
+ if (!container) return;
+ for (let i = 0; i < 40; i++) {
+ const el = document.createElement('div');
+ el.className = 'confetti-piece';
+ el.textContent = emojis[i % emojis.length];
+ el.style.left = (Math.random() * 100) + 'vw';
+ el.style.animationDuration = (2 + Math.random() * 2) + 's';
+ el.style.animationDelay = (Math.random() * 1.5) + 's';
+ el.style.fontSize = (16 + Math.random() * 20) + 'px';
+ container.appendChild(el);
+ setTimeout(() => el.remove(), 5000);
+ }
+ }
+}
+
+if (typeof window.webx11 !== 'undefined' && window.webx11) {
+ webx11.moduleManager.registerModule(new TutorialModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/tutorial.js');
+}